Load local images asynchronously in the table view

joker hook
Aug 6, 2019 · 8 min read

Chinese version -> Here

Table views have always been the most popular guests for many apps. In fact, more than 90% of the software has a tabular view. Numerous UI designers like to add images to the table view, such as the most popular Twitter and Instagram.

It’s good to use the image in the table view, but it will bring a problem. The image loading time is longer than other content. If the image loading process is placed in the main thread, it will cause the table view to appear unsmooth when sliding.

Table of Contents

  • Add picture control
  • Create a data source
  • Improve the table view
  • Select image function
  • Problem found
  • Solve the problem that the table view does not slide smoothly

Load image project

Open your Xcode, create a new project, select the single view app option, and name your project LazyLoadImage.

Then open your storyboard (Main.storyboard) and delete the pre-created view controller. Then drag a table view controller from the Library to the story panel.

Add a navigation bar to the table view controller (specifically select table view controller -> editor -> embed in -> navigation controller)

Add a button UIBarButtonItem to the navigation bar, then add a title to the navigation bar, which I added here as Images.

In order to more intuitively see the difference between asynchronous loading and synchronous loading of images, I will use a relatively simple interface to describe here.
Select the cell, open Attribute Inspector, and set the cell’s identifier. And open the Size Inspector and set the height of the cell to 300.

Add picture control

Note that the content mode of the image view is set to Aspect Fill in the Attribute Inspector.

Create a data source

import UIKitclass Image: Codable {
var imageData: Data?

init(imageData: Data) {
self.imageData = imageData
}

static let DocumentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
static let ArchiveURL = DocumentsDirectory.appendingPathComponent("Image").appendingPathExtension("plist")

static func loadImages() -> [Image]? {
guard let codedImages = try? Data(contentsOf: ArchiveURL) else { return nil }

let propertyListDecoder = PropertyListDecoder()
return try? propertyListDecoder.decode(Array<Image>.self, from: codedImages)
}

static func loadSampleImages() -> [Image] {
return []
}

static func saveImages(_ images: [Image]) {
let propertyListEncoder = PropertyListEncoder()
let codedImages = try? propertyListEncoder.encode(images)
try? codedImages?.write(to: ArchiveURL, options: .noFileProtection)
}
}

Knowledge about Codable and Decodable can be found in App Development with Swift.

Then create the actual data:

var images: [Image] = []

Improve the table view

Drag the image view to the created TableViewCell.swift and name it lazyImageView.

@IBOutlet weak var lazyImageView: UIImageView!

Go back to the UITableViewController.swift file and add the following code to the viewDidLoad method.

override func viewDidLoad() {
super.viewDidLoad()
tableView.estimatedRowHeight = 300
tableView.rowHeight = UITableView.automaticDimension

if let savedImages = Image.loadImages() {
images = savedImages
} else {
images = Image.loadSampleImages()
}
}

In order to be able to display the tabular data, we also need to add a proxy method for the table view.

Implement the table view proxy in the numberOfSections(in tableView: )numberOfRowsInSectioncellForRow(at:) and tableView(_:heightForRowAt:) :

override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return images.count
}override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! TableViewCellcell.lazyImageView.image = UIImage(data:images[indexPath.row].imageData!)
return cell
}override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 300
}

Select image function

@IBAction func addNewImageButtontapped(_ sender: UIBarButtonItem) {
}

We will select the images from the album and add them to the images, which will be displayed on the table view.

Add the following statement to your `addNewImageButtonTapped(sender:)` method:

let imagePicker = UIImagePickerController()
imagePicker.delegate = self

let alertViewController = UIAlertController(title: "Choose Image Source", message: nil, preferredStyle: .actionSheet)
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)

if UIImagePickerController.isSourceTypeAvailable(.photoLibrary) {
let photoLibraryAction = UIAlertAction(title: "Photo Library", style: .default, handler: { action in
imagePicker.sourceType = .photoLibrary
self.present(imagePicker, animated: true, completion: nil)
})
alertViewController.addAction(photoLibraryAction)
}alertViewController.addAction(cancelAction)
present(alertViewController, animated: true, completion: nil)

// --- 没有这一句话会有约束警告 ---
alertViewController.view.subviews.flatMap({$0.constraints}).filter{ (one: NSLayoutConstraint)-> (Bool) in
return (one.constant < 0) && (one.secondItem == nil) && (one.firstAttribute == .width)
}.first?.isActive = false

In order to be able to select images, we also need to add a proxy for image selection, adding two superclasses to the TableViewController: UIImagePickerController and UINavigationControllerDelegate.

class TableViewController: UITableViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
·······
}

