【SwiftUI】UIViewRepresentable の使い方とライフサイクルを理解する

はじめましての人ははじめまして。そうでないひとはお久しぶりです。猫ロキP(@deflis/id:deflis55)です。

この記事は SwiftUI Advent Calendar 2024はてなエンジニア Advent Calendar 2024 の 21 日目の記事です。 大遅刻してすみません。

みなさん、SwiftUIは使っていますか? 自分は今年からiOS開発を始めたので、ほとんどSwiftUIで書いています。

ですが、稀にどうしてもUIKitを使わなければならない場面があります。 例えば、広告SDKを使うときだったり、UICollectionViewを使いたいときだったり。

そういうときに便利なのが UIViewRepresentable1UIViewControllerRepresentable2 です。 今回は、これらのクラスの使い方について説明したいと思います。

makeUIView(context:)updateUIView(_:context:) とライフサイクル

UIViewRepresentableUIViewControllerRepresentable は、SwiftUI と UIKit を組み合わせるためのプロトコルです。 これらのプロトコルを使うことで、SwiftUI で UIKit の View や ViewController を使うことができます。使い方やライフサイクルはこの2つで大きく変わらないので、ここでは UIViewRepresentable を例に説明します。3

UIViewRepresentable を実装するには、最低でも makeUIView(context:) メソッドと updateUIView(_:context:) メソッドを実装する必要があります。

import SwiftUI

struct MyView: UIViewRepresentable {
    func makeUIView(context: Context) -> UIView {
        // ここで View を生成して返す。初期化処理が必要な場合ここに書く。
        return UIView()
    }

    func updateUIView(_ uiView: UIView, context: Context) {
        // ここに View の更新処理を書く。値の更新処理はここに書く。
    }
}

この2つのメソッドの呼び出しは以下の感じのライフサイクルで行われます。

stateDiagram-v2
    [*] --> makeUIView
    
    makeUIView --> updateUIView
    updateUIView --> View描画完了
    View描画完了 --> [*]


    View描画完了 --> updateUIView : Stateの値が変更された場合

ここで注意するべき点は、初回描画時に必ず makeUIView(context:) が呼ばれるのは当たり前ですが、 updateUIView(_:context:) も初回描画時に呼ばれることです。ですので、値が更新されたときの処理だけを updateUIView(_:context:) に書こうとすると、初回描画時にもその処理が呼ばれてしまいます。気をつけましょう。4

Coordinator の使い方と CoordinatorUIView への値の渡し方の注意点

また、 Coordinator というものを使うことで、SwiftUI の View と UIKit の View の間でデータをやり取りすることができます。 これは、例えば外部ライブラリのdelegateを使うときに便利です。

class MyUIView : UIView {
    var delegate: MyViewDelegate?
    // 省略
}

protocol MyUIViewDelegate {
    func myViewDidSomething()
}
struct MyView: UIViewRepresentable {
    let value: Int

    func makeUIView(context: Context) -> MyUIView {
        let view = MyUIView()
        view.delegate = context.coordinator
        return view
    }

    func updateUIView(_ uiView: MyUIView, context: Context) {
        // ここで値の更新処理を書く
        uiView.value = value
    }

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

    class Coordinator: NSObject, MyUIViewDelegate {
        var value: Int

        func myViewDidSomething() {
            // なんかする
        }
    }
}

ただ、このView専用に UIViewUIViewController を作る場合は、 Coordinator は使わなくてもいいかもしれません。なぜなら、別に UIViewUIViewController に必要なプロパティを持たせてもいいからです。

また、他の記事では Coordinator に自身をを渡していることが多いですが、やめたほうがいいと思います。 なぜなら、SwiftUI においては UIViewRepresentable は値が更新されるたびに再度生成されるため、 Coordinator に自身を渡しても、値が更新されるたびに別のインスタンスが生成されてしまうからです。 そのため、 context.coordinator に自身を渡すのは避けたほうがいいと思います。

// おそらくダメな例
struct MyView: UIViewRepresentable {
    let value: Int

    func makeUIView(context: Context) -> MyView {
        let view = MyUIView(myView: self)
        return view
    }

    func updateUIView(_ uiView: MyView, context: Context) {
    }

    class Coordinator: NSObject, MyUIViewDelegate {
        // MyViewは毎回生成されるので、最初のインスタンスを保持してはいけない
        let myView: MyView

        func myViewDidSomething() {
            // myView は最初のインスタンスを保持しているため、値が更新されていない
            print(myView.value)
        }
    }
}

この例では、 CoordinatorMyViewインスタンスを保持してしまっていますが、 MyView は値が更新されるたびに新しいインスタンスが生成されるため、最初のインスタンスを保持しても myView.value の値は更新されません。

