ArticlesiOSBoost development with reusable Swift Packages

Boost Development with Reusable Swift Packages

Introduction

Let’s take a deep dive into AsyncSession and Components our in-house packages designed to enhance productivity and reduce development time.

Supercharging your iOS projects with modular Swift Packages!

Hey there, fellow iOS developer! In the ever-changing world of app development, we’re always on the hunt for ways to make our code more efficient, reusable, and scalable. Whether you’re cooking up a brand-new app or spicing up an existing one, organizing your code can save you tons of time and headaches later on. That’s where Swift Package Manager swoops in to save the day!

What’s Swift Package Manager Anyway?

Think of SPM as your trusty sidekick that’s built right into Xcode. It helps you chop your app’s codebase into bite-sized, reusable pieces called packages. Instead of the old copy-paste routine between projects (we’ve all been there 😅), you create a package once and reuse it wherever you need.

Why Go Modular?

Imagine building with LEGO blocks. Instead of crafting every piece from scratch, you snap together pre-made blocks to build something awesome. That’s the magic of modular development! By breaking your code into modules, you can:

  1. Reuse code across projects effortlessly. ♻️
  2. Keep your code neat and tidy, reducing that dreaded technical debt. 🧹
  3. Make maintenance and testing a breeze, since each module stands on its own. 🧪

The Perks of Using SPM

Here’s why SPM is the go-to for many iOS devs:

  • Seamless Xcode integration: Adding, removing, and managing packages is smooth sailing. ⛵
  • Version control: Lock in package versions to prevent unexpected surprises when updates roll out. 🔒
  • Automatic dependency management: Let SPM handle the heavy lifting with dependencies. No more conflicts or outdated libraries! 🔄

Real-Life Wins with SPM

Picture this: you have a nifty package for handling network requests AsyncSession and another for cool UI elements Components. Instead of duplicating these gems in every project, turn them into Swift Packages! Now, they’re plug-and-play tools you can summon whenever you need.

In this guide, we’ll explore how to leverage modular development using two Swift Packages: AsyncSession and UIComponents. They’re designed to streamline your code, boost reusability, and help you ship projects faster. Let’s dive in!

Setting Up Swift Package Manager

Ready to get started with SPM? Whether you’re kicking off a new project or enhancing an existing one, adding reusable components has never been easier. Let’s jump right in! 🏃‍♂️

Adding AsyncSession and Components Packages to Your Xcode Project

First up, let’s add the two packages we’ll be working with:

Here’s how to set them up:

  1. Open your project in Xcode. Let’s get this show on the road! 🎬

  2. In the Project Navigator, click on your project’s root folder (probably named after your app).

  3. Select the Package Dependencies tab. This is where the magic happens! ✨

  4. Click the + button to add a new package dependency.

  5. Enter the package URL when prompted:

    • For AsyncSession, paste:

      https://github.com/mtdtechnology-net/async-url-session
    • For UIComponents, paste:

      https://github.com/mtdtechnology-net/swiftui-components
  6. Set the Version to “Up to Next Major Version”. This way, you’ll get all the latest goodies without worrying about breaking changes. 👍

  7. Click Add Package and let Xcode fetch everything. Time for a quick coffee break? ☕️

And that’s it! 🎉 You’ve successfully added the packages to your project. Now they’re ready to use whenever you need them.

Using AsyncSession for Secure Networking

Now that we’ve got AsyncSession added to our project, let’s put it to work! 🛠️ We’ll create a SecureSession class that inherits from AsyncSession. This subclass will automatically add custom headers, like an authorization token, to all your network requests. Let’s dive in!

Extending AsyncSession to Create a Secure Session

We’ll start by creating a SecureSession class that inherits from AsyncSession. This subclass will add custom headers, like an authorization token, to all requests.

import Foundation
import AsyncSession
 
/// A secure session that automatically handles authorization headers for network requests.
class SecureSession: AsyncSession {
 
    // MARK: - Init
 
    init() {
        super.init()
    }
 
    // Override the `request` method to inject authorization headers
    override func request(url: URL, method: Method, headers: [String: String]) async throws -> URLRequest {
        var headers = headers
        headers["Content-Type"] = "application/json"
        headers["Authorization"] = "Bearer your_custom_token"
        return try await super.request(url: url, method: method, headers: headers)
    }
}

Explanation:

  • We override the request method to inject the Authorization and Content-Type headers into every request.

Creating a Service Layer Using SecureSession

Now, let’s use our SecureSession within a service class to handle your API calls. This service will encapsulate network interactions, making it easy to reuse throughout your app.

class YourService {
 
    // MARK: - Private Properties
 
    private let session: SecureSession
 
    // MARK: - Init
 
    init(session: SecureSession = SecureSession()) {
        self.session = session
    }
}
 
extension YourService {
 
    /// Fetches data using a GET request.
    func fetchData<T: Decodable>(from url: URL) async throws -> T {
        return try await session.get(url: url, headers: [:])
    }
 
    /// Sends data using a POST request.
    func sendData<T: Decodable, U: Encodable>(to url: URL, body: U) async throws -> T {
        return try await session.post(url: url, headers: [:], body: body)
    }
}

