🌝

SwiftUIにGIPHY入れてGIF表示してみた

2023/04/20に公開

さいしょに

SwiftUIでGIPHYを使おうと公式のドキュメントを見ると、GIF画像を選択するサンプルしかなくて表示する部分を実装する必要があったので、試行錯誤して分かったことなどをまとめました。

codeまとめ。
GiphyMediaView.swift
import SwiftUI
import GiphyUISDK

struct GiphyMediaView: UIViewRepresentable {
    var media: GPHMedia?

    func makeUIView(context: Context) -> GPHMediaView {
        let mediaView = GPHMediaView()
        mediaView.media = media
        return mediaView
    }

    func updateUIView(_ uiView: GPHMediaView, context: Context) {
        uiView.media = media
    }
}
GiphyPicker.swift
import SwiftUI
import GiphyUISDK

struct GiphyPicker: UIViewControllerRepresentable {
    @Binding var media: GPHMedia?

    func makeUIViewController(context: UIViewControllerRepresentableContext<GiphyPicker>) -> GiphyViewController {
        Giphy.configure(apiKey: "your api key")
        let giphy = GiphyViewController()
        giphy.swiftUIEnabled = true
        giphy.delegate = context.coordinator

        return giphy
    }

    func updateUIViewController(_ uiViewController: GiphyViewController, context: UIViewControllerRepresentableContext<GiphyPicker>) {
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    final class Coordinator: NSObject, GiphyDelegate {
        var parent: GiphyPicker

        init(_ parent: GiphyPicker) {
            self.parent = parent
        }

        func didSelectMedia(giphyViewController: GiphyViewController, media: GPHMedia) {
            // mediaをviewに渡す
            parent.media = media
        }
    }
}
ContentView.swift
struct ContentView: View {
    @State private var isShow = false
    @State var media: GPHMedia? = nil

    var body: some View {
        VStack {
            if let media {
                GiphyMediaView(media: media) // 選択したmediaを表示
            }
            Button("Picker表示") {
                isShow.toggle()
            }
            .sheet(isPresented: $isShow) {
                GiphyPicker(media: $media)
            }
        }
    }
}

GIPHY SDKのインストール

  • SDKリポジトリ
    ドキュメントやExampleなどはこのリポジトリで管理されてます。

自分はCocoaPodsを使ってるので、CocoaPodsでのやり方を書いておきます。
といってもGiphyを追加してpod installするだけです。

pod 'Giphy'

Swift Package Managerなども使えるので詳しくはこちらを参照してください。

別途やること

使うにあたってAPIキーが必要になるので、
GIPHY Developersからアカウントを作成しAPIキーを取得しておいてください。アカウントを作成して、アプリの情報を入力して作るとAPIキーを取得できるようになります。

公式サンプルでPicker実装

公式画像
リポジトリの画像から拝借

GIFを選択するシートのビューの実装はこの部分に記載されてるので、必要な箇所だけ実装していきます。

GiphyPicker.swift
import SwiftUI
import GiphyUISDK // ライブラリ読み込み

struct GiphyPicker: UIViewControllerRepresentable {
    func makeUIViewController(context: UIViewControllerRepresentableContext<GiphyPicker>) -> GiphyViewController {
        Giphy.configure(apiKey:"") // APIキーを入れる
        let giphy = GiphyViewController() // GiphyPickerのViewControllerを生成
        giphy.swiftUIEnabled = true
        return giphy
    }

    func updateUIViewController(_ uiViewController: GiphyViewController, context: UIViewControllerRepresentableContext<GiphyPicker>) {
    }
}

GiphyPickerをButtonを押下した時にsheetで表示できるようにします。

ContentView.swift
struct ContentView: View {
    @State private var isShow = false

