[SwiftUI] UIViewRepresentable を使う時に気をつける点 ( NSViewRepresentable も同様)

SwiftUI

UIKit や AppKit の View を SwiftUI と一緒に使う時に UIViewRepresentable や NSViewRepresentable を使いますが、その時に気をつけることを説明します。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Big Sur 11.1
  • Xcode 12.3
  • iOS 14.2

以下では、UIViewRepresentable で説明していますが、NSViewRepresentable でも同様です。

UIViewRepresentable

SwiftUI に不足なビューがあると、UIViewRepresentable で wrap して、組み込むことができます。

UIViewRepresentable プロトコルでは、2つのメソッドが指定されています。

以下は、NSAttributedString を表示するために作った AttributedText のコードです。

AttributedText

struct AttributedText: UIViewRepresentable {
    let attributedString: NSAttributedString

    init(_ attributedString: NSAttributedString) {
        self.attributedString = attributedString
    }

    func makeUIView(context: Context) -> UILabel {
        // (1)
        let uiLabel = UILabel()
        uiLabel.attributedText = attributedString
        return uiLabel
    }
    
    func updateUIView(_ uiView: UILabel, context: Context) {
        // (2)
        // uiView.attributedText = attributedString
        print("not implemented")
    }
    typealias UIViewType = UILabel
}
コード解説
  1. 最初にビューが構築される時に呼ばれます。UIKit の UILabel を使いたかったので、インスタンス化して返しています
  2. 表示すべき内容を更新する時に呼ばれます。表示に使用しているデータが更新された時等が呼ばれるタイミングです。当初は必要ないと思って、実装していませんでした。

気をつけなければいけない点

先ほど、updateUIView が、「表示に使用しているデータが更新された時に呼ばれる」と説明しましたが、他にも呼ばれるタイミングがあります。

上位ビューから再構築(body の再評価)されたときに、SwiftUI 側で判断によっては、すでにインスタンス化されている カスタム View を再利用しようとするタイミングでも updateNSView が呼び出されます。

つまり、新しいデータと共にビューが instance 化されると思っていると、SwiftUI 的には「すでに View があるんだから、データを更新すれば再利用できるハズ。なので、新しい instance を作らずに、既存の instance を update して使おう」と考えるということです。

アプリの動作としては、「データが切り替わっているはずなのに表示が切り替わらない」という動作になり、ハマります。

親ビュー含め丸ごと更新されるから、カスタム View も再構築されると想像してしまいがちですが、上記のようなケースもあるため updateNSView も実装しておいた方が良いです。

makeUIView でなく、updateUIView が呼ばれる例

# 短いコードで表現したかったので、少し無理やりです。

App

struct Item {
    let text: String
    var attributedText: NSAttributedString {
        let attributes:[NSAttributedString.Key:Any] = [
            .font: UIFont.systemFont(ofSize: 36),
            .strokeColor: UIColor.black,
            .strokeWidth: 3
        ]
        let attrText = NSMutableAttributedString(string: text, attributes: attributes)
        return attrText
    }

    init(_ text: String) {
        self.text = text
    }
}
struct ContentView: View {
    let itemList = [Item("item0"), Item("item1"), Item("item2"), Item("item3")]
    @State private var showIndex: Int = 0
    var body: some View {
        VStack {
            Spacer()
            AttributedText(itemList[showIndex].attributedText)
                .padding(20)
            Spacer()
            HStack {
                Button(action: { showIndex = showIndex > 0 ? showIndex - 1 : 0 },
                       label: { Image(systemName: "minus.square").resizable().scaledToFit() } )
                Button(action: { showIndex = showIndex > 2 ? 3 : showIndex + 1 },
                       label: { Image(systemName: "plus.square").resizable().scaledToFit() } )
            }
            .frame(height:50)
            Spacer()
        }
    }
}
}
アプリの動作としては、データの配列がアプリ内に保持されていて、+ボタン/ーボタンを押すことで、その配列の要素を順番に確認できるという動作です。

期待動作

updateUIView を未実装にすると、以下のような動作に変わります。

まとめ:ViewRepresentable を使う時に気をつけるべき点

ViewRepresentable を使う時に気をつけるべき点
  • updateNSVIew/updateUIView は、予期しないタイミングで呼ばれるケースがあるので、必ず実装する
  • updateNSVIew/updateUIView が呼ばれない前提であれば、チェックする仕組みを入れておく

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です