12

I am developing Widgets for iOS and I really don't know how to download images for the widgets.

The widget currently downloads an array of Objects, and every object has a URL of an image. The idea is that every object makes a SimpleEntry for the Timeline.

What's the best way of achieving this? I read that Widgets shouldn't use the ObservableObject. I fetch the set of objects in the timeline provider, which seems to be what Apple recommends. But do I also download the images there and I wait until all are done to send the timeline?

Any advice would be very helpful,

Joan Cardona
  • 3,463
  • 2
  • 25
  • 43

2 Answers2

10

Yes, you should download the images in the timeline provider and send the timeline when they are all done. Refer to the following recommendation by an Apple frameworks engineer.

I use a dispatch group to achieve this. Something like:

let imageRequestGroup = DispatchGroup()
var images: [UIImage] = []
for imageUrl in imageUrls {
    imageRequestGroup.enter()
    yourAsyncUIImageProvider.getImage(fromUrl: imageUrl) { image in
        images.append(image)
        imageRequestGroup.leave()
    }
}
imageRequestGroup.notify(queue: .main) {
    completion(images)
}

I then use SwiftUI's Image(uiImage:) initializer to display the images

bee8ee
  • 346
  • 1
  • 12
  • This is the best solution at the moment. I used SDWebImage in my widget and this technique works reliably across iOS, iPadOS and macOS (Catalyst) (The host app, Widget will be native) – dezinezync Oct 02 '20 at 05:01
  • 1
    Should we use this inside "getTimeline(for configuration..." ? and when call completion(timeline) inside it!? – Ahmadreza Dec 13 '20 at 14:42
  • 1
    @Ahmadreza Yes, that's the way I'm using it. – bee8ee Dec 14 '20 at 16:58
  • @dezinezync You mean "loading the images before returning the timeline" is the best solution at the moment? Because if you use `SDWebImage`'s `WebImage` inside the widget view it will load the image while the view is rendered right? – fruitcoder Sep 16 '21 at 13:29
  • @fruitcoder yes. So by calling it in the timeline method, you're essentially pre-warming the SDWebImage cache so when its requested by the renderer, it's either immediately available or not in case the loading failed. – dezinezync Sep 19 '21 at 11:38
  • I used this function to get an image func downloadImage(url: URL, completion: @escaping (UIImage?) -> Void) { DispatchQueue.global(qos: .background).async { do { let data = try Data(contentsOf: url) let image = UIImage(data: data) DispatchQueue.main.async { completion(image) } } catch { DispatchQueue.main.async { completion(nil) } } } } – user2619824 Apr 08 '23 at 04:23
1

I dont have a good solution, but I try to use WidgetCenter.shared.reloadAllTimelines(), and it make sence. In the following code.

var downloadImage: UIImage?

func downloadImage(url: URL) -> UIImage {
    
    var picImage: UIImage!
    
    if self.downloadImage == nil {
        
        picImage = UIImage(named: "Default Image")
        
        DispatchQueue.global(qos: .background).async {
            do {
                let data = try Data(contentsOf: url)
                DispatchQueue.main.async {
                    self.downloadImage = UIImage.init(data: data)
                    if self.downloadImage != nil {
                        DispatchQueue.main.async {
                            WidgetCenter.shared.reloadAllTimelines()
                        }
                    }
                }
            } catch { }
        }
    } else {
        picImage = self.downloadImage
    }
    
    return picImage
}

Also you have to consider when to delete this picture. This like tableView.reloadData().

yang lu
  • 11
  • 2