Github.com/agelessman/…
Those of you who have not written a complete SwiftUI project should not have used Combine very much. It can be said that Combine is a special tool for processing data. ** If you learn these knowledge, your efficiency in writing SwiftUI programs will definitely increase exponentially.
Many articles have been written before about Publisher, Operator and Subscriber in Combine in detail. I believe that you have a basic understanding of Combine. Today, I will lead you to study the practical application of Combine.
You can find the collection of SwiiftUI and Combine here: FuckingSwiftUI
CombineDemoTest CombineDemoTest
Simulated Web search
The figure above illustrates one of the most common scenarios in development, where a search is performed based on user input in real time. Such a feature looks very simple on the surface, but the internal logic is quite detailed:
- You need to set a network request interval for user input, such as 0.5 seconds after the user stops input before sending a request, to avoid wasting unnecessary network resources
- duplicate removal
- Displaying loading state
Take a look at the home page code:
struct ContentView: View {
@StateObject private var dataModel = MyViewModel(a)@State private var showLogin = false;
var body: some View {
GeometryReader { geometry in
VStack(alignment: .leading, spacing: 0) {
ZStack {
HStack(spacing: 10) {
Group {
if dataModel.loading {
ActivityIndicator()}else {
Image(systemName: "magnifyingglass")
}
}
.frame(width: 30, height: 30)
TextField("Please enter a repository to search for", text: $dataModel.inputText)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button("Login") {
self.showLogin.toggle()
}
}
.padding(.vertical, 10)
.padding(.horizontal, 15)
}
.frame(width: geometry.size.width, height: 44)
.background(Color.orange)
List(dataModel.repositories) { res in
GithubListCell(repository: res)
}
}
}
.sheet(isPresented: $showLogin) {
LoginView()}}}Copy the code
The above code is very simple, there is no data related processing logic, these data processing logic is all in MyViewModel, the nice thing is, if the View depends on MyViewModel, then when MyViewModel data adaptation, the View automatically refresh.
- We use the
@StateObject
Initialize thedataModel
Let the View manage its life cycle - use
GeometryReader
You can get the frame of the parent View GithubListCell
Is the encapsulation of each warehouse cell, the code will not be posted, you can download the code to view
Let’s look at the contents of MyViewModel:
final class MyViewModel: ObservableObject {
@Published var inputText: String = ""
@Published var repositories = [GithubRepository] ()@Published var loading = false
var cancellable: AnyCancellable?
var cancellable1: AnyCancellable?
let myBackgroundQueue = DispatchQueue(label: "myBackgroundQueue")
init(a) {
cancellable = $inputText
//. Debounce (for: 1.0, scheduler: myBackgroundQueue)
.throttle(for: 1.0, scheduler: myBackgroundQueue, latest: true)
.removeDuplicates()
.print("Github input")
.map { input -> AnyPublisher"[GithubRepository].Never> in
let originalString = "https://api.github.com/search/repositories?q=\(input)"
let escapedString = originalString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
let url = URL(string: escapedString)!
return GithubAPI.fetch(url: url)
.decode(type: GithubRepositoryResponse.self, decoder: JSONDecoder())
.map {
$0.items
}
.replaceError(with: [])
.eraseToAnyPublisher()
}
.switchToLatest()
.receive(on: RunLoop.main)
.assign(to: \.repositories, on: self)
cancellable1 = GithubAPI.networkActivityPublisher
.receive(on: RunLoop.main)
.assign(to: \.loading, on: self)}}Copy the code
Here, I’ll outline the purpose of the main code without going into too much detail, as it was covered in previous articles.
$inputText
: When we are using@Published
Add a prefix to the decorated property$
After the symbol, you get a PublisherDebounce (for: 1.0, the scheduler: myBackgroundQueue)
: when there is input, the debounce will usher in a 1 second time window, if received the new data in 1 seconds, and open a new window 1 seconds, before the window of the void, until no new data 1 seconds, and then send the received data, its core idea is can control the frequent data problem.throttle(for: 1.0, scheduler: myBackgroundQueue, latest: true)
: Throttle opens a series of consecutive 1-second Windows, sending the most recent data each time the 1-second threshold is reached,Note that when the first data is received, it is sent immediately..removeDuplicates()
You can de-duplicate, for example, if the two most recently received data are swift, the second is ignored.print("Github input")
The process by which a pipline can be printed, and the output can be prefixed.map
The logic of the map above is to map the input string to a new Publisher, which will request the network and eventually output our encapsulated data modelGithubRepositoryResponse.self
.decode
Used to parse data.replaceError(with: [])
Used to replace errors and send an empty array if the network request fails.switchToLatest()
To output Publisher data, if the map returns PublisherswitchToLatest
Switch output.receive(on: RunLoop.main)
Used to switch threads.assign(to: \.repositories, on: self)
: Assign can be used directly as a property copy using KeyPath, which is a Subscriber
Can you see that? A number of Operators are used throughout the entire process, and you can accomplish almost anything by combining them.
Let’s look at the GithubAPI wrapper again:
enum GithubAPIError: Error.LocalizedError {
case unknown
case apiError(reason: String)
case networkError(from: URLError)
var errorDescription: String? {
switch self {
case .unknown:
return "Unknown error"
case .apiError(let reason):
return reason
case .networkError(let from):
return from.localizedDescription
}
}
}
struct GithubAPI {
/ / / loaded
static let networkActivityPublisher = PassthroughSubject<Bool.Never> ()/// request data
static func fetch(url: URL) -> AnyPublisher<Data.GithubAPIError> {
return URLSession.shared.dataTaskPublisher(for: url)
.handleEvents(receiveCompletion: { _ in
networkActivityPublisher.send(false)
}, receiveCancel: {
networkActivityPublisher.send(false)
}, receiveRequest: { _ in
networkActivityPublisher.send(true)
})
.tryMap { data, response in
guard let httpResponse = response as? HTTPURLResponse else {
throw GithubAPIError.unknown
}
switch httpResponse.statusCode {
case 401:
throw GithubAPIError.apiError(reason: "Unauthorized")
case 403:
throw GithubAPIError.apiError(reason: "Resource forbidden")
case 404:
throw GithubAPIError.apiError(reason: "Resource not found")
case 405..<500:
throw GithubAPIError.apiError(reason: "client error")
case 500..<600:
throw GithubAPIError.apiError(reason: "server error")
default: break
}
return data
}
.mapError { error in
if let err = error as? GithubAPIError {
return err
}
if let err = error as? URLError {
return GithubAPIError.networkError(from: err)
}
return GithubAPIError.unknown
}
.eraseToAnyPublisher()
}
}
Copy the code
GithubAPIError
Error is a package of various, interested can seeThe AFError AlamofireznetworkActivityPublisher
Is a Subject, which is essentially a Publisher. It is used to send notification events for network loading. You can see loading in the upper left corner of the videonetworkActivityPublisher
Implementation of theURLSession.shared.dataTaskPublisher(for: url)
Is the most common web request Publisher.handleEvents
You can listen for events in the Pipline.tryMap
Is a special Operator that is used primarily for data mapping but allows throw exceptions.mapError
For handling error messages, in the code above, we did error mapping logic,The core idea of error mapping is to map various errors to custom error types.eraseToAnyPublisher()
The type used to flatten Publisher, which I won’t go into
In summary, many of you may not immediately appreciate the subtlety of the code above, but the beauty of responsive programming is that we lay out the data pipeline ahead of time, and the data will automatically flow through the pipeline, which is really seconds.
To simulate the login
If network requests are for asynchronous data, mock logins are for multiple data streams, so let’s take a quick look at the UI code:
struct LoginView: View {
@StateObject private var dataModel = LoginDataModel(a)@State private var showAlert = false
var body: some View {
VStack {
TextField("Please enter user name", text: $dataModel.userName)
.textFieldStyle(RoundedBorderTextFieldStyle())
if dataModel.showUserNameError {
Text("User name cannot be less than 3 characters!!")
.foregroundColor(Color.red)
}
SecureField("Please enter your password", text: $dataModel.password)
.textFieldStyle(RoundedBorderTextFieldStyle())
if dataModel.showPasswordError {
Text("Password must be no less than 6 characters!!")
.foregroundColor(Color.red)
}
GeometryReader { geometry in
Button(action: {
self.showAlert.toggle()
}) {
Text("Login")
.foregroundColor(dataModel.buttonEnable ? Color.white : Color.white.opacity(0.3))
.frame(width: geometry.size.width, height: 35)
.background(dataModel.buttonEnable ? Color.blue : Color.gray)
.clipShape(Capsule())
}
.disabled(!dataModel.buttonEnable)
}
.frame(height: 35)
}
.padding()
.border(Color.green)
.padding()
.animation(.easeInOut)
.alert(isPresented: $showAlert) {
Alert(title: Text("Login successful"),
message: Text("\(dataModel.userName) \n \(dataModel.password)"),
dismissButton: nil)
}
.onDisappear {
dataModel.clear()
}
}
}
Copy the code
Specific knowledge related to SwiftUI will not be repeated, the routine is the same, in the ABOVE UI code, we directly use LoginDataModel, all business logic is encapsulated in LoginDataModel.
class LoginDataModel: ObservableObject {
@Published var userName: String = ""
@Published var password: String = ""
@Published var buttonEnable = false
@Published var showUserNameError = false
@Published var showPasswordError = false
var cancellables = Set<AnyCancellable> ()var userNamePublisher: AnyPublisher<String.Never> {
return $userName
.receive(on: RunLoop.main)
.map { value in
guard value.count > 2 else {
self.showUserNameError = value.count > 0
return ""
}
self.showUserNameError = false
return value
}
.eraseToAnyPublisher()
}
var passwordPublisher: AnyPublisher<String.Never> {
return $password
.receive(on: RunLoop.main)
.map { value in
guard value.count > 5 else {
self.showPasswordError = value.count > 0
return ""
}
self.showPasswordError = false
return value
}
.eraseToAnyPublisher()
}
init(a) {
Publishers
.CombineLatest(userNamePublisher, passwordPublisher)
.map { v1, v2 in
!v1.isEmpty && !v2.isEmpty
}
.receive(on: RunLoop.main)
.assign(to: \.buttonEnable, on: self)
.store(in: &cancellables)
}
func clear(a) {
cancellables.removeAll()
}
deinit{}}Copy the code
If you look closely at the code above, it is declarative and the processing of individual data is clear:
- We use the
userNamePublisher
To handle the username logic - We use the
passwordPublisher
To handle the logic of the password - We use the
CombineLatest
To merge user name and password data, used to control the status of the login button
It’s really declarative, and if you look at it from above, it looks more like a specification than a calculation of a bunch of variables.
Here, I am too lazy to write non-combine code, you can carefully understand the code, savor the charm.
conclusion
This article is not complex, nor comprehensive, nor is it a complete field experience, but just an example of Combine in a real development scenario. There are three further articles in this tutorial, which respectively explain how to customize Publisher, Operator and Subscriber. It is an advanced content, and we will wait and see.