Sponsor Link
環境&対象
- 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] Environment 変数を作る方法
# 逆方向ですが、子ビューから親ビューへの情報伝達の1つには Preference があります。
# Preference については、以下の記事で説明しています。
[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 で表示されています。
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 経由で設定できる方法を説明してきました。
- Environment 経由で値を設定する
- ViewModifier 的に指定すると、SwiftUI コンポーネントと同様に見える
- ViewModifier の層で、デフォルト値指定や、値の調整等が可能
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
Sponsor Link