Then implement the imagePickerController(_:didFinishPickingMediaWithInfo:) method in UITableViewController.swift:

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let selectedImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {

let image = Image(imageData: selectedImage.pngData()!)
images.append(image)
Image.saveImages(images)
dismiss(animated: true, completion: nil)
}

Now, if you run the project directly and click the Add Image button, the project will crash, because you have not added the relevant permissions in Info.pilst. Open this file and add a new project to it: Privacy — Photo Library Usage Description.

Restart your project, try to add images to it, try to add larger quality images, the effect will be more obvious.

problem found

Solve the problem that the table view does not slide smoothly

In fact, many developers don’t use this approach because it’s not good, and most developers use this method as one of the steps to solve the problem.

In fact, most developers prefer the way that loads images asynchronously.

Asynchronous Loading Knowledge

Asynchronous loading is loaded at the same time as the execution process, usually the things that are of less importance to the images.

Let’s take a look at how to use the asynchronous loading method in the project.

Asynchronous Loading Of Images

This feature can be easily implemented using DispatchQueue.
Rewrite in your cellForRow(at:) method:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: “Cell”, for: indexPath) as! TableViewCell
cell.lazyImageView.image = nil
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.75) {
//获取cell真正的indexPath.
if let index = tableView.indexPath(for: cell) {
if let data = images[index.row].imageData {
cell.lazyImageView.image = UIImage(data: data)
cell.setNeedsLayout()
}
}
}
return cell
}

Run the project, this time you should be able to see a lot of smooth images, and the images are loaded afterward.

New problem

There is a small problem with the project. You will see that the images outside the screen will be loaded repeatedly. We want to load those images when they are outside the screen, but they are already loaded when they return to the screen. The actual state of the picture appears instead of after the interface is loaded for a while.

This involves the knowledge of caching.

Here, in order to achieve this function, we need to use NSCache.

Add a variable inside the Data.swift file:

let imageCache = NSCache<AnyObject, AnyObject>()

At the same time, in order to be able to cache images, we need to use a String type keyword to retrieve the image. Add a variable to your Image class:

var recordID: String?
init(imageData: Data, recordID: String) {
self.imageData = imageData
self.recordID = recordID
}

recordID is a unique search keyword for each image. Here, you can use `*String(data: selectedImage.pngData()!, encoding: .unicode)!*`

Modify the imagePickerController(_:didFinishPickingMediaWithInfo:) method:

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let selectedImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {

// 获取图片唯一的ID,这里使用String(data: selectedImage.pngData()!, encoding: .unicode)!
let image = Image(imageData: selectedImage.pngData()!, recordID: String(data: selectedImage.pngData()!, encoding: .unicode)!)
images.append(image)
Image.saveImages(images)
dismiss(animated: true, completion: nil)
}
}

Also need to modify the cellForRow(at:) method:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! TableViewCell/**
*另外在下载图片之前先把cell的imageView的image置为nil
*可以防止图片下载失败而导致显示了以前的图片.
*如果照片有在缓存里面就去缓存里面取,没有就添加到缓存里面
*/
cell.lazyImageView.image = nilif let imageFromCache = imageCache.object(forKey: images[indexPath.row].recordID as AnyObject) as? UIImage {
cell.lazyImageView.image = imageFromCache
} else {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.75) {
// 获取cell真正的indexPath.
if let index = tableView.indexPath(for: cell) {
if let data = images[index.row].imageData {
let imageToCache = UIImage(data: data)
// 给缓存添加照片
imageCache.setObject(imageToCache!, forKey: images[indexPath.row].recordID! as AnyObject)
cell.lazyImageView.image = UIImage(data: data)
cell.setNeedsLayout()
}
}
}
}
return cell
}

Refer to the comments for specific implementation features of the code.

If you run this project at this time, you will find that the project crashes. This is because the recordID is newly added. The original images have no recordID content, so you need to delete the project on the emulator and re-run it.

The whole project you can find on GitHub.

The Startup

Get smarter at building your thing. Join The Startup’s +788K followers.

Sign up for Top 10 Stories

By The Startup

Get smarter at building your thing. Subscribe to receive The Startup's top 10 most read stories — delivered straight into your inbox, once a week. Take a look.

By signing up, you will create a Medium account if you don’t already have one. Review our Privacy Policy for more information about our privacy practices.

Check your inbox
Medium sent you an email at to complete your subscription.

joker hook

Written by

👨‍🎓/study communication engineering🛠/love iOS development💻/🐶🌤🍽🏸🏫

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +788K followers.

joker hook

Written by

👨‍🎓/study communication engineering🛠/love iOS development💻/🐶🌤🍽🏸🏫

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +788K followers.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store