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
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.
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
- Integrated Swift Package — NetworkKit to our application
- Implemented the MVVM architecture by using dependency injection.
- Integrated — Closures, Async/Await and Combine
- Unit tests the async/await method
- @MainActor vs DispatchQueue.main.async vs Runloop.main
- Error Handling
Let’s incorporate the Swift Package with TCA in the next article.