Combine und SwiftUI
Swift wurde auf der WWDC 2014 Developers Conference vorgestellt. Apple empfiehlt die Verwendung der MVC-Architektur. Wie die Praxis gezeigt hat, reicht die übliche MVC-Architektur für komplexe Anwendungen nicht aus. Auf der WWDC 2019 werden Entwicklern SwiftUI und Combine gezeigt, was das Leben eines iOS-Entwicklers verbessern soll. Apple empfiehlt jetzt die Verwendung der MVVM-Architektur. Und jetzt ist es an der Zeit, diese Technologien zu verstehen.
Combine
Die Hauptidee ist, dass Publisher Daten an einen oder mehrere Abonnenten senden. Die Daten werden über Operatoren an Abonnenten gesendet. Schauen wir uns die in dieser Technologie verwendeten Begriffe genauer an.
Publishers
Publisher senden Werte an einen oder mehrere Abonnenten. Sie entsprechen dem Publisher-Protokoll und deklarieren den Ausgabetyp und alle von ihnen verursachten Fehler.
Subscribers
Abonnenten abonnieren eine bestimmte Instanz des Herausgebers und erhalten Daten, bis das Abonnement gekündigt wird. Sie entsprechen dem Abonnentenprotokoll.
Operators
Operatoren werden benötigt, um Daten zu ändern. Filtern Sie beispielsweise Null heraus, formatieren Sie Daten usw.
Lasst uns beginnen
Ich zeige Ihnen ein Beispiel mit reaktivem Code. Beginnen wir mit einem Modell, das wir über die API ausfüllen.
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
Ich werde URLSession für Anfragen verwenden
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() | |
} |
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() | |
} |
Beachten Sie den Rückgabetyp: AnyPublisher & amp; lt; [TenantModel], Fehler & amp; gt; ist unser Verlag. Wir abonnieren es und erhalten Daten, die bereits im ViewModel enthalten sind
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 ist ein Teilnehmer, der Daten empfängt. self.tenants = Tenants - Weisen Sie einer mit @Publisher gekennzeichneten Variablen Werte zu. Diese Änderungen erzwingen die Aktualisierung der Ansicht
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") | |
} | |
})) | |
} | |
}) | |
} | |
} | |
} |
Fügen Sie den @ StateObject-Wrapper hinzu, um alle Änderungen am ViewModel zu erfassen. Hier für jedes (Array (viewModel.tenants ...)) gehen wir die Liste durch, diese Liste wird sich beim Empfang von Daten über das Netzwerk neu erstellen.
Folgendes müssen wir erhalten:
