[SwiftUI] ViewModifier を使って カスタムView を設定する方法

SwiftUI2021

     
⌛️ < 1 min.
カスタムビューを作った時の設定値を ViewModifier 経由で設定する方法を説明します。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Monterey 12.3
  • Xcode 13.3
  • iOS 15.4

CustomViewのカスタム

SwiftUI を使っていると、細かい調整をおこなった View を切り出して 独自 View にしたくなります。

このようにして作成した CustomView は、汎用度が低い時には 1つのアプリの中で使い回すだけかもしれませんが、ケースによっては、表示設定の一部を外部指定、例えば、BorderColor を外部から引数指定できるようにしてより多くのケースで使用できるようにしたくなるかもしれません。

再利用という観点からは良いことなのですが、この時に 気をつけないといけない点があります。

この方向性でいでいくと、initializer での引数指定がすごいことになります。
カスタムしたい箇所はどんどん増えていくと思いますので、initializer の引数がどんどん増えていくことを意味します。
デフォルト値を設定することで、実際に指定するものを減らすことはできるかもしれませんが、煩雑であることは変わりません。

ViewModifier の活用

SwiftUI にすでに存在する要素を見てみると、うまく作られていることがわかります。

例えば、Text です。

Text は文字列を表示するだけの要素ですが、フォントのサイズや、Bold 等の装飾、複数行にするかどうか 等 指定したいことはたくさん出てきます。Text のすごい点は、これらの指定を initializer の引数で受けないところです。

initializer で処理しようとすると 以下のようになっていたでしょう。


Text("Hello, world!", bold: true, fontSize: .largeTitle, lineLimit: 1, .....) 

ご存知のように、上記のような状況にはなっていません。代わりに Text は、ViewModifier でこれらの指定ができるようになっています。


Text("Hello, world!")
     .bold()
     .font(.largeTitle)
     .lineLimit(1)

ViewModifier 指定で行うことで、initializer がすっきりします。
同様のことを自分が作成したカスタム View でもやりたくなります。 ということで、作ってみました。

View と ViewModifier のやりとり

ViewModifier を使って設定するとして、View はどうやって ViewModifier で指定された値を取得できるようになるのでしょうか?

直感とは反するかもしれませんが、ViewModifier は、View の親に当たります。

例えば、以下のコードです。


Text("Hello, world!")
   .bold()

レイアウト時に行われる処理では、以下のようなシーケンスになっています。(WindowGroup 直下に記述していると想定してます)

sequenceDiagram
autonumber
WindowGroup->>.bold: おすすめサイズは(414.0 x 814.0)だけど, 必要なサイズは?
.bold->>Text: おすすめサイズは、(414.0 x 814.0) だけど、必要なサイズは?
Text->>.bold: (94.5 x 20.5) あれば、良いです
.bold->>WindowGroup: (94.5 x 20.5) あれば、良いです
WindowGroup-->>WindowGroup: .frame を配置しよう
.bold-->>.bold: Text を配置しよう

.bold は、Text の配置には無関係ですが、ビューの親子関係としては、WindowGroup – .bold – Text という関係になっています。

ですので、親ビューから子ビューへの情報を渡す方法が使えるはずです。

親View から 子View に情報を渡す

つまり、親ビューから子ビューに情報を渡す方法がわかれば良いことになります。

渡す方法の1つは、子ビュー の initializer での引数指定ですが、いまはそれではない方法を探したいです。

親ビューから子ビューに情報を渡す方法の1つとして Environment がありますので、それを利用します。

Environment の使い方は以下の記事で説明しています。
SwiftUI [SwiftUI] Environment 変数を作る方法

# 逆方向ですが、子ビューから親ビューへの情報伝達の1つには Preference があります。
# Preference については、以下の記事で説明しています。
SwiftUI2021 [SwiftUI] Scalable な要素を Text に合わせた高さで表示する

CustomView を作る

実際に以下のような CustomView を作ってすすめていきます。

カスタムビュー概要
・渡された 2つの String を Text 2行として表示する
・1行目、2行目の font をそれぞれ指定できる

具体的には、以下のように使えるようにしたいこととします。

使用例(想定)

TwoLineText("Hello, world!", "こんにちわ 世界")
    .fontConfig(.largeTitle, .body)

# この例のケースは練習問題ですので、2つの Text を並べて書くことと大きな差異はありません

Environment を使う準備

Environment を使って設定情報を渡すので、EnvironmentKey と EnvironmentValues にそれぞれを定義します。


struct TwoLineFontKey: EnvironmentKey {
    typealias Value = (Font,Font)
    static var defaultValue: (Font,Font) = (.body, .body)
}

extension EnvironmentValues {
    var twoLineFont: (font1: Font,font2: Font) {
        get {
            return self[TwoLineFontKey.self]
        }
        set {
            self[TwoLineFontKey.self] = newValue
        }
    }
}

2行表示するビュー

ベースとなるビューを作ります。


struct TwoLineText: View {
    @Environment(\.twoLineFont) var fontConfig
    let text1: String
    let text2: String
    init(_ text1: String, _ text2: String) {
        self.text1 = text1
        self.text2 = text2
    }
    var body: some View {
        VStack {
            Text(text1)
                .font(fontConfig.font1)
            Text(text2)
                .font(fontConfig.font2)
        }
    }
}

Environment から情報を取得して、font を設定するようにしています。

CustomView を使う

Environment で設定する

Environment 経由で設定を参照していますので、ここまでのコードでも以下のように使用できます。


struct ContentView: View {
    var body: some View {
        TwoLineText("Hello, world!", "こんにちわ 世界")
            .environment(\.twoLineFont, (.largeTitle, .body))
            .padding()
    }
}

以下のように、最初のテキストと2つ目のテキストがそれぞれ .largeTitle と .body で表示されています。

AppImage

ViewModifier で設定する

ViewModifier で使うのがゴールでしたので、View の extension で以下のように定義します。


extension TwoLineText {
    func fontConfig(_ font1: Font? = nil,_ font2: Font? = nil) -> some View {
        self
            .environment(\.twoLineFont, (font1 ?? .body, font2 ?? .body))
    }
}

ViewModifier の段階でフォントに nil 指定することで、デフォルト値を使用するようにしています。

元々デフォルトで (.body, .body) を持っていますので、何も指定しないと .body が使用されますが、Environment を設定する時には、両方を設定しなければいけませんでした。一方だけでも指定することができるように、ViewModifier の段階で省略時の対応をしています。

省略だけでなく、一方の値に応じて他方を調整する等もこの段階で可能です。

ここまでくると、以下のように ViewModifier を使ったコードになります。


struct ContentView: View {
    var body: some View {
        TwoLineText("Hello, world!", "こんにちわ 世界")
            .fontConfig(.largeTitle, nil) // nil 指定でデフォルト値使用が可能に
            .padding()
    }
}

厳密な ViewModifier?

ここまでのコードでは 厳密には protocol ViewModifier に準拠した ViewModifier を作成してはいません。View.fontConfig という形で設定できるようにしているだけです。
ViewModifier に準拠した struct を作成して 厳密な意味での ViewModifier にするのであれば、以下のようなコードになります。ただ、外部から Environment を設定するだけであれば ViewModifier に準拠しなくとも可能です。


struct TwoLineFontConfig: ViewModifier {
    let font1: Font
    let font2: Font
    init(_ font1: Font, _ font2: Font) {
        self.font1 = font1
        self.font2 = font2
    }
    func body(content: Content) -> some View {
        content
            .environment(\.twoLineFont, (font1, font2))
    }
}

まとめ

View の設定値を Environment 経由で設定できる方法を説明してきました。

ビュー設定値を ViewModifier 経由で設定する方法
  • Environment 経由で値を設定する
  • ViewModifier 的に指定すると、SwiftUI コンポーネントと同様に見える
  • ViewModifier の層で、デフォルト値指定や、値の調整等が可能

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です