[SwiftUI]Modifierを自作したい我
Modifierを自作して、一度に複数の設定ができるようにしたい。
以下を読む。
概要
ビューまたは別のビュー修飾子に適用する修飾子。元の値とは異なるバージョンを生成します。
任意のビューに適用できる再利用可能な修飾子を作成する場合は、
ViewModifier プロトコルを採用します。
struct BorderedCaption: ViewModifier {
func body(content: Content) -> some View {
content
.font(.caption2)
.padding(10)
.overlay(
RoundedRectangle(cornerRadius: 15)
.stroke(lineWidth: 1)
)
.foregroundColor(Color.blue)
}
}
modifier()をViewに直接適用することもできますが、
より一般的で環境的なアプローチでは、
modifier()を使用して、ビュー修飾子を組み込んだView自体の拡張機能を定義します。
extension View {
func borderedCaption() -> some View {
modifier(BorderedCaption())
}
}
Image(systemName: "bus")
.resizable()
.frame(width:50, height:50)
Text("Downtown Bus")
.borderedCaption()
bodyについて
呼び出し元の現在の本文を取得します。
必須: 提供先のデフォルト実装。
@ViewBuilder @MainActor @preconcurrency
func body(content: Self.Content) -> Self.Body
呼び出し元の現在に、bodyの中身を適用し、指定の型として返す。
Self.Bodyについて
ボディを表すビューの種類。
このbodyで返される型を定義する。
modifierメソッドについて以下を読む。
概要
ビューに修飾子を適用し、新しいビューを返します。
この修飾子を使用して View と ViewModifier を結合し、新しいビューを作成します。
nonisolated
func modifier<T>(_ modifier: T) -> ModifiedContent<Self, T>
T
はジェネリクスなので、どんな型も入ることが想定される。
modifier
メソッドを実行すると、ModifiedContent
という、合算後の値が返ってくる感じ。
modifiedContentについて
修飾子が適用された値。
@frozen
struct ModifiedContent<Content, Modifier>
Content=self
が入り、Modifier
は適用分の値
init(
content: Content,
modifier: Modifier
)
新しいビューまたはビュー修飾子を生成するために必要なコンテンツと修飾子を定義する構造。
コンテンツが View であり、修飾子が ViewModifier である場合、結果は Viewになります。コンテンツと修飾子が両方ともビュー修飾子である場合、結果はそれらを組み合わせた新しいViewModifier になります。
そういえば、modifierにmodifierを適用できるとかなんとか書いてあった気がする。
わかったこと
・ViewModifierプロトコルを継承すると、
呼び出し元のContentに、設定を適用した値を返すbodyメソッドを定義できる。
・このbodyプロパティがレンダリングの際に評価される。
・modifierメソッドは、modifiedContentを返す。
・modifiedContentは、呼び出し元とViewModifierを保持している。
・modifiedContentについては、
modifierを適用したViewのtypeをprintしてみると、構造が把握できる。
modifierメソッドの変換についてもう少し詳しく
View に修飾子を適用しても、View を直接変更するわけではないということです。実際に変更するプロパティはありません。代わりに、修飾子が適用されると、修飾子を適用した View をラップするModifiedContentが返されます。
let view = Rectangle().frame(width: 100, height: 100)
type(of: view) // ModifiedContent<Rectangle, _FrameLayout>
ModifiedContent は、実行時に適用されるコンテンツと修飾子を保持する、もう 1 つの非常にシンプルな構造体です。
struct ModifiedContent<Content, Modifier> {
var content: Content
var modifier: Modifier
}
ViewModifier作ろうと思ったが、拡張で良いじゃん。
// 💡 1. Use extension
extension View {
func blueBorder(width: CGFloat) -> some View {
self.border(.blue, width: width)
}
}
// 💡 2. Define original modifier
struct BlueBorderModifier: ViewModifier {
let width: CGFloat
func body(content: Content) -> some View {
content.border(.blue, width: width)
}
}
struct ContentView: View {
var body: some View {
VStack(spacing: 8) {
Text("Hello.")
.blueBorder(width: 2) // 💡 1. Use extension method
Text("Hello.")
.modifier(BlueBorderModifier(width: 2)) // 💡 2. Use original modifier
}
}
}
ViewModifierで作成するメリットとして、
@stateを使って、状態を保持するViewを持てることがあるとのこと。
struct SwitchBorderModifier: ViewModifier {
@State var isOn = false // ✅ Can hold state as like `View`.
func body(content: Content) -> some View {
VStack {
content.border(isOn ? .red : .blue) // 💡 Switch red and blue.
Toggle("Border", isOn: $isOn)
}
}
}
なるほど、確かにViewModifierをコードの記述量が多いし、
拡張でできることなら、そちらを使いたいかも。
拡張とViewModifierの比較はこれも。
State
ViewModifier は独自の状態を持つことができます。ViewModifier は準拠 View であるため、@State、@StateObject、@EnvironmentObject、および @ObservedObject を追加する同じ機能を持つことを意味します。つまり、ViewModifier のプロパティを変更し、そこから UI 更新をトリガーして、それ自体またはそのコンテンツを更新できます。
拡張だと、状態は持てないけど、外部から値を渡すことができるので、以下のようにも実装できる。
extension View {
func makeChangeColorToggle(isOn: Binding<Bool>) -> some View {
Toggle(isOn: isOn) {
self
.foregroundColor(isOn.wrappedValue ? .red : .blue)
}
.padding()
}
}
struct ContentView: View {
@State var isOn: Bool = false
var body: some View {
VStack {
Text("Test Title With Extension")
.makeChangeColorToggle(isOn: $isOn)
}
.padding()
}
}
階層
ViewModifier は、変更したコンテンツ (テキスト) の参照を保持しながら、型のカプセル化が優れています。一方、View 拡張機能はすべての変更を公開し、ModifiedContent の奥深くにコンテンツ タイプを保持します。
// 適用するModifier
struct PrimaryLabel: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(Color.black)
.foregroundColor(Color.white)
.font(.largeTitle)
.cornerRadius(10)
}
}
extension View {
func makePrimaryLabel() -> some View {
padding()
.background(Color.black)
.foregroundColor(Color.white)
.font(.largeTitle)
.cornerRadius(10)
}
}
// Modifierの適用を自作と拡張で行う
struct ContentView: View {
var textWithModifier: some View {
return Text("Test Title")
.modifier(PrimaryLabel())
}
var textWithExtension: some View {
return Text("Test Title")
.makePrimaryLabel()
}
var body: some View {
textWithModifier
textWithExtension
}
init() {
print(type(of: textWithModifier))
print(type(of: textWithExtension))
}
}
viewModifierの場合、printされるのは以下
ModifiedContent<Text, PrimaryLabel>
拡張の場合、printされるのは以下
ModifiedContent<
ModifiedContent<
ModifiedContent<
ModifiedContent<
ModifiedContent<
Text,
_PaddingLayout
>,
_BackgroundStyleModifier<Color>
>,
_EnvironmentKeyWritingModifier<Optional<Color>>
>,
_EnvironmentKeyWritingModifier<Optional<Font>>
>,
_ClipEffect<RoundedRectangle>
>