ですが、以下のようなやり方であれば Coordinator に自身を渡すのも悪くないのではないかと思います。

// よい例
struct MyView: UIViewRepresentable {
    let value: Int

    func makeUIView(context: Context) -> MyView {
        let view = MyUIView()
        view.delegate = context.coordinator
        return view
    }

    func updateUIView(_ uiView: MyView, context: Context) {
        context.coordinator.updateUIView(self)
    }

    func makeCoordinator() -> Coordinator {
        return Coordinator(myView: self)
    }

    class Coordinator: NSObject, MyUIViewDelegate {
        var value: Int

        init(myView: MyView) {
            self.value = myView.value
        }

        // updateUIView(_:context:) で呼ばれるので、ここに値の更新処理を書く
        func updateUIView(_ myView: MyView) {
            self.value = myView.value
        }
    }
}

この例では、 CoordinatorMyViewインスタンスinit で渡していますが、そのままインスタンスを保持しているわけではなく value だけを保持しています。 その上で、 updateUIView(_:context:)CoordinatorupdateUIView(_:) メソッドを呼び出すことで、値の更新処理を行っています 。 こうすることで、意図通り Coordinator に値を保持させることができます。

今回は、Coordinator での値の保持方法について説明しましたが、以下のように専用の UIViewUIViewController を作る場合でも同じように書くことが出来ます。

struct MyView: UIViewRepresentable {
    let value: Int

    func makeUIView(context: Context) -> View {
        let view = View()
        view.makeUIView(self)
        return view
    }

    func updateUIView(_ uiView: View, context: Context) {
        uiView.updateUIView(self)
    }

    class View: UIView {
        var value: Int

        func makeUIView(_ myView: MyView) {
            // ここで初期化処理を書く
            self.value = myView.value
        }

        func updateUIView(_ myView: MyView) {
            // ここで値の更新処理を書く
            self.value = myView.value
        }
    }
}

このように書くと、プロパティが増えても Coordinator や自作のViewに渡すプロパティを増やす必要がなくなったり、プロパティが更新されたときの処理を書く場所が分かりやすくなったりするのでおすすめです。

sizeThatFits(_:) メソッドの使い方

また、広告などで UIViewRepresentable のサイズを固定したい場合もあるかもしれません。

iOS15までは以下のように、利用箇所で frame を使ってサイズを指定する必要がありました。

struct MyView: UIViewRepresentable {
    // 省略
}

struct ContentView: View {
    var body: some View {
        MyView()
            .frame(width: 100, height: 100)
    }
}

ですが、iOS16からは sizeThatFits(_:) メソッド5を実装することでサイズを指定することができます。

struct MyView: UIViewRepresentable {
    func makeUIView(context: Context) -> UIView {
        return UIView()
    }

    func updateUIView(_ uiView: UIView, context: Context) {
    }

    func sizeThatFits(_ proposal: CGSize, uiView: UIView, context: Context) -> CGSize {
        // 100x100 のサイズを指定
        return CGSize(width: 100, height: 100)
    }
}

proposal には、親Viewから渡される推奨サイズが入っています。このサイズを元に、 sizeThatFits(_:) メソッドでサイズを指定することができます。 nil を返すと、デフォルトのアルゴリズムにいい感じにサイズが調整されます。(実装しなかった場合と同じ)

sizeThatFits(_:) メソッドを使うことで frame を使わずにサイズを指定することができるので、 UIViewRepresentable を使うときには便利です。

まとめ

以上が UIViewRepresentable (と UIViewControllerRepresentable )の使い方についての説明でした。

SwiftUI で UIKit の View や ViewController を使うときには、 UIViewRepresentableUIViewControllerRepresentable を使うことで、簡単に組み合わせることができるのでどうしてもSwiftUIで出来ないことがあるときに有効活用するといいと思います。


はてなではiOSアプリエンジニアを募集しています。是非一緒に働きませんか?

hatena.co.jp


  1. UIViewRepresentable - SwiftUI | Apple Developer Documentation
  2. UIViewControllerRepresentable - SwiftUI | Apple Developer Documentation
  3. UIViewControllerRepresentable は、ほぼ同じように使えます。ただし、UIViewControllerRepresentableUIViewController を扱うため、makeUIViewController(context:) メソッドと updateUIViewController(_:context:) メソッドを実装します。他に関しては大きく変わりません。
  4. そんなことあるのと思われるかもしれませんが、値の変更を契機にViewに対してエフェクトをかけたり、2回目以降の描画でバリデーション結果を反映する場合など、初回描画時には呼び出されてほしくない処理も実行されてしまって困る場面はあるかと思います。(実際、自分は一度やらかしました。)
  5. sizeThatFits(_:uiViewController:context:) | Apple Developer Documentation)