Efficiently Building iOS Apps: Unleashing the Power of SPM, MVVM, SwiftUI, and Combine/Async-Await

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

Kanagasabapathy Rajkumar
6 min readJan 4, 2024
iOSNetworkExample

Prerequisites

Please refer to the previous articles of the Networking series

This article has been updated and moved to my website here

Swift Package Integration

I have published my package on Swift Package Index. Please use this — https://github.com/sabapathyk7/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]

GitHub Repo

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

  • ObservableObject — The SwiftUI view to update when the object’s (ViewModel) published properties change.
  • @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
}
}
}
}
}
  • Result — A value that represents either a success or a failure, including an associated value in each case.
  • 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
  • In the Completion — finished and failure are the two cases available. In the Self.Output returns the output or result data.
  • store(in:) — Stores this type-erasing cancellable instance in the specified set.
  • cancellable/AnyCancellable — Refer to this LinkedIn post
  • Cancellable — allows us to manage and terminate the subscribers.
  • 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
}
}
}
  • async — asynchronous and the method performs asynchronous work
  • await — Await is awaiting a callback from async
  • 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

Grateful for your read, now let’s code on

Interested in connecting? 
Feel free to connect with me on: LinkedIn or follow on GitHub

Please take a look at my first-ever Article 👇

--

--

Kanagasabapathy Rajkumar
Kanagasabapathy Rajkumar

Written by Kanagasabapathy Rajkumar

Swift Enthusiast | Building Seamless iOS Experiences 🚀 | Swift & Objective-C |