In response to consumer interest in communicating with emojis and GIFs, an increasing number of companies are incorporating animated GIFs into their email campaigns, websites, and mobile apps in an effort to increase engagement and sales.
Graphics Interchange Format files are a collection of images that are played in a sequence so that they appear to be moving. GIFs can be used to share demos, highlight product features or changes, illustrate a use case, or showcase brand personality.
Many popular chat apps, like iMessage and WhatsApp, and social platforms, like Reddit or Twitter, support sending and receiving GIFs. So, what about iOS apps? Well, as of this writing, there‘s no native inbuilt support to use GIFs in SwiftUI or UIKit.
Building an efficient GIF image loader for iOS would take significant time and effort, but fortunately, some third-party frameworks are performant and can display GIFs without any frame delays.
In this article, we’ll demonstrate how to use a popular GIF library, FLAnimatedImage by Flipboard, to add GIFs to your iOS app with just a few lines of code. In the demo portion of the article, we’ll utilize GIFs from GIPHY, a popular database offering a wide range of animated GIFs.
Let’s get started, and learn how we can include some amazing GIFs in our apps!
Jump ahead:
- Installation
- Using FLAnimatedImage with Objective-C
- Using FLAnimatedImage with Swift
- Simple GIF demo with FLAnimatedImage
- Complex, real-world GIF demo with FLAnimatedImage
Installing FLAnimatedImage
Any of the following three dependency managers may be used to add FLAnimatedImage to a project:
- Cocoapods
- Carthage
- Swift Package Manager
CocoaPods
To add FLAnimatedImage to a project using CocoaPods, add the following code to the Podfile:
pod 'FLAnimatedImage'
Then, go to your terminal’s project directory and install the pod, like so:
pod install
Carthage
To add FLAnimatedImage to a project using Carthage, add the following code in the Cartfile:
github "Flipboard/FLAnimatedImage"
Then, to update only this library, go to your terminal’s project directory and run the following command:
carthage update FLAnimatedImage
Swift Package Manager
To use Swift Package Manager to add FLAnimatedImage to a project, open Xcode, go to the menu bar, and select File > Add Packages. Next, paste the following link in the project URL field:
https://github.com/Flipboard/FLAnimatedImage
Then, click on Next and select the project target to which the library should be added. Click continue, and you’ve added FLAnimatedImage
to the project!
Using FLAnimatedImage with Objective-C
The example in the GitHub repo for FLAnimatedImage is written in Objective-C:
#import "FLAnimatedImage.h" FLAnimatedImage *image = [FLAnimatedImage animatedImageWithGIFData:[NSData dataWithContentsOfURL:[NSURL URLWithString:@"https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif"]]]; FLAnimatedImageView *imageView = [[FLAnimatedImageView alloc] init]; imageView.animatedImage = image; imageView.frame = CGRectMake(0.0, 0.0, 100.0, 100.0); [self.view addSubview:imageView];
We want to use FLAnimatedImage with the SwiftUI framework, but before doing so, we should understand this library’s two main classes:
FLAnimatedImage
FLAnimatedImageView
FLAnimatedImage
class
FLAnimatedImage
is a class that helps deliver the frames in a performant way. We use it to set the image property of the FLAnimatedImageView
class.
To load a GIF image, we convert the GIF into a Data
value type. FLAnimatedImage
has an initializer that accepts this Data
:
convenience init(animatedGIFData data: Data!)
After creating an instance of an image of the type FLAnimatedImage
, we can use it to set the image property of FLAnimatedImageView
.
imageView.animatedImage
FLAnimatedImageView
class
FLAnimatedImageView
is a subclass of UIImageView
and takes a FLAnimatedImage
.
class FLAnimatedImageView
FLAnimatedImageView
automatically plays the GIF in the view hierarchy and stops when it’s removed. We can set the animated image by accessing the animatedImage
property.
Using FLAnimatedImage with Swift
Now that we’re familiar with both FLAnimatedImage classes and their usage, we can translate the above Objective-C example into Swift like so:
let url = URL(string: "https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif")! let data = try! Data(contentsOf: url) /// Creating an animated image from the given animated GIF data let animatedImage = FLAnimatedImage(animatedGIFData: data) /// Creating the image view let imageView = FLAnimatedImageView() /// Setting the animated image to the image view imageView.animatedImage = animatedImage imageView.frame = CGRect(x: 0, y: 0, width: 100, height: 100) view.addSubview(imageView)
This example is relevant for UIKit. It’s very easy to use FLAnimatedImage with UIKit because it‘s native. However, to use FLAnimatedImage in SwiftUI, we have to create a custom view taking advantage of UIViewRepresentable
, a wrapper for a UIKit view that we use to integrate it into the SwiftUI view hierarchy.
So, let’s create our custom view to simplify working with FLAnimatedImage!
GIFView
We’ll create a custom view in SwiftUI called GIFView
, which helps us load GIFs from local assets and a remote URL.
Before creating the custom view, let’s create an enum URLType
that defines two cases:
name
: the name of the local fileurl
: the URL of the GIF that must be fetched from a server
enum URLType { case name(String) case url(URL) var url: URL? { switch self { case .name(let name): return Bundle.main.url(forResource: name, withExtension: "gif") case .url(let remoteURL): return remoteURL } } }
In the above code, we can see there is also a computed property url
that returns the local URL if we provide both the remote URL and the name of the GIF.
We create a new file, GIFView.swift
. In this file, we’ll import the FLAnimatedImage
library:
import FLAnimatedImage
Next, we create a struct
, GIFView, that conforms to the UIViewRepresentable
protocol. This structure has one initializer that takes the URL type:
struct GIFView: UIViewRepresentable { private var type: URLType init(type: URLType) { self.type = type } }
Then, we add two closures, an instance of FLAnimatedImageView
and UIActivityIndicatorView
, to show an activity indicator while the GIF loads:
private let imageView: FLAnimatedImageView = { let imageView = FLAnimatedImageView() imageView.translatesAutoresizingMaskIntoConstraints = false imageView.layer.cornerRadius = 24 imageView.layer.masksToBounds = true return imageView }() private let activityIndicator: UIActivityIndicatorView = { let activityIndicator = UIActivityIndicatorView() activityIndicator.translatesAutoresizingMaskIntoConstraints = false return activityIndicator }()
In the above code, we specify a value of 24
for the FLAnimatedImageView
corner radius, but we can customize this however we wish.
The UIViewRepresentable
protocol has two required methods that must be implemented. The first, makeUIView(context:)
, creates the view object, UIView
, and configures the initial state. The second, updateUIView(_:context:)
, updates the state of the specified view.
In the makeUIView(context:)
method, we:
- Create a
UIView
- Add both views,
makeUIView(context:)
andupdateUIView(_:context:)
, to theUIView
as subviews - Activate the necessary constraints
- Return the
UIView
Here’s the code for the the makeUIView(context:)
method:
func makeUIView(context: Context) -> UIView { let view = UIView(frame: .zero) view.addSubview(activityIndicator) view.addSubview(imageView) imageView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true imageView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true return view }
In the updateUIView(_:context:)
method, we:
- Start animating the activity indicator
- Check if the URL is optional or not; if it’s optional, we return from the method
- Create an instance of
Data
that contains the contents of the URL - Create an instance of
FLAnimatedImage
passing the animated GIF data - Stop the activity indicator from animating and set the
FLAnimatedView
‘s animated image property to the fetched image
Here’s the code for the updateUIView(_:context:)
method:
func updateUIView(_ uiView: UIView, context: Context) { activityIndicator.startAnimating() guard let url = type.url else { return } DispatchQueue.global().async { if let data = try? Data(contentsOf: url) { let image = FLAnimatedImage(animatedGIFData: data) DispatchQueue.main.async { activityIndicator.stopAnimating() imageView.animatedImage = image } } } }
The image takes time to load the animated GIF date, so we run it in the background on a concurrent thread to avoid blocking the UI. Then, we set the image on the main thread.
Here’s the final code for the GIFView
:
import SwiftUI import FLAnimatedImage struct GIFView: UIViewRepresentable { private var type: URLType init(type: URLType) { self.type = type } private let imageView: FLAnimatedImageView = { let imageView = FLAnimatedImageView() imageView.translatesAutoresizingMaskIntoConstraints = false imageView.layer.cornerRadius = 24 imageView.layer.masksToBounds = true return imageView }() private let activityIndicator: UIActivityIndicatorView = { let activityIndicator = UIActivityIndicatorView() activityIndicator.translatesAutoresizingMaskIntoConstraints = false return activityIndicator }() } extension GIFView { func makeUIView(context: Context) -> UIView { let view = UIView(frame: .zero) view.addSubview(activityIndicator) view.addSubview(imageView) imageView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true imageView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true return view } func updateUIView(_ uiView: UIView, context: Context) { activityIndicator.startAnimating() guard let url = type.url else { return } DispatchQueue.global().async { if let data = try? Data(contentsOf: url) { let image = FLAnimatedImage(animatedGIFData: data) DispatchQueue.main.async { activityIndicator.stopAnimating() imageView.animatedImage = image } } } } }
Now that we have a view for using FLAnimatedImage
with SwiftUI, it’s as easy to use it with SwiftUI as it was with UIKit.
Let’s explore two simple examples, as well as a more complex, real-world example.
Simple GIF demos: FLAnimatedImage and SwiftUI
We’ll start with a simple example of a GIF present in the assets folder. First, download the GIF, happy-work-from-home
, that we’ll use in this example.
We create GIFView
, using the method described previously. Next, all we have to do is pass in the name of the GIF, like so:
struct ContentView: View { var body: some View { GIFView(type: .name("happy-work-from-home")) .frame(maxHeight: 300) .padding() } }
Running this code on the simulator provides us with an animated GIF:
Here’s another example using the same GIF, but fetching it from a remote URL:
struct ContentView: View { var body: some View { GIFView(type: .url(URL(string: "https://media.giphy.com/media/Dh5q0sShxgp13DwrvG/giphy.gif")!)) .frame(maxHeight: 300) .padding() } }
If we run this code on the simulator, we’ll see an animated GIF. Note that while the GIF is being fetched, the screen displays an activity indicator:
Now, let’s move on to a more complex, real-world example!
Complex, real-world GIF demo
To take full advantage of FLAnimatedImage, let’s explore an example where we load many GIFs simultaneously. This example will demonstrate how performant this framework is under memory pressure by displaying and scrolling smoothly through a large number of GIFs.
GIPHY is the biggest marketplace for GIFs. It also has a developer program that allows us to fetch their data using the provided APIs. We’ll use one of its APIs for fetching the trending GIFs and displaying them in a list.
First, create a GIPHY account here. After logging in, click on the Create New App and select API. Then, enter your app name and description, and confirm to the agreements. Next, click the Create App button and take note of the API Key.
The trending endpoint returns a list of the most relevant and engaging content for that day. It looks like this:
http://api.giphy.com/v1/gifs/trending
For authentication, we pass the API key as a parameter. We can also limit the number of GIFs in one response by using the limit
parameter and offset
to get the next set of responses.
The endpoint returns a massive JSON file, but we’ll trim it down to use it for our requirements. To do so, create another file, GIFs.swift
, and add the following code:
import Foundation struct GIFs: Codable { let data: [GIF] } struct GIF: Codable, Identifiable { let id: String let title: String let images: Images } struct Images: Codable { let original: GIFURL } struct GIFURL: Codable { let url: String }
The endpoint returns an array of GIF
, and each GIF
has a title and an image URL.
Moving on to creating the UI, we add a variable that stores the GIFs and keeps track of the offset
. The idea is that when we refresh the screen with a pull-to-refresh gesture, it fetches the next batch of data:
struct ContentView: View { @State private var gifs: [GIF] = [] @State private var offset = 0 }
Next, we’ll add a method to fetch the GIF data from the trending endpoint:
extension ContentView { private func fetchGIFs() async { do { try await fetchGIFData(offset: offset) } catch { print(error) } } private func fetchGIFData(for limit: Int = 10, offset: Int) async throws { var components = URLComponents() components.scheme = "https" components.host = "api.giphy.com" components.path = "/v1/gifs/trending" components.queryItems = [ .init(name: "api_key", value: "<API KEY>"), // <-- ADD THE API KEY HERE .init(name: "limit", value: "\(limit)"), .init(name: "offset", value: "\(offset)") ] guard let url = components.url else { throw URLError(.badURL) } let (data, _) = try await URLSession.shared.data(from: url) gifs = try JSONDecoder().decode(GIFs.self, from: data).data } }
The above code performs the following actions:
- Creates the URL for the trending endpoint and adds the query parameters for
api_key
,limit
, andoffset
- Fetches the data from the URL and decodes it using the
GIFs
structure - Sets the data to the variable
gifs
For the UI, we have a list that loads over the array of gifs
and shows individual GIFs in the GIFView
that takes the URL:
extension ContentView { var body: some View { NavigationView { if gifs.isEmpty { VStack(spacing: 10) { ProgressView() Text("Loading your favorite GIFs...") } } else { List(gifs) { gif in if let url = URL(string: gif.images.original.url) { GIFView(type: .url(url)) .frame(minHeight: 200) .listRowSeparator(.hidden) } } .listStyle(.plain) .navigationTitle("GIPHY") } } .navigationViewStyle(.stack) .task { await fetchGIFs() } .refreshable { offset += 10 await fetchGIFs() } } }
When the view is refreshed by pulling down on the screen, it increases the offset
by 10
, fetches new GIFs, and updates the screen.
Here’s a screen recording showing what happens when the view is refreshed:
With that, we’ve created a full performant app that shows the trending GIFs from GIPHY!
Conclusion
We’d have to put in a lot of effort to create a performant animated UIImageView
for handling GIFs. Alternatively, we can take advantage of the existing FLAnimatedImage framework, used by many popular apps such as Facebook, Instagram, Slack, and Telegram.
With the custom GIFView
, we only need one line of code in SwiftUI to display amazing GIFs in our app! Have fun experimenting and coding! Animated GIFs are just one of many ways to enhance your iOS app’s UX and UI.
The post Adding GIFs to your iOS app with FLAnimatedImage and SwiftUI appeared first on LogRocket Blog.
from LogRocket Blog https://ift.tt/F8tIpWQ
via Read more