【SwiftUI】 今日のなんでやねん - @ViewBuilder


@ViewBuilder

よく見かけますが、なんのために使うのか。

 

■ 公式ドキュメント - ViewBuilder

A custom parameter attribute that constructs views from closures.

クロージャから View を構築するカスタムパラメータ属性。

You typically use ViewBuilder as a parameter attribute for child view-producing closure parameters, allowing those closures to provide multiple child views.

通常、ViewBuilderを子 View 生成クロージャパラメータのパラメータ属性として使用し、それらのクロージャが複数の子 View を提供できるようにします。


func contextMenu<MenuItems: View>(
  @ViewBuilder menuItems: () -> MenuItems
) -> some View

myView.contextMenu {
  Text("Cut")
  Text("Copy")
  Text("Paste")
  if isSymbol {
    Text("Jump to Definition")
  }
}

👉 ViewBuilder | Apple Developer Documentation hatena-bookmark

並列のまま、持ち運びできて、

記述が一層不要になるってことか。

 

■ if も使えるようになる


// NG
var body: some View {
    if imageName.isEmpty {
        return Text("no image")
    } else {
        return Text(imageName)
    }
}

@ViewBuilder
var body: some View {
    if imageName.isEmpty {
        Text("no image")
    } else {
        Image(imageName)
    }
}

var body: some View {
    Group {
        if imageName.isEmpty {
            Text("no image")
        } else {
            Image(imageName)
        }
    }
}

👉 SwiftUIのViewで条件によってViewを出し分ける方法 - The Pragmatic Ball boy hatena-bookmark

そういえば、

公式ドキュメントのサンプルコードにも、

しれっと if が入っていた。

なるほど、

Group / VStack / HStack がなければ使えなかった if を使えるようにしてくれるのか。

便利そう !

 

■ やってみた

こういうのがあったとして、


VStack {
  Text("top")
  Text("bottom")
}
.font(.title)
.foregroundColor(.red)

こう書ける。


TitleTextFormatView1 {
  Text("top")
  Text("bottom")
}

struct TitleTextFormatView1<Content: View>: View {
  @ViewBuilder var content: Content

  var body: some View {
    content
      .font(.title)
      .foregroundColor(.red)
  }
}

続いて、@ViewBuilder を消してみます。


TitleTextFormatView2 {
  Text("top")
  Text("bottom")
}

struct TitleTextFormatView2<Content: View>: View {

  // Type '() -> ()' cannot conform to 'View'

  var content: Content // *

  var body: some View {
    content
      .font(.title)
      .foregroundColor(.red)
  }
}

しかし、これは NG ですね、分かります。

勉強しましたから !

続きましては、

渡す View を VStack を使って、1つにまとめます。

他は変更ありません。

結果は、OK なはずです。


TitleTextFormatView3 {
  VStack { // *
    Text("top")
    Text("bottom")
  }
}

struct TitleTextFormatView3<Content: View>: View {
  var content: Content

  var body: some View {
    content
      .font(.title)
      .foregroundColor(.red)
  }
}

...

なんでや !?

 

■ まとめ

私はこれまで、

まず VStack や HStack を常に書いていたので

@ViewBuilder の必要性を感じなかった

ことが分かりました。

しかし、今回の結果は残念です。

謎です。

以下の基本的なキーワード、


「TupleView」
「Content」
「some View」
「any View」
「View | Stack | Layout」

分かりづらくないですか。

直感的にふんわりしてません ?

長い説明が必要ですよね ?

あなたの感想ですよね ?


【SwiftUI】カスタム ViewModifier の使い方と使いどころ 🤔

あまり使ってないので上手に使いたい。

なんとなく使ってる感じなのではっきりさせておきたい。

 

🤔 ViewModifier - 公式ドキュメント

公式ドキュメントをきちんと読んでみます。

To create consistent views, you might reuse the same view modifier or group of modifiers repeatedly across your views.

一貫したビューを作成するために、同じ ViewModifier、または、それのグループを View で繰り返し再利用することができます。

A modifier that you apply to a view or another view modifier, producing a different version of the original value.

View または別の ViewModifier に適用する Modifier で、元の異なるバージョンを生成します。


struct CaptionTextFormat: ViewModifier {
  func body(content: Content) -> some View {
    content
      .font(.caption)
      .foregroundColor(.secondary)
  }
}

Text("Some additional information...")
  .modifier(CaptionTextFormat())

To make your custom view modifier conveniently accessible, extend the View protocol with a function that applies your modifier

カスタム ViewModifier を便利にアクセスできるようにするには、カスタム View Modefier を適用する関数を使用して View プロトコルを拡張します。


extension View {
  func captionTextFormat() -> some View {
    modifier(CaptionTextFormat())
  }
}

