While working on one of my private projects which deals with showing large photos as small thumbnails in a list, I found myself needing something like AsyncImage but for any kind of data sources. AsyncImage looks pretty great, but sad it is limited to loading images from URL. It has building blocks like providing placeholder and progress views. In my case, I needed something where instead of the URL argument, I would have an async closure which returns image data. This would give me enough flexibility for different cases like loading a photo from a file or even doing what AsyncImage is doing, loading image data from a server. I would love to know why Apple decided to go for a narrow use-case of loading images from URL but not for more generic approach. In addition, I would like to pre-define the target image size which allows me to scale the image to smaller size and therefore saving memory usage which would increase a lot when dealing with large photos. Enough talk, let’s jump in.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The AsyncPhoto type is a generic over 4 types: ID, Content, Progress, Placeholder. Last three are SwiftUI views and the ID is equatable. This allows us for notifying the AsyncPhoto when to reload the photo by calling the data closure. Basically the same way as the task(id:priority:_:) is working – if the id changes, work item is run again. Since we expect to deal with large photos, we want to scale images before displaying them. Since the idea is that the view does not change the size while it is loading, or displaying a placeholder, we’ll require to pre-define the scaled size. Scaled size is used for creating a thumbnail image and also setting the AsyncPhoto’s frame view modifier to equal to that size. We use a data closure here for giving a full flexibility on how to provide the large image data.
AsyncImage has a separate type AsyncImagePhase for defining different states of the loading process. Since we need to do the same then, let’s add AsyncPhoto.Phase.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This allows us to use a switch statement in the view body and defining a local state for keeping track of in which phase we currently are. The view body implementation is pretty simple since we use view builders for content, progress and placeholder states. Since we want to have a constant size here, we use the frame modifier and the task view modifier is the one managing scheduling the reload when id changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The load function is updating the phase state and triggering the heavy load of scaling the image.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The prepareScaledImage is another function which wraps the work of fetching the image data and scaling it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
I am using an UIImage extension for scaling the image data. The implementation goes like this:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Here is an example of using AsyncPhoto from my test app, where I replaced photos with generated image data.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This year Apple added CGAnimateImageAtURLWithBlock and CGAnimateImageDataWithBlock for animating GIFs and APNGs on all the platforms to the ImageIO framework. We can pass in URL or data and get callbacks when animation changes the current frame. In Xcode 11 beta 7 implicit bridging to Swift is disabled for those APIs and therefore we need to create a small wrapper around it in Objective-C.
Creating ImageFrameScheduler for managing CGAnimateImageAtURLWithBlock in Objective-C
Calling CGAnimateImageAtURLWithBlock starts the animation immediately. When animation frame changes, the handler block is called with frame index, current animation frame image and stop argument. When setting stop to YES, we can stop the animation. With this in mind we can create ImageFrameScheduler what takes in URL and has methods for starting and stopping the animation. Then we can expose this class to Swift and use it for managing the animation.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
ImageAnimator conforming to ObservableObject in Swift
When updating views in SwiftUI, we can use ObservableObject protocol and @Published property wrapper what enables SwiftUI to get notified when the ObservableObject changes. This means that we need a model object written in Swift what stores our Objective-C class ImageFrameScheduler and exposes the current animation frame when animation is running. Whenever we update the property internally, property wrapper will take care of notifying SwiftUI to update the view.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
ContentView displaying animation frames in SwiftUI
Integrating ImageAnimator with ContentView is now pretty straight-forward, we check if animation frame image is available and display it. Animation is started when SwiftUI appears and stopped when it disappears.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Although CGAnimateImageAtURLWithBlock and CGAnimateImageDataWithBlock are not directly usable in Swift, we can get away from it by adding a simple wrapper class in Objective-C. ImageFrameScheduler could be used in non-SwiftUI views by updating UIImageView when frame changes. In SwiftUI, views can use ImageAnimator for storing the current animation frame and using @Published property wrapper for letting SwiftUI view to know when to refresh.