fbpx

I encountered some performance issues while working with a UITableView when I was building our first AI powered iOS app, I was there.  I was building a solution where a UITableView would have a series of rows where the data includes image. The issue with this was, as more rows were added to the table, the app became really slow (on my test device i.e. my old iPhone 5). Initially I was quite confused and then I found a solution to the problem after which was even more confused…by the simplicity of the solution. Anyway in this blog post I will be talking about

  1. my solution, which was little more than simple image scaling
  2. providing some code samples – including a full iOS project you can download from GitHub
  3. asking you if there’s a better way to solve this problem and whether or not this was a problem at all?

So without further a due let’s get into it by looking at my motivations for this.

Why the need for this?

If you read my previous post you will know a little about what our new app, I was there is i.e. it is an AI powered photo, where the user captures image with the camera, which the app tries to analyse using image processing and machine learning. The captured image is then added to a row of a UITableView which the user can tap to examine the original image!

One of the first places I learned iOS development from was from Apple i.e. their Start developing iOS apps (Swift) article. That’s a great tutorial and it lays a good foundation of getting started with iOS UI controls such as UITableView, Segue and such. Anyway I used that tutorial as a foundation for building my iOS app and I soon realised that, the tutorial works is great when dealing with a small number rows with. As you add more rows with images, the device gets progressively slower as the number of rows increases. This was a high priority problem since the devices that I used for my tests got progressively hot to the point it was uncomfortable as more and more images were captured, analysed and added to the table. To solve this, I came up with a rather straight forward solution which to me seemed very obvious and this made me think? given that I am relatively new to iOS development, was this a problem at all? Did I solve a problem, was this even a problem or is there some iOS info that I completely ignored? Anyway you can let me know all that in the comments on this post but first let’s see what my solution is.

Solution

The solution is quite simple: every time an image is captured save two versions of the captured image i.e. the full-size image and a thumbnail. To better understand this solution, it’s best we examine what the target app is,

Target App

This is an app where you store data in a UITableView, where each cell contains an image and some text next to it that describes the image. Now, every time I am learning something new programmatically, looking at some code samples helps me understand it better. I get the general idea after reading about it and then code samples really drive home the point. So for anyone who’s in a similar situation, I have created a GitHub repository (UITableViewWithImage) with some sample code for this.

Sample code

The app in the sample code has some very basic code,

  1. The main view of the app is embedded in a navigation view and it has a UITableView
  2. On the navigation bar  there’s bar button (UIBarButtonItem) ‘+’  that you can tap to open the DetailView
  3. The DetailView can be accessed to either create a new record or to view an existing TableEntry

I am not going to go into the details of iOS UI creation in this blogpost, if you want that then I suggest you have a look at this tutorial! Here I will talk about aspects of the sample code that aren’t in that tutorial e.g. the code invoked on tapping “+”

@IBAction func capture(_ sender: Any) {
    let imagePicker = UIImagePickerController()
    imagePicker.delegate = self
    imagePicker.allowsEditing = false
    if UIImagePickerController.isSourceTypeAvailable(.camera) {
        imagePicker.sourceType = .camera
    } else {
        imagePicker.sourceType = .photoLibrary
    }
    self.present(imagePicker, animated: true)
}

The UIImagePickerController is first defined and it’s all fairly standard the only thing that’s a little different is we check whether we have access to the device camera to capture an image. The idea is that you add a record (row) to your table view by either capturing an image or selecting an image from your photo library.

hmm, what else? I think the rest of the code is fairly standard. If you run the app both on your mac or on your iDevice, you will realise the device gets progressively hotter as you keep adding data. This is because you need to add the following image resizing code

Like our blog? subscribe to our newsletter

Image resize code

Onto the main focus of this post, as a general rule of thumb, I tend to identify a group of functions that solve problems in a certain domain and bundle them into “one” logically named class. You know because I like “separating my concerns”… ahh haha see what I did there?