Explanation:

  • We define a service called YourService that uses SecureSession.
  • The fetchData method performs a GET request, while sendData handles a POST request with a request body.
  • By using generics (T: Decodable and U: Encodable), we make the service flexible for any type of response and request body.

Using the Service in Your SwiftUI Project

Now, let’s see how to leverage YourService within your SwiftUI views.

// MARK: - Model
 
struct Post: Decodable {
    let id: Int
    let title: String
    let body: String
}
 
struct ContentView: View {
    // MARK: - Properties
    @State private var message: String = "Press the button to fetch data"
    private let service = YourService()
    
    // MARK: - Body
    var body: some View {
        VStack {
            Text(message)
                .padding()
            
            Button("Fetch Data") {
                Task {
                    do {
                        let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1")!
                        let post: Post = try await service.fetchData(from: url)
                        message = "Title: \(post.title)"
                    } catch {
                        message = "Failed to fetch data: \(error.localizedDescription)"
                    }
                }
            }
            .padding()
        }
        .padding()
    }
}

Explanation:

  • The button triggers a network call to fetch data using YourService.
  • The response is displayed in the UI.

While this implementation works, it has room for improvement. Let’s dive into the next section.

Combining AsyncSession and Components to Build a Better Data Fetcher in SwiftUI

Refactoring with a ViewModel

Time to give our app a makeover! 🎨 Using the Components package, we’ll design a polished interface with an input field and a styled button. Plus, we’ll refactor our code to separate business logic from the UI. Let’s get started! 🏗️

To keep our code clean and maintainable, we’ll move the data-fetching logic into a separate ViewModel. This adheres to the MVVM (Model-View-ViewModel) pattern, which separates concerns and makes testing easier.

import SwiftUI
 
class DataFetcherViewModel: ObservableObject {
 
    // MARK: - Properties
 
    @Published var message: String = ""
    @Published var isLoading: Bool = false
    private let service: YourService
 
    // MARK: - Init
 
    init(service: YourService = YourService()) {
        self.service = service
    }
 
    // MARK: - Request
 
    @MainActor
    func fetchData(for id: String) {
        isLoading = true
        Task {
            defer { isLoading = false }
            do {
                let url = URL(string: "https://jsonplaceholder.typicode.com/posts/\(id)")!
                let post: Post = try await service.fetchData(from: url)
                self.message = "Title: \(post.title)\nBody: \(post.body)"
            } catch {
                self.message = "Failed to fetch data: \(error.localizedDescription)"
            }
        }
    }
}

Explanation:

  • The fetchData method now uses id to construct the URL.
  • We keep network logic out of the view, improving separation of concerns.

Enhancing the User Interface

Using Components, we’ll design a polished interface with an input field and a styled button.

import SwiftUI
import Components
 
struct Post: Decodable {
    let id: Int
    let title: String
    let body: String
}
 
struct ContentView: View {
 
    // MARK: - Properties
 
    @StateObject private var viewModel = DataFetcherViewModel()
    @State private var inputText: String = ""
 
    // MARK: - Body
 
    var body: some View {
        VStack(spacing: 16) {
            InputView(
                title: "Enter Post ID",
                color: .blue,
                systemImage: "number.circle",
                inputBackground: Color.gray.opacity(0.2),
                inputOverlay: Color.blue.opacity(0.5)
            ) {
                TextField("Post ID", text: $inputText)
                    .keyboardType(.numberPad)
                    .textFieldStyle(PlainTextFieldStyle())
            }
 
            PrimaryButton(
                label: "Fetch Data",
                foregroundColor: .blue,
                textColor: .white,
                isLoading: viewModel.isLoading,
                action: { viewModel.fetchData(for: inputText) }
            )
 
            if !viewModel.message.isEmpty {
                Text(viewModel.message)
                    .padding()
                    .background(Color.gray.opacity(0.2))
                    .cornerRadius(8)
                    .multilineTextAlignment(.leading)
            }
 
            Spacer()
        }
        .padding()
        .navigationTitle("Data Fetcher")
    }
}

Explanation:

  • We use InputView and PrimaryButton from the Components package.
  • These components provide a consistent look and feel across the app.
  • The UI is now cleaner and more user-friendly.

Admiring the Improved UI

Run your app to see the new interface in action! Your app now has:

  • A sleek input field where users can type their queries.
  • A stylish button that fits the overall theme of your app.
  • A refined display for showing the fetched data.

Conclusion - The Power of Modular Development

By leveraging Swift Packages like AsyncSession and Components, we’ve transformed our app into a more organized, reusable, and scalable project. Here’s what we’ve accomplished:

  • Modular Networking: Created a secure networking layer that’s reusable across projects.
  • Clean Architecture: Adopted the MVVM pattern for better code maintenance.
  • Enhanced UI: Improved the user interface with custom components for a better user experience.

Embracing modular development not only accelerates your development process but also makes your codebase cleaner and more maintainable. So next time you’re starting a new project or refactoring an existing one, consider breaking it down into Swift Packages. You’ll thank yourself later! 🙌


Written by @marcelfagadariu