Text("Some additional information...")
  .captionTextFormat()

👉 ViewModifier | Apple Developer Documentation hatena-bookmark
👉 Reducing view modifier maintenance | Apple Developer Documentation hatena-bookmark

「一貫性のため」か。

確かに新規作成も変更も面倒ですよね。

 

🤔 Custom ViewModifier vs View extension

@State が必要かどうか。

ViewModifier let you have @State variables, but View extensions do not.

ViewModifier では @State が持てるが、View extension では持てない。

👉 ios - Difference between creating ViewModifier and View extension in SwiftUI - Stack Overflow hatena-bookmark

View を拡張したい場合は原則として extension を使用し、状態保持が必要な場合のみ `ViewModifier` を実装する。

👉YusukeHosonuma/Effective-SwiftUI · Discussion #31 hatena-bookmark

また、

以下、WWDC 2022 SwiftUI Q&A での話のようです。

What’s the difference between a custom ViewModifier vs View extension

Q: What’s the difference between a custom ViewModifier (without DynamicProperty) that uses some built-in modifiers in body(content:), and a custom View extension func that just use those built-in modifiers?

Similarly, what’s the difference between a custom ViewModifier with some DynamicProperty and a custom View with some DynamicProperty (also has a @ViewBuilder content property to receive content to modify) ?

I think two have the same render result and behavior.

A: Because of the way a ViewModifier is expressed, the engine knows it’s not changing the content passed in and can apply performance optimizations (compared to just an extension on View)

カスタム ViewModifier と View extension の違いは何ですか ?

質問:
body(content:) でいくつかの組み込み modifier を使用するカスタム ViewModifier (DynamicPropertyなし)と、それらの組み込み Modifier を使用するカスタム View extension の違いは何ですか?

同様に、いくつかの DynamicProperty を持つカスタム ViewModifier と、いくつかの DynamicProperty を持つカスタム View の違いは何ですか ? (modify する content を受け取るための @ViewBuilder content property もあります)

2つは同じレンダリング結果と動作を持っていると思います。

回答:
ViewModifier の表現方法により、エンジンは渡されたコンテンツを変更していないことを知っており、パフォーマンスの最適化を適用できます。Viewの単なる拡張と比較すると。

👉 WWDC22 SwiftUI Q&A | Swift Discovery hatena-bookmark
👉 https://wwdc22.slack.com/ - Apple Events ​に⁠サ⁠イ⁠ン⁠イ⁠ン⁠す⁠る​ | Slack 

「一度 ViewModifier に切り出すとパフォーマンスが最適化される。」

ということのようです。

一発で View extension にしたほうが間違いなく見通しは良いので、

適正化でどれくらいパフォーマンスが変わるか知りたいところです。

ここで、上の質問で挙げられている4つのパターンのコードを想像してみます。


// body(content:) でいくつかの組み込み Modifier を使用する
// カスタムViewModifier (DynamicPropertyなし)

struct CustomViewModifier: ViewModifier {
  var body(content: Content) -> some View {
    // use built-in modifier
  }
}


// それらの組み込み Modifier を使用する
// カスタム View extension

extension View {
  func customModifier() -> some View {
    modifier(CustomViewModifier())
  }
}


// いくつかの DynamicProperty を持つ
// カスタム ViewModifier

struct CustomViewModifier: ViewModifier {
   @State var state: State = .loading

   var body(content: Content) -> some View {
    // use built-in modifier
  }
}


// いくつかの DynamicProperty を持つカスタム View
// modify する content を受け取るための
// @ViewBuilder content property

struct CustomView<Content: View>: View {
  @State var state: State = .loading
  @ViewBuilder var content: Content

  var body: some View {
    // use built-in modifier
  }
}

この質問者の人、さらっと聞いてるように見えますが、

結構ナイスな質問です。

初心者的には、カスタム View が自然に見えますが。

考えさせられます。

 

🤔 公式サンプルコード

続いて、WWDC2023 のサンプルコードを見てみます。


private struct BackyardViewportContentModifier: ViewModifier {
  var value: BackyardViewportContent
    
  func body(content: Content) -> some View {
    content.layoutValue(key: BackyardViewportContentKey.self, value: value)
  }
}

fileprivate extension View {
  func backyardViewportContent(_ value: BackyardViewportContent) -> some View {
    modifier(BackyardViewportContentModifier(value: value))
  }
}


struct GenerateDataViewModifier: ViewModifier {
  @Environment(\.modelContext) private var modelContext
    
  func body(content: Content) -> some View {
    content.onAppear {
      DataGeneration.generateAllData(modelContext: modelContext)
    }
  }
}

fileprivate extension View {
  func generateData() -> some View {
    modifier(GenerateDataViewModifier())
  }
}

