This article is published on my wechat official account. You can follow it by scanning the QR code at the bottom of the article or searching for HelloWorld Jieshao in wechat.
I believe that when you use the App, you often have such an experience, that is, the waiting time for loading network data is too long, the scrolling is accompanied by a lag, and even in the absence of the network, the whole application is unavailable. So how can we improve the user experience to ensure that the user does not have to wait for a long time, but can also enjoy the wait easily, with a clear expectation of the content after loading?
Case sharing
In modern work and life, mobile phone is no longer a simple communication tool, but more like a terminal integrating office, entertainment and consumption, which has become a part of our life unconsciously. Therefore, as iOS developers, we are no longer dealing with the display of sporadic data in daily development. In order to attract users, we often need to display a lot of valuable information in the App to attract users. How to display these massive data gracefully is your personal experience.
As most iOS developers know, displaying scrolling data is a common task in building mobile applications, and Apple’s SDK provides two large components, UITableView and UICollectionVIew, to help perform such tasks. However, when large amounts of data need to be displayed, ensuring smooth, silky scrolling can be tricky. So today is a good time to share with you a personal experience dealing with large amounts of scrollable data.
In this article, you will learn the following:
1. Let your App scroll infinite scrolling and scrolling data load seamlessly
2. Let your App data scroll without stashing and achieve smooth and silky scrolling
3. Cache and retrieve images asynchronously to make your App more responsive
Unlimited scrolling, seamless loading
MJRefresh is used to pull down and refresh data. When the scrolling data reaches the bottom, it sends a Loading animation to the server and displays a Loading animation at the bottom of the control. When the requested data returns, the Loading animation disappears. The UITableView or UICollectionView control continues to load the data and display it to the user.
In this case, there is a phenomenon, that is, there is a gap between the time when the App requests data from the server and the time when the data is returned. If the network is poor, this gap will last, which will give people a bad experience. So how to avoid this phenomenon! Or can we get the rest of the data ahead of time, request it without the user knowing, and it looks like a seamless load?
The answer, of course, is yes!
To improve the application experience, on iOS 10, Apple introduced the Prefetching API on UICollectionView and UITableView. It provides a mechanism to prepare data before it needs to be displayed, in order to improve data fetching performance.
First, LET me introduce you to the concept of infinite scrolling, which allows users to load content continuously without paging. The App loads some initial data when the UI is initialized, and then loads more data when the user scrolls near the bottom of the display.
For years, social media companies like Instagram, Twitter and Facebook have used this technology. If you look at their App, you can see infinite scrolling in action, and I’ll show you how it works on Instagram!
How to implement
Since Instagram’s UI is too complex, I won’t imitate the implementation here, but I imitate its loading mechanism and achieve the same simple data scrolling and seamless loading effect.
Here’s my idea:
Customize a Cell view consisting of a UILabel and a UIImageView to display text and web images. Then simulate the network request to get the data, noting that this step must be performed asynchronously; Finally, you can use UITableView to display the returned data. In viewDidLoad, you can request network data to get some initialization data. Then you can use the Preloading API of UITableView to pre-load data so as to achieve seamless loading of data.
How about infinite scrolling! In fact, the infinite scrolling is not really endless, strictly speaking, it has an end, but the data behind this function is incalculable, only a large number of data to support the application has been constantly getting data from the server.
Normally, when we build the UITableView control, we initialize the number of rows (numsOfRow), which is a key factor for infinite and seamless loading. If we update the rows of UITableView each time based on the amount of data returned by the server and Reload it, then the Prefetching API I mentioned earlier will not last. Because it only works if the current number of rows in the UITableView is less than its total number of rows when the data is preloaded. Of course, the former can also achieve data Loading, but its effect is not seamless Loading, it will have a Loading waiting time every time the data is loaded.
Infinite scroll back I mentioned above, in fact it is not difficult to implement, under normal circumstances, we ask the server for a lot of the same type of data, will provide an interface that is what I call the paging request interface, the interface at the time of each data back, will tell the client how many pages of data, how much is the amount of data, each page is currently what page, So we can figure out how much total data there is, and this is the total number of rows in a UITableView.
An example of the response data is as follows (for clarity, it shows only the fields related to paging) :
{
"has_more": true,
"page": 1,
"total": 84,
"items": [
...
...
]
}
Copy the code
Below, I will use the code to achieve it step by step!
Simulating paging requests
Since no suitable paging test interface was found, I simulated a paging request interface by myself. Each time I called this interface, I delayed 2s to simulate the state of network request. The code is as follows:
func fetchImages() { guard ! IsFetchInProcess else {return} isFetchInProcess = true // print("+++++++++++ DispatchQueue.global().asyncAfter(deadline: DispatchTime. Now () + 2) {print (" + + + + + + + + + + + simulation network data request returns success + + + + + + + + + + + ") DispatchQueue. Main. The async {self. Total = 1000 Self.currentpage += 1 self.isfetchinProcess = false self.currentPage += 1 self.isfetchinProcess = false self.currentPage += 1 self.isfetchinProcess = false 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
Data callback processing:
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
Preloaded data
First if you want UITableView to preload data, you need to insert the following code into the viewDidLoad () function and request the first page of data:
override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. ... tableView.prefetchDataSource = self ... Viewmodel.fetchimages ()}Copy the code
Then, you need to implement UITableViewDataSourcePrefetching agreement, the agreement contains two functions:
// this protocol can provide information about cells before they are displayed on screen. @protocol UITableViewDataSourcePrefetching <NSObject> @required // indexPaths are ordered ascending by geometric distance from the table view - (void)tableView:(UITableView *)tableView prefetchRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths; @optional // indexPaths that previously were considered as candidates for pre-fetching, but were not actually used; may be a subset of the previous call to -tableView:prefetchRowsAtIndexPaths: - (void)tableView:(UITableView *)tableView cancelPrefetchingForRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths; @endCopy the code
The first function prefetches the next IndexPaths based on the current scrolling direction and speed. This is where we usually implement the logic of preloading data.
The second function is an optional method that allows you to cancel any pending data loading when the user is scrolling so fast that some of the cells are invisible. This will improve scrolling performance, which I’ll cover below.
The logical code to implement these two functions is:
The extension ViewController: UITableViewDataSourcePrefetching {/ / page request func tableView (_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { let needFetch = indexPaths.contains { $0.row >= viewModel.currentCount} if needFetch { // 1. Meet the conditions for page request indicatorView. StartAnimating () the viewModel. FetchImages ()} for indexPath in indexPaths {if let _ = viewModel.loadingOperations[indexPath] { return } if let dataloader = viewModel.loadImage(at: Indexpa.row) {print(" prefetch images in \(indexpa.row) ") // 2 Prefetch images to download ViewModel. LoadingQueue. AddOperation (dataloader) / / 3 will be added to the record in the array to the download thread according to an index lookup viewModel. LoadingOperations [indexPath] = dataloader } } } func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]){// Cancel prefetch when the row does not need to be displayed, Avoid resource waste indexPaths. ForEach {if let dataLoader = viewModel. LoadingOperations [$0] {print (" in the \ ($0. Row) cancelPrefetchingForRowsAt ") dataLoader.cancel() viewModel.loadingOperations.removeValue(forKey: $0) } } } }Copy the code
Finally, add two useful methods to complete the feature:
/ / used to calculate the tableview loading new data need to reload the cell func visibleIndexPathsToReload (intersecting indexPaths: [IndexPath]) -> [IndexPath] { let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows ?? [] let indexPathsIntersection = Set(indexPathsForVisibleRows).intersection(indexPaths) return } func isLoadingCell(for indexPath: IndexPath) -> Bool { return indexPath.row >= (viewModel.currentCount) }Copy the code
Witness the miracle of the moment, look at the effect:
From the log, we can also clearly see that there are Prefetch and CancelPrefetch operations during the scrolling process:
Ok, so here I have simply implemented the effect of UITableView endless scrolling and seamless loading of data, do you understand?
How do I avoid scrolling jams
When you have an application that is stuck scrolling, usually because a task has been running for a long time and prevents the UI from updating on the main thread, the first step to freeing the main thread to respond to such an update event is to give the time-consuming task to a child thread to avoid blocking the main thread while retrieving data.
Apple provides many ways to implement concurrency for applications, such as GCD, which I used here to asynchronously load images on the Cell.
The code is as follows:
class DataLoadOperation: Operation { var image: UIImage? var loadingCompleteHandle: ((UIImage?) - > ())? private var _image: ImageModel private let cachedImages = NSCache<NSURL, UIImage>() init(_ image: ImageModel) { _image = image } public final func image(url: NSURL) -> UIImage? { return cachedImages.object(forKey: url) } override func main() { if isCancelled { return } guard let url = _image.url else { return } downloadImageFrom(url) { (image) in DispatchQueue.main.async { [weak self] in guard let ss = self else { return } if ss.isCancelled { return } ss.image = image ss.loadingCompleteHandle? (ss.image) } } } // Returns the cached image if available, otherwise asynchronously loads and caches it. func downloadImageFrom(_ url: NSURL, completeHandler: @escaping (UIImage?) -> ()) { // Check for a cached image. if let cachedImage = image(url: Url) {DispatchQueue. Main. Async {print (" cache hit ") completeHandler (cachedImage)} return} URLSession. Shared. DataTask (with: url as URL) { data, response, error in guard let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200, let mimeType = response? .mimeType, mimeType.hasPrefix("image"), let data = data, error == nil, let _image = UIImage(data: data) else { return } // Cache the image. self.cachedImages.setObject(_image, forKey: url, cost: data.count) completeHandler(_image) }.resume() } }Copy the code
That specific how to use! Don’t worry, listen to me explain, I give you a small suggestion, everyone knows the UITableView instantiation of Cell method is: tableView: cellForRowAtIndexPath: This method needs to be called once for each Cell. It should be executed quickly and return an instance of the reuse Cell. Don’t do the data binding here. Because there are no cells on the screen yet. We can in the tableView: willDisplayCell: forRowAtIndexPath: this method of data binding, this method is called before displaying the cell.
The implementation code for performing the download task for each Cell is as follows:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: "PreloadCellID") as? ProloadTableViewCell else { fatalError("Sorry, could not load cell") } if isLoadingCell(for: indexPath) { cell.updateUI(.none, orderNo: "\(indexPath.row)") } return cell } func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {// preheat image, Guard let cell = cell as? ProloadTableViewCell else {return} let updateCellClosure: (UIImage?) -> () = { [unowned self] (image) in cell.updateUI(image, orderNo: "\(indexPath.row)") viewModel.loadingOperations.removeValue(forKey: indexPath) } // 1. First of all determine whether already exists to create good download thread if let dataLoader = viewModel. LoadingOperations [indexPath] {if let image = dataLoader. Image {/ / 1.1 Cell. updateUI(image, orderNo: "\(indexpath.row)")} else {// 1.2 If the image has not been downloaded, ., wait for after the images are downloaded updates cell dataLoader loadingCompleteHandle = updateCellClosure}} else {/ / 2. Print (" create a new image download thread in \(indexpath.row) ") if let dataloader = viewModel.loadimage (at: IndexPath. Row) {/ / 2.1 add images download after the callback dataloader. LoadingCompleteHandle = updateCellClosure / / 2.2 to start the download ViewModel. LoadingQueue. AddOperation (dataloader) / / 2.3 to download the thread into the records in the array to consult the viewModel. According to the index loadingOperations [indexPath] = dataloader } } }Copy the code
To asynchronously download preloaded images:
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { let needFetch = indexPaths.contains { $0.row >= viewModel.currentCount} if needFetch { // 1. Meet the conditions for page request indicatorView. StartAnimating () the viewModel. FetchImages ()} for indexPath in indexPaths {if let _ = viewModel.loadingOperations[indexPath] { return } if let dataloader = viewModel.loadImage(at: Indexpa.row) {print(" prefetch images in \(indexpa.row) ") // 2 Prefetch images to download ViewModel. LoadingQueue. AddOperation (dataloader) / / 3 will be added to the record in the array to the download thread according to an index lookup viewModel. LoadingOperations [indexPath] = dataloader } } }Copy the code
Cancel Prefetch tasks to avoid resource waste
func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]){// Cancel prefetch when the row does not need to be displayed, Avoid resource waste indexPaths. ForEach {if let dataLoader = viewModel. LoadingOperations [$0] {print (" in the \ ($0. Row) cancelPrefetchingForRowsAt ") dataLoader.cancel() viewModel.loadingOperations.removeValue(forKey: $0) } } }Copy the code
With this treatment, our UITableView should be silky smooth, so why wait for it?
Image cache
Although I added concurrent operations to my application above, when I looked at the performance analysis of Xcode, I couldn’t help but think that my application was eating too much memory. If I kept scrolling, my phone would probably kill my application sooner or later. Here is the performance analysis of my application when I reached 200 lines.
memory
disk
It can be seen that the performance analysis of my application is not ideal. The reason is that my application displays a large number of image resources. Every time I scroll back and forth, I download new images again instead of caching them.
So, to solve this problem, I added a cache NSCache object for my application to do a cache of images, the specific code implementation is as follows:
class ImageCache: NSObject {
private var cache = NSCache<AnyObject, UIImage>()
public static let shared = ImageCache()
private override init() {}
func getCache() -> NSCache<AnyObject, UIImage> {
return cache
}
}
Copy the code
At the start of the download, check if there is a hit in the cache. If there is a hit, return the image directly. Otherwise, re-download the image and add it to the cache:
func downloadImageFrom(_ url: URL, completeHandler: @escaping (UIImage?) -> ()) { // Check for a cached image. if let cachedImage = getCacheImage(url: Url as NSURL) {print(" hit cache ") dispatchQueue.main.async {completeHandler(cachedImage)} return} URLSession.shared.dataTask(with: url) { data, response, error in guard let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200, let mimeType = response? .mimeType, mimeType.hasPrefix("image"), let data = data, error == nil, let _image = UIImage(data: data) else { return } // Cache the image. ImageCache.shared.getCache().setObject(_image, forKey: url as NSURL) completeHandler(_image) }.resume() }Copy the code
When I look at the performance of my application with Xcode, I can see that the memory and disk usage have decreased a lot:
memory
disk
About the image cache technology, here is only the simplest one, many outside open source image libraries have different caching strategies, interested can go to GitHub to learn their source code, I will not do the details here.
The last
Finally finished, breathed a sigh of relief, the length of this article is a bit long, I spent some time doing research, and then see the want to tell you the knowledge points, see the knowledge points, also want to tell you the final chapter and verse (patchwork) to complete the article, hope everyone can love:).
In accordance with international practice, the final attached project address: github.com/ShenJieSuzh…
Related reading:
UICollectionView custom layout! Just read this one
Swift explore the SupplementaryView and Decoration View of UICollectionView
Swift custom layout achieves Cover Flow effect
UICollectionView Custom layout implementation waterfall flow view
Use UICollectionView to achieve the paging slide effect
Use UICollectionView to achieve the card rotation effect of home page
Follow my technical public account “HelloWorld Jieshao “to get more quality technical articles.