In I was there code, we have a class called ImageHelper which has all the code that deals with things such as, resizing, adding text overlays and various other things that help when working with images. For this example, however I have only included the one function that’s relevant to this post i.e. resizeImage

static func resizeImage(image: UIImage, targetSize: CGSize) -> UIImage {
    let size = image.size
    let widthRatio = targetSize.width / size.width
    let heightRatio = targetSize.height / size.height

    var newSize: CGSize
    if (widthRatio > heightRatio) {
        newSize = CGSize(width: size.width * heightRatio, height: size.height * heightRatio)
    } else {
        newSize = CGSize(width: size.width * widthRatio, height: size.height * widthRatio)
    }
    let rect = CGRect(x: 0, y: 0, width: newSize.width, height: newSize.height)

    UIGraphicsBeginImageContextWithOptions(newSize, false, 1.0)
    image.draw(in: rect)
    let newImage = UIGraphicsGetImageFromCurrentImageContext()!
    UIGraphicsEndImageContext()
    return newImage
}

That’s to resize an image, the next interesting bit is the FileIO class. Yes you guessed it right, this class has all the functions that deal with the file operations i.e. saving, loading, retrieving etc etc. Anyway in this class we have a few functions of interest

func getFilename(name: String) -> (name:String, thumbnail: String) {
    let now = Date()
    let filename = "\(now.timeIntervalSince1970)_\(name).png"
    let thumbnail = "\(filename)_thumbnail.png"
    return (name:filename, thumbnail: thumbnail)
}

This is fairly straightforward, when we save a file we want two things, a string representing the name and the thumbnail of the file. To ensure a filename is unique we append a time stamp to it. We could parametrise the file extension but for simplicity’s sake let’s just leave it to .png.

func saveImage(saveFileName:String, image: UIImage) {
    let data = UIImagePNGRepresentation(image)
    FileManager.default.createFile(atPath: saveFileName, contents: data, attributes: nil)
}
static func getFileURL(fileName:String) -> URL? {
    let documentsUrl =  FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
    let dirContents = try? FileManager.default.contentsOfDirectory(at: documentsUrl, includingPropertiesForKeys: nil, options: [])
    if let dc = dirContents {
        return dc.first{$0.absoluteString.contains(fileName)}
    }
    return nil
}

The the important thing to know in the saveImage post is UIImagePNGRepresentation and then there’s the getFileURL function that retrieves the right URL based on the filename. Now to know more about how to save a file, refer to this tutorial here.

Like our blog? subscribe to our newsletter

Putting it all together

Now that we have looked at all the relevant functions, let’s look at how it all fits together. It all starts with a UIImagePickerControllerDelegate method

func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {

    guard let image = info[UIImagePickerControllerOriginalImage] as? UIImage else {
        print("Error getting the image")
        return
    }
    //save two copies of the image
    let imageName = "Image_\(data.count)"

    //Step 1: save original image
    let fullImgFilename = FileIO.fileIO.getNameWithDirPath(filename: "\(imageName)\(Constants.ROW_DATA_IMG_EXTENSION)")
    FileIO.fileIO.saveImage(saveFileName: fullImgFilename, image: image)
    //Step 2: resize image
    let thumbnailImage = UIImage.resizeImage(image: image, targetSize: CGSize(width: 75, height: 75))
    //Step 3: save thumbnail
    let thumbnailname = FileIO.fileIO.getNameWithDirPath(filename: "\(imageName)\(Constants.THUMBNAIL_NAME)\(Constants.ROW_DATA_IMG_EXTENSION)")
    FileIO.fileIO.saveImage(saveFileName: thumbnailname, image: thumbnailImage)

    let newRow = TableData(name: imageName)
    data.append(newRow)
    tableView.reloadData()
    picker.dismiss(animated: true, completion: nil)
}

