In my Apple Watch app, I have a table with about 25 rows, and each one has a few bits of text and an image that needs to be loaded from the internet. Similar to an Instagram-style feed, but these are profile images of about 8k each.
When I build the table based on incoming JSON data, I'm doing everything correctly, utilizing WatchKit's built in image caching, to reduce unnecessary network traffic.
The app "works" and the images display correctly. But the problem is it takes 10-20 seconds for the view to be ready for interaction from the user. Before those 10-20 seconds are up, scrolling is sluggish, and buttons don't do anything until all the images have loaded.
What I would like is some way to lazy load the images as the user scrolls down.
I've already tried implementing a paging solution, but that has its drawbacks as well, and doesn't solve my original problem entirely.
If I only populate the text bits (no images), the table displays instantly and is ready for interaction.
***UPDATE: Mike Swanson's answer below was extremely thorough and did solve most of my problem. The sluggishness and blocking have gone away.
I wanted to post the final code for anybody else who comes along and has a similar issue. Specifically, I wanted to show how I implemented the background dispatch.
I'm now also temporarily setting the image to the placeholder during the main thread to show activity. It gets replaced as soon as the actual image loads and transmits to the watch. I also improved the image cache key to miss if a new image has been uploaded.
NEW CODE:
let qualityOfServiceClass = QOS_CLASS_BACKGROUND
let backgroundQueue = dispatch_get_global_queue(qualityOfServiceClass, 0)
i = 0
for p in self.profiles {
if let profileId = p["id"] as? NSNumber {
var profileIdStr = "\(profileId)"
let row = self.profilesTable.rowControllerAtIndex(i) as! WatchProfileRow
if let hasImage = p["has_image"] as? NSNumber {
let profileImageCacheKey = "\(profileIdStr)-\(hasImage)"
// if the image is already in cache, show it
if self.device.cachedImages[profileImageCacheKey] != nil {
row.profileImageThumbnail.setImageNamed(profileImageCacheKey)
// otherwise, get the image from the cdn and cache it
} else {
if let imageHost = self.imageHost as String! {
var imageURL = NSURL(string: NSString(format: "%@%@-thumbnail?version=%@", imageHost, profileId, hasImage) as String)
var imageRequest = NSURLRequest(URL: imageURL!)
NSURLConnection.sendAsynchronousRequest(imageRequest, queue: NSOperationQueue.mainQueue()) {
(response, data, error) -> Void in
if error == nil {
row.profileImageThumbnail.setImageNamed("profile-placeholder")
dispatch_async(backgroundQueue, {
self.device.addCachedImageWithData(data, name: profileImageCacheKey)
dispatch_async(dispatch_get_main_queue(), { () -> Void in
row.profileImageThumbnail.setImageNamed(profileImageCacheKey)
})
})
}
}
} else {
row.profileImageThumbnail.setImageNamed("profile-placeholder")
}
}
} else {
row.profileImageThumbnail.setImageNamed("profile-placeholder")
}
}
i++
}
ORIGINAL CODE:
i = 0
for p in self.profiles {
if let profileId = p["id"] as? NSNumber {
var profileIdStr = "\(profileId)"
let row = self.profilesTable.rowControllerAtIndex(i) as! WatchProfileRow
if let hasImage = p["has_image"] as? NSNumber {
// if the image is already in cache, show it
if self.device.cachedImages[profileIdStr] != nil {
row.profileImageThumbnail.setImageNamed(profileIdStr)
// otherwise, get the image from the cdn and cache it
} else {
if let imageHost = self.imageHost as String! {
var imageURL = NSURL(string: NSString(format: "%@%@-thumbnail?version=%@", imageHost, profileId, hasImage) as String)
var imageRequest = NSURLRequest(URL: imageURL!)
NSURLConnection.sendAsynchronousRequest(imageRequest, queue: NSOperationQueue.mainQueue()) {
(response, data, error) -> Void in
if error == nil {
var image = UIImage(data: data)
row.profileImageThumbnail.setImage(image)
self.device.addCachedImage(image!, name: profileIdStr)
}
}
} else {
row.profileImageThumbnail.setImageNamed("profile-placeholder")
}
}
} else {
row.profileImageThumbnail.setImageNamed("profile-placeholder")
}
}
i++
}
First, there is no method in WatchKit to determine the current position in a WKInterfaceTable
. That means that any logic you use will have to work around that fact.
Second, it looks like you're sending an asynchronous network request then using setImage
when you have the data. setImage
encodes the image as a PNG before transmitting it to the device, and in your existing code, this is happening on the main thread. If you don't need a PNG image, I'd consider using setImageData
to send JPEG-encoded data to further reduce the number of bytes that need to be transmitted to the Watch.
Finally, it appears that you're double-transmitting your images. Once with setImage
and twice with addCachedImage
. After you receive your image, I'd suggest dispatching to a background thread, then using WKInterfaceDevice
's addCachedImageWithData
(which can safely be used on a background thread) to send the data across. Then, dispatch back to the main thread before setting the now-cached image with setImageNamed
.