Combine та SwiftUI
Swift був представлений на конференції розробників WWDC 2014. Apple рекомендує використовувати архітектуру MVC. Як показала практика, що для складних додатків звичайної MVC архітектури недостатньо. На WWDC 2019 розробникам показують SwiftUI і Combine, які повинні зробити життя iOS розробника краще. Тепер Apple рекомендує використовувати архітектуру MVVM. І зараз саме час почати розбиратися з цими технологіями.
Combine
Видавці відправляють дані одному або декільком учасникам, дані проходять через оператори.
Publishers
Publishers відправляють значення одному або декільком передплатникам. & Nbsp; Вони відповідають & nbsp; Publisher протоколу і оголошують тип виведення та будь-яку помилку, яку вони виробляють.
Subscribers
Передплатники підписуватися на один конкретний екземпляр видавця і отримують дані до тих пір, поки передплачених послуг не підтримує буде скасована. Вони відповідають Subscriber протоколу.
Operators
Оператори потрібні для зміни даних. Наприклад, відфільтрувати nil, відформатувати дані і т.д.
Давайте почнемо
Я покажу приклад з використанням реактивного коду. Почнемо з моделі, яку будемо заповнювати з 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? | |
} |
Мережеві запити
Я буду використовувати URLSession для запитів
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() | |
} |
Зверніть увагу на повертається тип: AnyPublisher & lt; [TenantModel], Error & gt; - це наш publisher. Підписуємося на нього і отримувати дані вже в 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 - це сабскрайбер, який отримує дані. self.tenants = tenants - Надаємо значення в змінну позначену як @Publisher. Ці зміни змусять відновити View
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") | |
} | |
})) | |
} | |
}) | |
} | |
} | |
} |
Додаємо врапер @StateObject щоб відловлювати всі зміни ViewModel. Ось тут ForEach (Array (viewModel.tenants ...)) ми пробігаємо по списку, цей список буде сам перебудовуватися, при отриманні даних через мережу.
Ось що має получитися:
