Sponsor Link
環境&対象
- macOS Big Sur 11.1
- Xcode 12.3
- iOS 14.2
以下では、UIViewRepresentable で説明していますが、NSViewRepresentable でも同様です。
UIViewRepresentable
SwiftUI に不足なビューがあると、UIViewRepresentable で wrap して、組み込むことができます。
UIViewRepresentable プロトコルでは、2つのメソッドが指定されています。
以下は、NSAttributedString を表示するために作った 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
}
- 最初にビューが構築される時に呼ばれます。UIKit の UILabel を使いたかったので、インスタンス化して返しています
- 表示すべき内容を更新する時に呼ばれます。表示に使用しているデータが更新された時等が呼ばれるタイミングです。当初は必要ないと思って、実装していませんでした。
気をつけなければいけない点
先ほど、updateUIView が、「表示に使用しているデータが更新された時に呼ばれる」と説明しましたが、他にも呼ばれるタイミングがあります。
上位ビューから再構築(body の再評価)されたときに、SwiftUI 側で判断によっては、すでにインスタンス化されている カスタム View を再利用しようとするタイミングでも updateNSView が呼び出されます。
つまり、新しいデータと共にビューが instance 化されると思っていると、SwiftUI 的には「すでに View があるんだから、データを更新すれば再利用できるハズ。なので、新しい instance を作らずに、既存の instance を update して使おう」と考えるということです。
アプリの動作としては、「データが切り替わっているはずなのに表示が切り替わらない」という動作になり、ハマります。
親ビュー含め丸ごと更新されるから、カスタム View も再構築されると想像してしまいがちですが、上記のようなケースもあるため updateNSView も実装しておいた方が良いです。
makeUIView でなく、updateUIView が呼ばれる例
# 短いコードで表現したかったので、少し無理やりです。
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 を使う時に気をつけるべき点
- updateNSVIew/updateUIView は、予期しないタイミングで呼ばれるケースがあるので、必ず実装する
- updateNSVIew/updateUIView が呼ばれない前提であれば、チェックする仕組みを入れておく
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
Sponsor Link