    var body: some View {
        Button("Picker表示") {
            isShow.toggle()
        }
        .sheet(isPresented: $isShow) {
            GiphyPicker()
        }
    }
}

apiKeyと書いてある部分に自分のAPIキーを入れると選択ビューが表示されます。
サンプルはここまでしかなくGIFを選択した際のGIFデータを取得できないので、ここからはワイが実装した部分を書いていきます。

Delegateメソッドで選択したGIFを取得する

SDKではGIFデータをmedia(GPHMedia)に格納していて、GIF表示ビュー(GPHMediaView)にもこのmediaを使います。

そのため、まずはCoordinatorの実装をしてGiphyDelegateに用意されているdelegateメソッドのdidSelectMediaで、選択したmediaを取得できるようにします。

func makeCoordinator() -> Coordinator {
    Coordinator(self)
}

final class Coordinator: NSObject, GiphyDelegate {
    var parent: GiphyPicker

    init(_ parent: GiphyPicker) {
        self.parent = parent
    }
    func didSelectMedia(giphyViewController: GiphyViewController, media: GPHMedia) {
        print(media) // 選択したmediaをデバッグコンソールに表示
    }
}

ここらへんの書き方は、大体どのUIViewControllerRepresentableでも同じな気がします。というかワイは「まぁこういうものだよね」ってぐらいの雑な理解しかしてません。

クラスとmakeCoordinator関数が作成できたら、GiphyViewControllerのdelegateに作ったCoordinatorを割り当てていきます。

func makeUIViewController(context: UIViewControllerRepresentableContext<GiphyPicker>) -> GiphyViewController {
    // ... 省略
    giphy.delegate = context.coordinator // 作成したcoordinatorクラスを割り当て

    return giphy
}

これで、Picker内部でGIFをタップするとdidSelectMediaが発火し、選択したmediaがデバッグメッセージに表示されます。

Bindingで選択したGIFを取得できるようにする

まだ、今の段階だとタップしたmedia情報が取得できるだけなので、取得したmediaをSwiftUI側で使えるようにプロパティを設定していきます。

今回はUIViewControllerRepresentableにmediaプロパティを設定しました。
また、型はGPHMediaを指定し、選択してない状態があるためoptionalで定義しnilでも使えるようにしました。

Delegateメソッドからプロパティを直接呼び出せないので、先ほどGiphyPickerのプロパティを定義していたparentからparent.mediaでアクセスしmediaを渡します。

struct GiphyPicker: UIViewControllerRepresentable {
     @Binding var media: GPHMedia? // Bindingプロパティの設定

          // ... 省略

          func didSelectMedia(giphyViewController: GiphyViewController, media: GPHMedia) {
              // bindingにmediaを渡す
              parent.media = media
          }
     }
 }

そして、GiphyPickerを呼び出す側でState定義したmediaを渡します。

ContentView.swift
@State var media: GPHMedia? = nil
// ... 省略
GiphyPicker(media: $media)

これで、PickerでGIFを選択するとmediaを取得できるようになりました。選択すると@Stateのmediaに値が入りView側で扱えるようになります。

まだ表示部分を作ってないので、表示ビューを作成していきますよい。

UIViewRepresentableでGifの表示ビュー実装

表示ビューはGPHMediaViewを使います。
SwiftUIだとそのままでは使えないため、UIViewRepresentableで使える形にラップしていきます。
ラップして表示してるだけなので一気に全部実装していきます

プロパティに同じようにmedia(GPHMedia)を設定し、makeUIViewでGPHMediaViewを生成して選択したmediaを渡して表示させます。

GiphyMediaView.swift
import SwiftUI
import GiphyUISDK

struct GiphyMediaView: UIViewRepresentable {
    var media: GPHMedia?

    func makeUIView(context: Context) -> GPHMediaView {
        let mediaView = GPHMediaView()
        mediaView.media = media // mediaViewにmediaデータを設定して表示する
        return mediaView
    }

    func updateUIView(_ uiView: GPHMediaView, context: Context) {
        uiView.media = media // 違うGIFデータが来た時にアップデートする
    }
}

最後に、選択したmediaを表示できるように呼び出してみます。

ContentView.swift
struct ContentView: View {
    @State private var isShow = false
    @State var media: GPHMedia? = nil

    var body: some View {
        VStack {
            if let media {
                GiphyMediaView(media: media) // 選択したmediaを表示
            }
            Button("Picker表示") {
                isShow.toggle()
            }
            .sheet(isPresented: $isShow) {
                GiphyPicker(media: $media)
            }
        }
    }
}

完成です。

終わりに

