Home Articles

Building Modular iOS Apps: A Guide to SPM, MVVM, SwiftUI, and Combine/Async-Await

Seamless Integration: A Step-by-Step Guide to Incorporating Swift Package NetworkKit for Robust iOS App Development

Demo of Building Modular Apps

Swift Package Integration

I have published my package on Swift Package Index. Please use this — https://github.com/sabapathy7/NetworkKit.git to add the dependency.

Integrate the package

Defining Models

Please check this test data — https://jsonplaceholder.typicode.com/todos

Models are building blocks of our app and represent the data that makes the application thoughtful.

Check out this article to understand JSON Parsing.


    import Foundation

    // MARK: - Todo
    struct Todo: Codable, Identifiable {
        let userID, id: Int
        let title: String
        var completed: Bool

        enum CodingKeys: String, CodingKey {
            case userID = "userId"
            case id, title, completed
        }
    }

    typealias Todos = [Todo]

ViewModels

ViewModel can be considered a bridge between the presentation and business logic layers.

It focuses on the Views and also data binding and testing.


    import Foundation

    final class TodoViewModel: ObservableObject {
        @Published var todos: Todos = Todos()
        
        func fetchData() {
            // API calls
            todos = [Todo(userID: 1, id: 1, title: "et labore eos enim rerum consequatur sunt", completed: true),
                    Todo(userID: 2, id: 2, title: "suscipit repellat esse quibusdam voluptatem incidunt", completed: false)]
        }
    }

Breaking down into pieces

  1. ObservableObject — The SwiftUI view to update when the object’s (ViewModel) published properties change.
  2. @Published — A type that publishes a property marked with an attribute.

Dependency Injection for the ViewModel:

ViewModel depends on NetworkKit to fetch network components through our Networkable protocol. Injecting the components into ViewModel yields better testable and modular code.



    final class TodoViewModel: ObservableObject {
        @Published var todos: Todos = Todos()
        @Published var todoError: String = String()
        private var networkService: Networkable
        init(networkService: Networkable = NetworkService() ) {
            self.networkService = networkService
            networkSelection()
        }
    }

Updated API Endpoints


    import Foundation
    import NetworkKit 

    enum TodoEndPoint {
        case todo
    }
    extension TodoEndPoint: EndPoint {
        var host: String {
           return "jsonplaceholder.typicode.com"
        }
        var scheme: String {
            return "https"
        }
        var path: String {
            return "/todos"
        }
        var method: NetworkKit.RequestMethod {
            switch self {
            case .todo:
                return .get
            }
        }
    }

Let’s integrate NetworkKit into our App and create a function that returns our Typical Swift example @escaping closures.

Closures


    import Foundation
    import NetworkKit

    extension TodoViewModel {
        func makeClosureRequest() {
            networkService.sendRequest(endpoint: TodoEndPoint.todo) { (response: Result<Todos, NetworkError>) in
                switch response {
                case .success(let todoData):
                    DispatchQueue.main.async {
                        self.todos = todoData
                    }
                case .failure(let error):
                    DispatchQueue.main.async {
                        self.todosError = error.debugDescription
                    }
                }
            }
        }
    }

  1. Result — A value that represents either a success or a failure, including an associated value in each case.
  2. DispatchQueue.main.async — Runs on the main thread to update the UI.

Combine

Incorporating Reactive Programming Framework. Modern way of handling the asynchronous events and data streams.


    import Combine
    import Foundation
    import NetworkKit

    final class TodoViewModel: ObservableObject {
      // Declaration 
      private var cancellables = Set<AnyCancellable>()
     // Dependency Injection
    }

    extension TodoViewModel {
        func makeCombineRequest() {
            networkService.sendRequest(endpoint: TodoEndPoint.todo, type: Todos.self)
                .receive(on: RunLoop.main)
                .sink { [weak self] completion in
                    switch completion {
                    case .finished:
                        print("Finished")
                    case .failure(let error):
                        self?.todosError = error.debugDescription
                    }
                } receiveValue: { [weak self] todoData in
                    self?.todos = todoData
                }
                .store(in: &cancellables)
        }
    }

sink(receiveCompletion:receiveValue:) — Attaches a subscriber with closure-based behavior.


    func sink(
        receiveCompletion: @escaping ((Subscribers.Completion<Self.Failure>) -> Void),
        receiveValue: @escaping ((Self.Output) -> Void)
    ) -> AnyCancellable

  1. In the Completion — finished and failure are the two cases available. In the Self.Output returns the output or result data.
  2. store(in:) — Stores this type-erasing cancellable instance in the specified set.
  3. cancellable/AnyCancellable — Refer to this LinkedIn post
  4. Cancellable — allows us to manage and terminate the subscribers.
  5. AnyCancellable — Type Erasure — Cancellable for efficient resource cleanup and preventing memory leaks.

