iOS WatchKit / Swift - Lazy在WKInterfaceTable行中加载图像

时间:2022-11-04 07:43:00

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.

在我的Apple Watch应用程序中,我有一个大约25行的表,每一行都有一些文本和一个需要从互联网上加载的图像。类似于Instagram风格的Feed,但这些是每张约8k的个人资料图片。

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.

当我基于传入的JSON数据构建表时,我正在利用WatchKit的内置图像缓存正确地做所有事情,以减少不必要的网络流量。

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.

该应用程序“工作”,图像显示正确。但问题是视图需要10-20秒才能为用户进行交互做好准备。在那些10-20秒之前,滚动缓慢,并且在所有图像都已加载之前按钮不会执行任何操作。

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.

***更新:Mike Swanson的回答非常彻底,确实解决了我的大部分问题。缓慢和阻碍已经消失。

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++
}

1 个解决方案

#1


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.

首先,WatchKit中没有方法来确定WKInterfaceTable中的当前位置。这意味着您使用的任何逻辑都必须解决这个问题。

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.

其次,看起来你正在发送异步网络请求,然后在拥有数据时使用setImage。 setImage在将图像传输到设备之前将其编码为PNG,并且在现有代码中,这发生在主线程上。如果您不需要PNG图像,我会考虑使用setImageData发送JPEG编码数据,以进一步减少需要传输到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.

最后,您似乎正在双重传输图像。一次使用setImage,两次使用addCachedImage。收到图像后,我建议调度后台线程,然后使用WKInterfaceDevice的addCachedImageWithData(可以安全地在后台线程上使用)来发送数据。然后,在使用setImageNamed设置now-cached图像之前,先调回主线程。

#1


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.

首先,WatchKit中没有方法来确定WKInterfaceTable中的当前位置。这意味着您使用的任何逻辑都必须解决这个问题。

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.

其次,看起来你正在发送异步网络请求,然后在拥有数据时使用setImage。 setImage在将图像传输到设备之前将其编码为PNG,并且在现有代码中,这发生在主线程上。如果您不需要PNG图像,我会考虑使用setImageData发送JPEG编码数据,以进一步减少需要传输到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.

最后,您似乎正在双重传输图像。一次使用setImage,两次使用addCachedImage。收到图像后,我建议调度后台线程,然后使用WKInterfaceDevice的addCachedImageWithData(可以安全地在后台线程上使用)来发送数据。然后,在使用setImageNamed设置now-cached图像之前,先调回主线程。