案外GIF扱うの楽で助かりました、やっぱりAPIは楽にアプリの機能を拡張できるので使うの楽しいですね。
初めて、一からUIViewControllerRepresentableやUIViewRepresentableを使って書いてみましたが案外理解が深まったのと意外と簡単に作れたので今後UIKit使うのもいけそうです。

最後に使ったパラメータや、GIPHYのIDからmediaを取得する部分などを書いて終わりにします。

使ったパラメータ

GIPHYのGPHMediaViewGiphyViewContollerでワイが使ったパラメータの説明などを軽くしておきます。まだあんまり分かってないので理解したらいつか書きます多分

GPHMediaViewで使ったもの

GPHMediaViewではそんなに設定することはなかったです。
アスペクト比がおかしい場合はcontentModeの設定などできます。

let mediaView = GPHMediaView()
mediaView.media = media
mediaView.contentMode = .scaleAspectFit
return mediaView

ただ、アスペクト比を保つよりも、実装時MediaViewのサイズをSwiftUI側でframe使って変えても画像自体のサイズが変わらずはみ出てしまうなどサイズ調整の方につまづきました。結局解決できてなくて課題です

View側にfixedSize()を指定することで、一応サイズを変更できたので一旦良しとしています。

GiphyMediaView(media: media)
    .fixedSize()
    .frame(minHeight: 100, maxHeight: 300)

ワイはfixedSizeを設定して、高さを最小100, 最大300設定していい感じに表示できるよう調整してました。(うまくいってるのかは謎です)

注意点

  • 幅が固定になることとwidthを使うとうまくいきません。
    ここら辺は何でかよく分からないので諦めました。

GiphyViewControllerで使ったもの

  • GiphyViewController.trayHeightMultipiler: snap pointの高さ
    多分GIFを表示している部分の高さが変わるのかな(デフォルトは0.7)
  • shouldLocalizeSearch: スマホの設定言語で検索をするかしないかのフラグ。(デフォルトはfalseで英語)
  • theme: GPHThemeを入れてPickerのデザインを変えられます。Typeを指定することでデザインが変わります。指定できるタイプには .automatic, .dark, .lightBlur, .darkBlurがあります。また、カスタムのデザインも作れるみたいです。
  • mediaTypeConfig: シートのタブに表示する画像を表示できます。配列でタイプを指定でき、指定できるタイプは以下です。
    • recents: 最近選択したもの
    • gifs
    • clips: videoみたいなの
    • stickers
    • text
    • emoji

設定するとこんな感じです。

GiphyViewController.trayHeightMultipiler = 0.7
let giphy = GiphyViewController()
giphy.swiftUIEnabled = true
giphy.shouldLocalizeSearch = true
giphy.mediaTypeConfig = [.gifs, .stickers, .recents]
giphy.theme = GPHTheme(type: .lightBlur)

return giphy

他にもあるみたいですが、ドキュメントになかったり読んでも分からんので一旦このくらいで。

IDからmediaを取得

公式にある実装ではIDからmediaが取得できるようになると、データを送る際もIDを送るだけで表示でき便利そうでした。
そのため、書いてあった部分を勉強ついでにasync/awaitで書き換えて実装してみました。

import GiphyUISDK

func getMedia(giphyId: String) async -> GPHMedia? {
    await withCheckedContinuation { continuation in
        guard let id = giphyId else {
            continuation.resume(returning: nil)
            return
        }

        GiphyCore.shared.gifByID(id) { response, error in
            if let error {
                print(error)
                continuation.resume(returning: nil)
            }
            if let media = response?.data {
                continuation.resume(returning: media)
            } else {
                continuation.resume(returning: nil)
            }
        }
    }
}

await getMedia(giphyId: "abcdefghijklmn") // mediaを取得

GiphyCoreを使うにも、APIキーの指定がいるみたいなので下記を起動時に呼び出しておくなどしておいてください。

GiphyCore.shared.apiKey = "your api key"

HELP

  • GIFデータのキャッシュとアプリ開発時のAPIキーの管理ベストプラクティス分かってないので、わかる方いたらコメント欄で是非に教えてください🙇

おわり。
参考になれば幸いです。

Discussion