Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.
This article has participated in the “Digitalstar Project” and won a creative gift package to challenge the creative incentive money.
preface
When you need to develop a new application at work, the first thing you will consider is which design pattern to use, MVC or MVVM? Of course, today, we will always choose MVVM, because compared with MVC mode, MVVM mode has too many advantages, such as removing the business logic in View Controller, putting this part of the code in View Model, clear division of responsibilities and so on.
Data binding
However, when talking about the MVVM pattern, we must also talk about data binding. In the past, asynchronous data was handled as a Delegate or Notification, and then the UI was refreshed after receiving asynchronous data. There’s nothing wrong with handling data this way, but if you have a large number of controls on the UI that need to update data irregularly, using Delegate and Notification can be a little less elegant, which is why we’re talking about data binding.
There are many mature solutions for data binding, such as RXSwift, KVO, etc. I will not introduce these methods here, but you can Google them. Today I’m going to show you another approach, which is to use closures for data binding.
What a closure is
Closures are self-contained blocks of function code that can be passed and used in code. Closures can capture and store references to arbitrary constants or variables in their context. You can use a closure as an argument to a function or as a return value of the function.
That’s what I found online about closures. As I understand them, a closure is an executable block of code that can be passed in as arguments.
Create a Box class
Well, without all the nonsense, let’s get right to coding.
First, in order to bind ViewModel and View, we need to provide a simple mechanism to bind data sources in the ViewModel to controls in the View. One of the methods I’ve used here is called Boxing, which I found very interesting when I read someone else’s code, and it uses a property observer mechanism to notify the observer that the value has changed if it has changed.
Create a class called Box with the following code:
Import Foundation final class Box<T> {// declare an alias typeAlias Listener = (T) -> Void var Listener: Listener? var value: T { didSet { listener? (value) } } init(_ value: T){ self.value = value } func bind(listener: Listener?) { self.listener = listener listener? (value) } }Copy the code
- The typeAlias keyword declares an alias. We call the closure (T) -> Void Listener;
- The Box class defines a Listener property: Listener;
- The Box class defines a generic property value and uses the didSet property observer to detect if the value has changed, notifies the Listener to update the value if it has;
- When a Listener calls bind(Listener 🙂 on a Box, it becomes a Listener and is immediately notified of the current value of the Box;
Case study
In this demo, I used a previous project code as a reference. This project is also an article I wrote before “iOS gracefully handles network data, can you really? Why don’t you take a look at this code from the research.
A simple description of the requirements: we need to asynchronously fetch image data in the ViewModel over the network and return it to the TableView in the main view, and load the data out.
In the original project, I implemented the data callback and refresh in a Delegate mode as follows:
- Define PreloadCellViewModelDelegate agreement for correction
protocol PreloadCellViewModelDelegate: NSObject {
func onFetchCompleted(with newIndexPathsToReload: [IndexPath]?)
func onFetchFailed(with reason: String)
}
Copy the code
- Defining data sources
private var images: [ImageModel] = []
Copy the code
- After the asynchronous data is retrieved, the method in the protocol is called, the data is called back and the UI is refreshed
func fetchImages(a) {
guard !isFetchInProcess else {
return
}
isFetchInProcess = true
// Delay 2s to simulate network environment
print("+++++++++++ Simulated network data request +++++++++++")
DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + 2) {
print("+++++++++++ Simulated network data request returned successfully +++++++++++")
DispatchQueue.main.async {
self.total = 1000
self.currentPage + = 1
self.isFetchInProcess = false
// Initialize 30 images
let imagesData = (1.30).map {
ImageModel(url: baseURL+"\ [$0).png", order: $0)}self.images.append(contentsOf: imagesData)
if self.currentPage > 1 {
let newIndexPaths = self.calculateIndexPathsToReload(from: imagesData)
self.delegate?.onFetchCompleted(with: newIndexPaths)
} else {
self.delegate?.onFetchCompleted(with: .none)
}
}
}
}
Copy the code
- Refresh the data in the main view
extension ViewController: PreloadCellViewModelDelegate {
func onFetchCompleted(with newIndexPathsToReload: [IndexPath]?) {
guard let newIndexPathsToReload = newIndexPathsToReload else {
tableView.tableFooterView = nil
tableView.reloadData()
return
}
let indexPathsToReload = visibleIndexPathsToReload(intersecting: newIndexPathsToReload)
indicatorView.stopAnimating()
tableView.reloadRows(at: indexPathsToReload, with: .automatic)
}
func onFetchFailed(with reason: String) {
indicatorView.stopAnimating()
tableView.reloadData()
}
}
Copy the code
Now I don’t think this is very elegant, so I changed the code to use closures for data binding.
- Change the code in the ViewModel to external data sources
private var images: [ImageModel] = []
Copy the code
To:
var images: Box<[ImageModel]> = Box([])
Copy the code
- To get the image data asynchronously, you don’t need to call the methods in the protocol. Instead, you can directly modify the values of the images array to trigger the property observer, as follows:
func fetchImages(a) {
guard !isFetchInProcess else {
return
}
isFetchInProcess = true
// Delay 2s to simulate network environment
print("+++++++++++ Simulated network data request +++++++++++")
DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + 2) {
print("+++++++++++ Simulated network data request returned successfully +++++++++++")
DispatchQueue.main.async {
self.total = 1000
self.currentPage + = 1
self.isFetchInProcess = false
// Initialize 30 images
let imagesData = (1.30).map {
ImageModel(url: baseURL+"\ [$0).png", order: $0)}self.images.value.append(contentsOf: imagesData)
}
}
}
Copy the code
- Call the bind function in the main view to bind ViewModel as follows:
viewModel.images.bind { [weak self] _ in
guard let strongSelf = self else {
return
}
strongSelf.tableView.reloadData()
}
Copy the code
Now that we’ve done data binding using closures, the code is much simpler than using a Delegate, and all of a sudden the code is elegant.
The last
Before November 1, hurry code finished this hydrology, is also a summary of their daily online learning, I hope this article can be helpful to you. Project code address: github.com/ShenJieSuzh…
Previous articles:
- Binary tree brush summary: binary tree traversal
- StoreKit2 smells this good? Yeah, I tried it. It smells good
- After reading this article, I am no longer afraid of being asked how to construct a binary tree.
- The game guys are trying to get people to pay again. That’s bad!
- Take you rolled a netease holding cloud music home | adapter
- Netease Cloud Music Home Page (3)
- Netease Cloud Music Home Page (2)
- Netease Cloud Music Home Page (a)
- Does the code need comments? Write and you lose
- I would not study in Codable for a long time. They are for fun
- IOS handles web data gracefully. Do you really? Why don’t you read this one
- UICollectionView custom layout! This one is enough
Please drink a cup ☕️ + attention oh ~
- After reading, remember to give me a thumbs-up oh, there is 👍 power
- Follow the public number – HelloWorld Jie Shao, the first time push new posture
Finally, creation is not easy, if it is helpful to you, I hope you can praise and support, what questions can also be discussed in the comments section 😄 ~