Most of the code above is self-explanatory but let me try to summarise a few key points of it,

  1. Ensure we have a valid image and give it a name
  2. Save the original image, as well as a smaller sized thumbnail for the image
  3. Make sure it is saved to the user’s documents directory
  4. Append a new record to the array of data that constitutes to the rows of the UITableView
  5. Reload the table

Ok so once we have added the data, it’s time to see how the table reloading works but wait before that, I think we should look at the custom data structure in our project TableData

class TableData: NSObject {
    var name: String        
    init(name: String) {
        self.name = name
    }        
    func getRowThumbnail() -> UIImage {
        let thumbnailname = FileIO.fileIO.getNameWithDirPath(filename: "\(name)\(Constants.THUMBNAIL_NAME)\(Constants.ROW_DATA_IMG_EXTENSION)")
        let thumbnail = UIImage(contentsOfFile: thumbnailname)!
        return thumbnail
    }
    func getRowImage() -> UIImage {
        let imageName = FileIO.fileIO.getNameWithDirPath(filename: "\(name)\(Constants.ROW_DATA_IMG_EXTENSION)")
        let image = UIImage(contentsOfFile: imageName)!
        return image
    }
}

It’s a simple Swift class that extends NSObject with 2 extra methods for our needs. getRowImage() and getRowThumbnail(). These methods with the help of our FileIO helper class get the name or path of our saved image and initialise and return a UIImage object. All right, now that we know all that, we can look at how our UITableView loads it’s data

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? TableViewCell else {
        fatalError("Cannot cast table cell")
    }
    let rowData = data[indexPath.row]
    cell.rowLabel.text = rowData.name
    cell.rowImage.image = rowData.getRowThumbnail()
    cell.rowData = rowData
    return cell
}

This is actually pretty straight forward i.e. our TableViewCell has two properties, a UILabel for rowLabel and a UIImage for rowImage, so if we are loading contents to our table cell we just call the TableData.getRowThumbnail() method. So where do we use the getImage() method? In the DetailViewController, remember a user taps on a tableView cell to examine it in more detail.  Let’s have a look at the DetailViewController method 

class DetailViewController: UIViewController {
    var rowData: TableData?
    //MARK: IBOutlet
    @IBOutlet var nameLbl: UILabel!
    @IBOutlet var rowImage: UIImageView!

    override func viewDidLoad() {
        super.viewDidLoad()

        if let rd = rowData {
            nameLbl.text = rd.name
            rowImage.image = rd.getRowImage()
        }
    }
}

In here we use the getRowImage() method to show the original higher resolution image…and that’s a wrap I think? Did I miss anything? ok maybe one more thing that I can think of? hmm, for each image captured, we are storing an additional item, however small the space maybe, what if you delete a row from the table? We would need to delete the image right and probably the thumbnail as well, so how do we do that? Well there’s code for that too, have a look at the deleteImageFile() method of the FileIO class.

Summary

As we wrap it up, let me repeat I am not sure if this was a real problem or not? I was building an iOS app that showed data in a UITableView where each row of the table has an image captured by the user’s camera. In that solution when the user taps on a row, the user can see the full image. Now when I first started building it, I was testing it on my 4 year old iPhone 5 and I noticed significant slow down and device over heating as the number of rows in the table kept increasing. To save time I came up with a quick solution that involved storing 2 copies of an image, one the original image that the user copied and another a smaller sized thumbnail that can be displayed in the UITableView. In this blogpost I presented my solution and provided some code samples via a GitHub repo for such a solution that can help you paint a picture of my simple image scaling solution.

As usual, if you find any of my posts useful and want to support me, buy or even try one of our products and leave us a review on the app store.

My Day To-Do Lite - Task list
My Day To-Do Lite - Task list
Snap! I was there
Snap! I was there
Developer: Bhuman Soni
Price: Free
Categories: iOSiosapp

Leave a Reply

Your email address will not be published. Required fields are marked *