Seamless Integration: A Step-by-Step Guide to Incorporating Swift Package NetworkKit for Robust iOS App Development
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.
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
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
}
}
}
}
}
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
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
}
}
}
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
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.