Structured Concurrency — Async/Await


    import NetworkKit

    @MainActor
    final class TodoViewModel: ObservableObject {
      // Declaration
      // Dependency Injection 
    }
    extension TodoViewModel {
        func makeAsyncAwaitRequest() async {
            do {
                let todoData = try await networkService.sendRequest(endpoint: TodoEndPoint.todo) as Todos
                if todoData.isNotEmpty {
                    todos = todoData
                }
            } catch {
                guard let error = error as? NetworkError else {
                    return
                }
                todosError = error.debugDescription
            }
        }
    }

  1. async — asynchronous and the method performs asynchronous work
  2. await — Await is awaiting a callback from async
  3. try/catch, guard — error handling

Let’s test the networking individually


    enum RequestVariant {
        case combineFRP
        case asyncAwait
        case escapingClosure
    }

    extension TodoViewModel {
        func networkSelection(variant: RequestVariant) {
            switch variant {
            case .asyncAwait:
                Task(priority: .background) {
                    await makeAsyncAwaitRequest()
                }
            case .combineFRP:
                makeCombineRequest()
            case .escapingClosure:
                makeClosureRequest()
            }
        }
    }

Finally, we have the ViewModel


    @MainActor
    final class TodoViewModel: ObservableObject {
        @Published var todos: Todos = Todos()
        @Published var todosError: String = String()
        private var cancellables = Set<AnyCancellable>()
        private var networkService: Networkable

        init(networkService: Networkable = NetworkService() ) {
            self.networkService = networkService
            networkSelection()
        }
        func networkSelection() {
            print("Executed")
            networkSelection(variant: .escapingClosure)
        }
    }

Data Binding

SwiftUI — Powerful Declarative Framework.

SwiftUI’s data binding is powerful. It works well with the ViewModel — ObservableObject. To prepare for working with SwiftUI, it's recommended to learn more about Property Wrappers first.


    struct ContentView: View {
        @StateObject private var viewModel: TodoViewModel = TodoViewModel()
        var body: some View {
            TodoListView(viewModel: viewModel)
        }
    }
    struct TodoListView: View {
        @ObservedObject var viewModel: TodoViewModel
        @State private var showError = false
        var body: some View {
            VStack {
                List {
                    ForEach(viewModel.todos, id: \.id) { todo in
                        TodoRowView(todo: todo)
                    }
                }
            }
        }
    }

Testing Strategies Writing test cases is a fundamental practice in software development that offers numerous benefits, contributing to the creation and maintenance of clean codebases.

Created a Unit TestCase for our ViewModel class.


    import XCTest
    @testable import NetworkingExample
    import NetworkKit

    final class IosNetworkExampleTests: XCTestCase {
      @MainActor override func setUpWithError() throws {
            try super.setUpWithError()
            sut = TodoViewModel(networkService: MockServiceable())
        }
        override func tearDownWithError() throws {
            sut = nil
            try super.tearDownWithError()
        }
        func testFetchTodosAsync() async {
            var todos = await sut.todos
            await sut.makeAsyncRequest()
            todos = await sut.todos
            XCTAssertGreaterThan(todos.count, 0, "")
            XCTAssertEqual(todos.first?.title, "et labore eos enim rerum consequatur sunt")
        }
    }
    final class MockServiceable: Networkable {
        func sendRequest<T>(endpoint: GenericNetworkFramework.EndPoint) async throws -> T where T: Decodable {
            let todoData = Todo.testTodosData()
            guard let todo = todoData as? T else {
                fatalError("Not TodoData we are expecting")
            }
            return todo
        }
    }
    

MockServiceable is a mock of our NetworkService class available in our NetworkKit — Swift Package. Using Dependency injection, we can use it for mock testing.

Key Takeaways

  1. Integrated Swift Package — NetworkKit to our application
  2. Implemented the MVVM architecture by using dependency injection.
  3. Integrated — Closures, Async/Await and Combine
  4. Unit tests the async/await method
  5. @MainActor vs DispatchQueue.main.async vs Runloop.main
  6. Error Handling

The entire project can be found here

This is a free third party commenting service we are using for you, which needs you to sign in to post a comment, but the good bit is you can stay anonymous while commenting.