Rudrank Riyam Apple Platforms developer. WWDC '19 scholar.

Adding GIFs to your iOS app with FLAnimatedImage and SwiftUI

8 min read 2335

Swift Logo

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:

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.


More great articles from LogRocket:


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 file
  • url: 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:) and updateUIView(_:context:), to the UIView 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:

No Idea 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:

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, and offset
  • 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:

Scrolling Giphy and Refreshing GIFs

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.

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page web and mobile apps.

.
Rudrank Riyam Apple Platforms developer. WWDC '19 scholar.

Leave a Reply