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()
}
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()
}

Зверніть увагу на повертається тип: 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 ...)) ми пробігаємо по списку, цей список буде сам перебудовуватися, при отриманні даних через мережу.

Ось що має получитися:

 

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

Харків, Україна

61023

вул. Трінклера, 9

+38 (050) 908-31-07

info@a5.ua

Burgas, Bulgaria (Development)

8008

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

+359 877 350129

info@a5.ua