用FLAnimatedImage和SwiftUI将GIF添加到iOS应用中的教程

982 阅读10分钟

为了回应消费者对使用表情符号和GIF交流的兴趣,越来越多的公司正在将GIF动画纳入他们的电子邮件活动、网站和移动应用程序,以努力提高参与度和销售额。

图形交换格式文件是一个图像集合,按顺序播放,使其看起来像在移动。GIF可以用来分享演示,突出产品功能或变化,说明使用案例,或展示品牌个性。

许多流行的聊天应用程序,如iMessage和WhatsApp,以及社交平台,如Reddit或Twitter,支持发送和接收GIF。那么,iOS应用程序呢?嗯,截至目前,在SwiftUI或UIKit中还没有原生的内置支持来使用GIF。

为iOS构建一个高效的GIF图像加载器需要大量的时间和精力,但幸运的是,一些第三方框架是高性能的,可以显示GIF而没有任何帧延迟。

在这篇文章中,我们将演示如何使用一个流行的GIF库,即Flipboard的FLAnimatedImage,只需几行代码就能将GIF添加到你的iOS应用中。在文章的演示部分,我们将利用GIPHY的GIF,这是一个流行的数据库,提供广泛的动画GIF。

让我们开始吧,学习如何在我们的应用程序中加入一些令人惊叹的GIF!

安装FLAnimatedImage

以下三个依赖管理器中的任何一个都可以用来将FLAnimatedImage添加到一个项目中:

  • Cocoapods
  • Carthage
  • Swift软件包管理器

CocoaPods

要使用CocoaPods将FLAnimatedImage添加到项目中,请在Podfile中添加:

pod 'FLAnimatedImage'

然后,去你终端的项目目录,安装这个Pod,像这样:

pod install

Carthage

要将FLAnimatedImage添加到使用Carthage的项目中,请在Cartfile中添加以下代码:

github "Flipboard/FLAnimatedImage"

然后,为了只更新这个库,去你终端的项目目录,运行以下命令:

carthage update FLAnimatedImage

Swift 软件包管理器

要使用Swift Package Manager将FLAnimatedImage添加到项目中,请打开Xcode,进入菜单栏,并选择File > Add Packages。接下来,在项目的URL字段中粘贴以下链接。

https://github.com/Flipboard/FLAnimatedImage

然后,点击 "Next"并选择项目目标,该库应该被添加到该项目中。点击继续,你就把FLAnimatedImage 添加到项目中了!

在Objective-C中使用FLAnimatedImage

GitHub repo中的FLAnimatedImage的例子是用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];

我们想在SwiftUI框架中使用FLAnimatedImage,但在这样做之前,我们应该了解这个库的两个主要类:

  • FLAnimatedImage
  • FLAnimatedImageView

FLAnimatedImage

FLAnimatedImage 是一个帮助以高性能方式提供帧的类。我们用它来设置 FLAnimatedImageView类的图像属性。

为了加载一个GIF图像,我们将GIF转换成一个Data 值类型。FLAnimatedImage 有一个初始化器,接受这个Data

convenience init(animatedGIFData data: Data!) 

在创建一个类型为FLAnimatedImage 的图像实例后,我们可以用它来设置FLAnimatedImageView 的图像属性。

imageView.animatedImage

FLAnimatedImageView

FLAnimatedImageViewUIImageView的一个子类,并接受一个FLAnimatedImage

class FLAnimatedImageView

FLAnimatedImageView 在视图层次中自动播放GIF,当它被移除时就会停止。我们可以通过访问 animatedImage属性来设置动画图像。

在Swift中使用FLAnimatedImage

现在我们已经熟悉了FLAnimatedImage类和它们的用法,我们可以像这样把上面的Objective-C例子翻译成Swift。

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)

这个例子与UIKit有关。在UIKit中使用FLAnimatedImage非常容易,因为它是原生的。然而,为了在SwiftUI中使用FLAnimatedImage,我们必须创建一个自定义视图,利用UIViewRepresentable ,这是一个UIKit视图的包装器,我们用它来整合到SwiftUI的视图层次中。

因此,让我们创建我们的自定义视图,以简化与FLAnimatedImage的工作

GIF视图

我们将在 SwiftUI 中创建一个名为GIFView 的自定义视图,它可以帮助我们从本地资产和远程 URL 中加载 GIF。

在创建自定义视图之前,让我们创建一个枚举URLType ,定义两种情况:

  • name:本地文件的名称
  • url: 必须从服务器获取的GIF的URL
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
    }
  }
}

在上面的代码中,我们可以看到还有一个计算属性url ,如果我们同时提供远程URL和GIF的名称,就会返回本地URL。

我们创建一个新的文件,GIFView.swift 。在这个文件中,我们将导入FLAnimatedImage 库。

import FLAnimatedImage

接下来,我们创建一个struct ,GIFView,它符合UIViewRepresentable 协议。这个结构有一个初始化器,接收URL类型。

struct GIFView: UIViewRepresentable {
  private var type: URLType

  init(type: URLType) {
    self.type = type
  }
}

然后,我们添加两个闭包,即FLAnimatedImageViewUIActivityIndicatorView 的实例,以便在GIF加载时显示一个活动指示器。

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
}()

