SwiftUIで特定のViewからしか呼べないViewのextensionを作る
きっかけ
SwiftUIのリストの並び替えや削除処理を書く際、onMoveやonDeleteはForEachにしか書くことが出来ないことを知り、どういう仕組みか気になったので調べてみました。
DynamicViewContent
定義を見てみるとonMove
はDynamicViewContentの拡張で、DynamicViewContentを返す関数となっています。
onMove
DynamicViewContent
はViewに準拠したprotocolです。プロパティとしてdataを持っているのは並べ替えや削除の判定を行うためだと思われます。
DynamicViewContent
そしてDynamicViewContentに準拠しているのはForEach
とModifiedContent
です。上で見たようにonMoveはDynamicViewContentの拡張なので、この書き方によってonMoveが利用できる箇所が制限されているんですね。ちなみにModifiedContentはViewModifierを利用した際の戻り値です。
ForEachのextension
ModifiedContentのextension
実際にextensionを作ってみる
簡単な例を用いて実際にextensionを作ってみます。
まずはシンプルなViewとprotocolを利用した拡張部分を用意します。
struct HelloWorldView: View {
var body: some View {
Text("Hello, World!")
}
}
protocol CustomProtocol: View {} // DynamicViewContentに該当
extension HelloWorldView: CustomProtocol {} // ForEachに該当
extension ModifiedContent: CustomProtocol where Content: CustomProtocol, Modifier: ViewModifier {}
これに対してonMove部分を作っていきます。
今回はパディングをつけて青いボーダーをつけるだけのViewModifierを作りました。
struct BlueBorderModifier: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.border(.blue)
}
}
そしてCustomProtocolを拡張します。
これで完成です。
extension CustomProtocol {
func addBlueBorder() -> some CustomProtocol {
self.modifier(BlueBorderModifier())
}
}
作ったextensionを利用する
onMoveやonDeleteと同じように特定のViewに対してのみ利用できるextensionが作成できました。
ModifiedContentの拡張があるおかげで、複数連ねて利用することもできます。
var body: some View {
VStack {
HelloWorldView()
.addBlueBorder() // OK: HelloWorldViewがCustomProtocolに準拠しているため
.addBlueBorder() // OK: ContentがCustomProtocolのModifiedContentがCustomProtocolに準拠しているため
Text("Hello, World!")
.addBlueBorder() // NG: Value of type 'Text' has no member 'addBlueBorder'
}
.addBlueBorder() // NG: Value of type 'VStack<Content>' has no member 'addBlueBorder'
}
まとめ
今までonMoveやonDeleteを書ける条件を知らずに使っていたため、自分で制約をつけられるのは新たな気づきでした。実際にはViewModifier直呼びができてしまいますが、SDKで提供するなどすれば適切な箇所でしか利用できない仕組みを提供できそうです。
自分なりの解釈ですので、なにか間違っている点がありましたらコメントいただけますと幸いです。
Discussion