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:
Any of the following three dependency managers may be used to add FLAnimatedImage to a project:
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
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
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!
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
classFLAnimatedImage
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
classFLAnimatedImageView
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.
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!
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 serverenum 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:
UIView
makeUIView(context:)
and updateUIView(_:context:)
, to the UIView
as subviewsUIView
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:
Data
that contains the contents of the URLFLAnimatedImage
passing the animated GIF dataFLAnimatedView
‘s animated image property to the fetched imageHere’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.
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!
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:
api_key
, limit
, and offset
GIFs
structuregifs
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!
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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]