在上面的代码中,我们为FLAnimatedImageView 的转角半径指定了一个值24 ,但我们可以根据自己的意愿来定制。

UIViewRepresentable 协议有两个必须实现的方法。第一个,makeUIView(context:) ,创建视图对象,UIView ,并配置初始状态。第二个,updateUIView(_:context:) ,更新指定视图的状态。

makeUIView(context:) 方法中,我们:

  • 创建一个UIView
  • 将两个视图,makeUIView(context:)updateUIView(_:context:) ,作为子视图添加到UIView 中。
  • 激活必要的约束
  • 返回视图UIView

下面是makeUIView(context:) 方法的代码:

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
}

updateUIView(_:context:) 方法中,我们:

  • 开始制作活动指示器的动画
  • 检查URL是否是可选的;如果是可选的,我们从该方法中返回
  • 创建一个Data 的实例,其中包含URL的内容
  • 创建一个FLAnimatedImage 的实例,传递GIF动画的数据
  • 停止活动指示器的动画,并将FLAnimatedView's animated image属性设置为所获取的图像。

下面是updateUIView(_:context:) 方法的代码:

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
      }
    }
  }
}

加载GIF动画日期需要时间,所以我们在后台的并发线程上运行它以避免阻塞UI。然后,我们在主线程上设置图片。

下面是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
        }
      }
    }
  }
}

现在我们有了一个将FLAnimatedImage 与SwiftUI一起使用的视图,在SwiftUI中使用它就像在UIKit中一样简单。

让我们来探讨两个简单的例子,以及一个更复杂的、真实世界的例子。

简单的 GIF 演示。FLAnimatedImage和SwiftUI

我们将从assets文件夹中存在的一个简单的GIF例子开始。首先,下载我们将在本例中使用的 GIF,happy-work-from-home

我们使用之前描述的方法创建GIFView ,。接下来,我们所要做的就是传入GIF的名称,像这样:

struct ContentView: View {
  var body: some View {
    GIFView(type: .name("happy-work-from-home"))
      .frame(maxHeight: 300)
      .padding()
  }
}

在模拟器上运行这段代码,我们就可以得到一个GIF动画:

No Idea GIF

下面是另一个例子,使用同样的GIF,但从一个远程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()
  }
}

如果我们在模拟器上运行这段代码,我们会看到一个GIF动画。注意,在获取GIF的同时,屏幕上会显示一个活动指示器:

Activity Indicator

现在,让我们来看看一个更复杂的、真实世界的例子!

复杂的、真实世界的GIF演示

为了充分利用FLAnimatedImage的优势,让我们来探讨一个同时加载许多GIF的例子。这个例子将展示这个框架在内存压力下的性能,通过显示和流畅地滚动大量的GIF图片。

GIPHY是最大的GIFs市场。它也有一个开发者项目,允许我们使用所提供的API来获取他们的数据。我们将使用它的一个API来获取流行的GIF,并将它们显示在一个列表中。

首先,在这里创建一个GIPHY账户。登录后,点击创建新的应用程序并选择API。然后,输入你的应用程序名称和描述,并确认协议。接下来,点击创建应用程序按钮,并注意到API密钥

趋势端点返回当天最相关和最吸引人的内容列表。它看起来像这样:

http://api.giphy.com/v1/gifs/trending 

为了验证,我们将API密钥作为参数传递。我们还可以通过使用limit 参数来限制一个响应中的GIF数量,offset ,以获得下一组响应。

该端点返回一个巨大的JSON文件,但我们要修剪它以满足我们的要求。为此,创建另一个文件,GIFs.swift ,并添加以下代码:

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
}

端点返回一个GIF 的数组,每个GIF 都有一个标题和一个图像URL。

继续创建用户界面,我们添加一个变量来存储GIF,并跟踪offset 。我们的想法是,当我们用拉到刷新的手势刷新屏幕时,它就会获取下一批数据:

struct ContentView: View {
  @State private var gifs: [GIF] = []
  @State private var offset = 0
}

接下来,我们将添加一个方法,从趋势端点获取GIF数据:

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
  }
}

上面的代码执行了以下动作:

  • 创建趋势端点的URL,并添加查询参数api_key,limit, andoffset
  • 从URL中获取数据并使用GIFs 结构对其进行解码
  • 设置数据到变量中gifs

对于用户界面,我们有一个列表,在gifs 的数组上加载,并在获取URL的GIFView 中显示单个GIF。

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()
    }
  }
}

当通过下拉屏幕刷新视图时,它通过10 增加offset ,获取新的GIF,并更新屏幕。

下面是一个屏幕记录,显示了视图被刷新时发生的情况:

Scrolling Giphy and Refreshing GIFs

就这样,我们创建了一个完整的高性能应用程序,显示了来自GIPHY的趋势GIF!

结论

我们必须投入大量的精力来创建一个处理GIF的高性能动画UIImageView 。另外,我们可以利用现有的FLAnimatedImage框架,它被许多流行的应用程序所使用,如Facebook、Instagram、Slack和Telegram。

有了自定义的GIFView ,我们只需要在SwiftUI中写一行代码,就可以在我们的应用程序中显示令人惊叹的GIF了祝你实验和编码愉快!GIF动画只是增强你的iOS应用的用户体验和用户界面的众多方法之一。