👉 apple/sample-backyard-birds hatena-bookmark

ここでは、カスタム ViewModifier の利用のほとんどが、

「データコンテナ」と「通信API」処理まわりです。

これは #Preview でも使いやすいし、定型のパターン。

👉 【 #SwiftData 】ModelContainer の View へのセット hatena-bookmark

 

🤔 まとめ

やっぱ、View で切り出したくなるのですが。

[参考]
👉 ViewBuilderやViewModifierでSwiftUIのViewを分割する|TAAT hatena-bookmark


【SwiftUI】Environment の考え方

まず、作ってみます。

定型です。


private struct MyEnvironmentKey: EnvironmentKey {
  static let defaultValue: String = "Default"
}

extension EnvironmentValues {
  var myCustomValue: String {
    get { self[MyEnvironmentKey.self] }
    set { self[MyEnvironmentKey.self] = newValue }
  }
}

extension View {
  func myCustomValue(_ myCustomValue: String) -> some View {
    environment(\.myCustomValue, myCustomValue)
  }
}

デフォルトに "Default" という文字列を設定していますが、

SwiftUI の View であればどこでもプロパティラッパーを経由して利用できます。

入れ子になった View で使ってみます。


struct Root: View {
  var body: some View {
    First()
    Second()
  }
}

struct First: View {
  @Environment(\.myCustomValue) var myCustomValue

  var body: some View {
    Text("First " + myCustomValue)
  }
}

struct Second: View {
  @Environment(\.myCustomValue) var myCustomValue

  var body: some View {
    Text("Secound " + myCustomValue)
    Third()
  }
}

struct Third: View {
  @Environment(\.myCustomValue) var myCustomValue

  var body: some View {
    Text("Third " + myCustomValue)
  }
}

「Scound」とか www

続いて、デフォルトの値を上書きして変更します。

Environment の値の変更時には、View のツリー構造を思い浮かべながら影響範囲を考えます。

変更箇所は一行追加のみです。

Enviroment 定義時に extension も作成していますので、それを使っても等価です。


struct Root: View {
  var body: some View {
    First()
    Second()
      .environment(\.myCustomValue, "Another")  // *
      //.myCustomValue("Another") // * extension
  }
}

struct First: View {
  @Environment(\.myCustomValue) var myCustomValue

  var body: some View {
    Text("First " + myCustomValue)
  }
}

struct Second: View {
  @Environment(\.myCustomValue) var myCustomValue

  var body: some View {
    Text("Secound " + myCustomValue)
    Third()
  }
}

struct Third: View {
  @Environment(\.myCustomValue) var myCustomValue

  var body: some View {
    Text("Third " + myCustomValue)
  }
}

Second() に付けた .environment() で更新しています。

影響範囲は「その下位の View すべて」となります。

SwiftUI には、あらかじめ用意された便利な Environment がたくさんあります。

その中から、さらに、加えて font を使ってみます。


var font: Font?
The default font of this environment.

👉 EnvironmentValues | Apple Developer Documentation hatena-bookmark

定義 (extension を含む) の記述は不要なので

ツリー構造だけ気にしながら、

好きなところですばやく上書きしていくことができます。


struct Root: View {

  var body: some View {
    First()
      .environment(\.font, .caption) // *
      //.font(.caption) // * extension
    Second()
      .environment(\.myCustomValue, "Another")
      //.myCustomValue("Another")
      .font(.largeTitle) // * extension
  }
}

struct First: View {
  @Environment(\.myCustomValue) var myCustomValue
  var body: some View {
    Text("First " + myCustomValue)
  }
}

struct Second: View {
  @Environment(\.myCustomValue) var myCustomValue
  var body: some View {
    Text("Secound " + myCustomValue)
    Third()
  }
}

struct Third: View {
  @Environment(\.myCustomValue) var myCustomValue
  var body: some View {
    Text("Third " + myCustomValue)
      .font(.caption) // * extension
  }
}

プロパティラッパー @Environment の利用は不要で、

下位 View に適用されていきます。

さらに、それを上書きもできます。

しかし、「Scound」とか恥ずいわ。

 

🌝 まとめ

ある程度調べてからやってみたのですが、少しイメージと違いました。

- デフォルト値はプロパティとしてどの View でも取得できる。

- 値の変更時に範囲を考慮しながら上書きする。

- プロパティで取得しなくてもそのまま適用されるものもある。

ちなみに、GitHub で調べてみると一番使われてる Built-in EnvironmentValue は、


@Environment(\.dismiss) var dismiss

でした。タイプは DismissAction です。

👉 Environment | Apple Developer Documentation hatena-bookmark
👉 EnvironmentValues | Apple Developer Documentation hatena-bookmark
👉 EnvironmentKey | Apple Developer Documentation hatena-bookmark