Combine and SwiftUI

Swift was introduced at the WWDC 2014 Developers Conference. Apple recommends using the MVC architecture. As practice has shown, the usual MVC architecture is not enough for complex applications. At WWDC 2019, developers are shown SwiftUI and Combine, which should make an iOS developer's life better. Apple now recommends using the MVVM architecture. And now is the time to start understanding these technologies.

Combine

Main idea is Publishers send data to one or more subscribers, the data goes through Operators to Subscribers. Let's look in more details into terms used in this technology.

Publishers

Publishers send values to one or more Subscribers. They conform to the Publisher protocol and declare the output type and any error they produce.

Subscribers

Subscribers subscribe to one specific instance of the Publisher and receive data until the subscription is canceled. They conform to the Subscriber protocol.

Operators

Operators are needed to change data. For example, filter out nil, format data, etc.

Lets start

I'll show you an example using reactive code. Let's start with a model that we will fill from the API.

import Foundation
class TenantModel: NSObject, Codable {
var name: String?
var code: String?
var dateFormat: String?
var timeFormat: String?
var advanceDaysCount: Int?
var historyDaysCount: Int?
var features: [String]?
var timeSlots: [TimeSlot]?
var timezone: String?
var country: String?
var displayUserNamesInBooking: Bool?
var allowMultiSlotBookings: Bool?
var allowMultipleBookingsPerUser: Bool?
var provideNoteFieldInBookings: Bool?
var noteFieldInBookingsIsRequired: Bool?
var noteFieldInBookingsHelpText: String?
var accessRestrictionStart: String?
var accessRestrictionEnd: String?
var bookingRestrictionStart: String?
var bookingRestrictionEnd: String?
}
class TimeSlot: NSObject, Codable {
let id: Int?
let startTime: String?
let endTime: String?
}

Network requests

I will use URLSession for requests

func perform<T: Decodable>(_ request: URLRequest) -> AnyPublisher<HTTPResponse<T>, Error> {
return URLSession.shared.dataTaskPublisher(for: request)
.tryMap { data, response -> HTTPResponse<T> in
guard let httpResponse = response as? HTTPURLResponse,
200...299 ~= httpResponse.statusCode else {
throw APIError.responseError(((response as? HTTPURLResponse)?.statusCode ?? 500,
String(data: data, encoding: .utf8) ?? ""))
}
let data = try JSONDecoder().decode(T.self, from: data)
return HTTPResponse(value: data, response: response)
}
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
view raw perform.swift hosted with ❤ by GitHub
func getTenants(_ userID: String) -> AnyPublisher<[TenantModel], Error>? {
guard let url = Endpoint.userTenants(userID).absoluteURL else { return nil }
var request = URLRequest(url: url)
request.httpMethod = HTTPMethod.get.rawValue
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(Token.access ?? "")", forHTTPHeaderField: "Authorization")
return HTTPClient.perform(request)
.map(\.value)
.eraseToAnyPublisher()
}

Note the return type: AnyPublisher & lt; [TenantModel], Error & gt; is our publisher. We subscribe to it and receive data already in the ViewModel

import Foundation
import Combine
class TenantViewModel: ObservableObject {
@Published var tenants: [TenantModel] = []
private var cancellableSet: Set<AnyCancellable> = []
init() {}
func getTenants() {
UserAPI.getTenants(User.idWithToken)?
.sink(
receiveCompletion: { [weak self] completion in
if case let .failure(error) = completion {
switch error {
case let apiError as APIError:
HTTPClient.apiErrorRefreshToken(apiError) {
self?.getTenants()
}
default:
break
}
}
},
receiveValue: { [weak self] tenant in
self?.tenants = tenant
})
.store(in: &self.cancellableSet)
}
}

.sink is a subscriber that receives data. self.tenants = tenants - Assign values to a variable marked with @Publisher. These changes will force the View to be updated

import SwiftUI
struct TenantPicker: View {
@StateObject var viewModel: TenantViewModel = TenantViewModel()
@Binding var selectedTenant: TenantModel?
@Binding var showTenantPicker: Bool
var label: String
var title: String
var body: some View {
HStack {
Button(action: {
viewModel.getTenants()
showTenantPicker.toggle()
}) { HStack(alignment: .center, spacing: 2) {
Text(label)
.foregroundColor(Color(.label))
Spacer()
Text(selectedTenant?.name ?? "Select tenant")
.foregroundColor(Color(.secondaryLabel))
}
}.sheet(isPresented: $showTenantPicker, content: {
NavigationView {
ScrollView {
CustomList {
ForEach(Array(viewModel.tenants.enumerated()), id: \.element) { tenantIndex, tenant in
CustomListItem {
Button {
selectedTenant = tenant
CoreDataManager.shared.saveTenant(tenant)
showTenantPicker = false
} label: {
HStack {
Text("\(tenant.name ?? "")")
.foregroundColor(Color(.label))
Spacer()
}
}
}
if viewModel.tenants.count > 1 &&
viewModel.tenants.count - 1 > tenantIndex {
Divider()
}
}
}.cornerRadius(8)
}
.padding()
.fillBy(Color(.systemGray6))
.navigationBarTitle(title, displayMode: .inline)
.navigationBarItems(leading: Button(action: {
showTenantPicker = false
}, label: {
HStack(spacing: 8) {
Image(systemName: "chevron.left")
Text("Back")
}
}))
}
})
}
}
}

Add the @StateObject wrapper to catch all changes to the ViewModel. Here ForEach (Array (viewModel.tenants ...)) we go through the list, this list will rebuild itself when receiving data over the network.

Here's what we have to obtain:

 

swiftui

 

More like this

Get in touch

Свържете се

Frankfurt am Main, Germany (Sales)

60354

Eckenheimer Schulstraße, 20

+38 (098) 630-49-85

info@a5.ua

Kharkiv, Ukraine (Development)

61023

Trinklera street, 9

+38 (050) 908-31-07

info@a5.ua

Burgas, Bulgaria (Development)

8008

бул. „Транспортна“ 15, Northern Industrial Zone

+359 877 350129

info@a5.ua