Closed6

[SwiftUI]Modifierを自作したい我

ほへとほへと

以下を読む。

https://developer.apple.com/documentation/swiftui/viewmodifier


概要

ビューまたは別のビュー修飾子に適用する修飾子。元の値とは異なるバージョンを生成します。
任意のビューに適用できる再利用可能な修飾子を作成する場合は、
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の中身を適用し、指定の型として返す。
https://developer.apple.com/documentation/swiftui/viewmodifier/body(content:)-36wiq

Self.Bodyについて

ボディを表すビューの種類。

このbodyで返される型を定義する。
https://developer.apple.com/documentation/swiftui/viewmodifier/body

ほへとほへと

modifierメソッドについて以下を読む。

https://developer.apple.com/documentation/swiftui/view/modifier(_:)


概要

ビューに修飾子を適用し、新しいビューを返します
この修飾子を使用して View と ViewModifier を結合し、新しいビューを作成します。

nonisolated
func modifier<T>(_ modifier: T) -> ModifiedContent<Self, T>

Tはジェネリクスなので、どんな型も入ることが想定される。
modifierメソッドを実行すると、ModifiedContentという、合算後の値が返ってくる感じ。

modifiedContentについて

修飾子が適用された値。

@frozen
struct ModifiedContent<Content, Modifier>

Content=selfが入り、Modifierは適用分の値

https://developer.apple.com/documentation/swiftui/modifiedcontent

init(
    content: Content,
    modifier: Modifier
)

新しいビューまたはビュー修飾子を生成するために必要なコンテンツと修飾子を定義する構造。

コンテンツが View であり、修飾子が ViewModifier である場合、結果は Viewになります。コンテンツと修飾子が両方ともビュー修飾子である場合、結果はそれらを組み合わせた新しいViewModifier になります

そういえば、modifierにmodifierを適用できるとかなんとか書いてあった気がする。

https://developer.apple.com/documentation/swiftui/modifiedcontent/init(content: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
}

https://dev.to/mtsrodrigues/understanding-swiftui-modifiers-3e50

ほへとほへと

ViewModifier作ろうと思ったが、拡張で良いじゃん。

https://github.com/YusukeHosonuma/Effective-SwiftUI/discussions/31


// 💡 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の比較はこれも。

https://medium.com/@johanesriandy/swiftui-custom-viewmodifier-vs-view-extension-f0cea3b7a5fa


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>
>
このスクラップは5ヶ月前にクローズされました