[SwiftUI] if系extensionのmodifierは多用しない方がいい
TL;DR
- if系extensionのmodifierは暗黙的にレシーバを複製し、パフォーマンス低下・状態の消失を引き起こす
- if系extensionのmodifierは
if {}
と(ほぼ)同じ挙動をする- それぞれの分岐でStructural Identity (View Identity)が変わる
- if系extensionのmodifierは便利だが、上記デメリットを見えにくくし、不必要な利用を助長する
- 使わない努力をした後、必要に迫られデメリットを理解した上で使うなら良いと思う
if系extensionのmodifierとは
以下のようなmodifierです。
extension View {
@ViewBuilder
func `if`<Content: View>(
_ condition: Bool,
@ViewBuilder transform: (Self) -> Content
) -> some View {
if condition {
transform(self)
} else {
self
}
}
@ViewBuilder
func `if`<TrueContent: View, FalseContent: View>(
_ condition: Bool,
@ViewBuilder _ then: (Self) -> TrueContent,
@ViewBuilder `else`: (Self) -> FalseContent
) -> some View {
if condition {
then(self)
} else {
`else`(self)
}
}
}
例えば、「ある条件に応じてmodifierを付け替えたい」と言う場合
このmodifierを用いると簡潔に記述できるという利点があります。
struct SampleView: View {
let condition: Bool
var body: some View {
Text("text")
.if(condition) {
// trueの時だけ文字色を赤に
$0.foregroundColor(.red)
}
}
}
なお、以下のようなextensionの利用時にif {}
を使った場合も今回の議論に該当します。
func extend<Content: View>(@ViewBuilder transform: (Self) -> Content) -> some View {
transform(self)
}
Text("text")
.extend {
if condition {
$0.foregroundColor(.red)
} else {
$0
}
}
なぜ「多用しない方がいい」?
このコードを見てどう思うでしょうか?
一見特に問題のないコードに見えます。
struct CounterView: View {
@State var count = 0
var body: some View {
Button {
count += 1
} label: {
Text("count: \(count)")
}
}
}
struct SampleView: View {
@State var condition = false
var body: some View {
VStack {
CounterView()
Button {
condition.toggle()
} label: {
Text("toggle background")
}
}
.if(condition) {
$0.background(Color.red)
}
}
}
ですが、上のコードは次のコードと意味・挙動的には(ほぼ)一緒です。
struct SampleView: View {
@State var condition = false
var body: some View {
if condition {
VStack {
CounterView()
Button {
condition.toggle()
} label: {
Text("toggle background")
}
}
.background(Color.red)
} else {
VStack {
CounterView()
Button {
condition.toggle()
} label: {
Text("toggle background")
}
}
}
}
}
「なんで@State
を持っている CounterView
を2回宣言してるんだ...」という感想だと思います。
これだとcondition
が変わる度に別のCounterView
の@State
が表示され、さらにcondition
が変わる度に状態が消失してしまいます。
実行例
.if
modifierの定義を改めて見るとself
が if { self } else { self }
のように囲われているので、こういう結果になるのも理解できると思います。
extension View {
@ViewBuilder
func `if`<Content: View>(
_ condition: Bool,
@ViewBuilder transform: (Self) -> Content
) -> some View {
if condition {
transform(self)
} else {
self
}
}
}
以上から .if
modifierは暗黙的にレシーバを複製し、パフォーマンス低下・状態の消失を引き起こすため多用すべきではない と思います。
代わりにどうすればいい?
.if
modifierを使いたいケースは「条件によって要素のmodifier切り替えたい」というケースだと思います。要素のmodifierの切り替えにおいては、modifierの引数に分岐を移動させることで(大抵のケースは)対処できます。この場合は軽いコストかつ単一のView identityで表現できます。
struct SampleView: View {
let condition: Bool
var body: some View {
Text("text")
.foregroundColor(condition ? .red : .primary)
.overlay {
if condition {
Text("overlayed!")
} else {
EmptyView()
}
}
}
}
そもそも、大抵のケースでは、if {}
は「違う要素の出し分け」をするために用いるもので、「要素のmodifierを切り替える」ために用いる物ではないと思います。
要素のmodifierを切り替えるために if {}
, .if
modifier を使うのは大抵のケースで悪い選択です。
if {}
を使わなくてはいけない時
どうしてもmodifierの引数にisActive: Bool
がない場合等、「どうしてもmodifierの付け替えにif {}
を使わなくては行けない場合」もあるのかなと思っています。
struct SampleView: View {
let condition: Bool
var body: some View {
VStack {
Text("hoge")
if condition {
Text("text")
.hidden() // isActiveがない
} else {
Text("text")
}
Text("fuga")
}
}
}
このケースになって初めて .if
modifierを使う選択をしてもいいかなと思います。
これなら上のコードも次のコードも同じ意味なので、このケースでは純粋に「便利なmodifier」だと思います。
struct SampleView: View {
let condition: Bool
var body: some View {
VStack {
Text("hoge")
Text("text")
.if(condition) {
$0.hidden()
}
Text("fuga")
}
}
}
他にも、「conditionが複雑で可読性がとてつもなく低い」かつ「@State
等の状態が内部にない」かつ「軽い要素なので再描画でも大したコストではない」みたいなケースでも、デメリットを理解した上で.if
modifierを利用するのはありかなと思います。
つまり、 可能な限り使わない努力をした上で、デメリットを理解して使うなら問題ない だと思います。
"自分は"View Identityの変化のデメリットを見た目で認識したいので、"自分は".if
modifierを使いませんが、他の方がメリットとデメリットのバランスを考えて決断した結果、使うことになったのなら良いと思います。
Special Thanks
このツイートに参加してくれた皆さん
@fummicc1
@omochimetaru
@treastrain
(アルファベット順)
Discussion
Optionalをアンラップした上でModifierを使いたい時に分岐Modifierを使うのは問題ないでしょうか?
例えば以下の様なエラーがあってそれをOptionalで持っているときに、エラーをAlertで表示したい様なケースです。
このとき、以下の様な分岐Modifierがあったら
この様にすっきり書けるのですが、パフォーマンス的に問題ないでしょうか?
質問ありがとうございます!
結論から言いますと、このケースも「特定の条件下でmodifierの付ける/付けないを切り替えている」ので同様にパフォーマンスやIdentityの問題を持っているかなと思います。
例えばonAppearでbodyの型(
Structural Identity
)を確認してみるとわかるのですが_ConditionalContent
に囲われてButton<Text>
が二つ出ているのでこれも同様に以下のように複数回Buttonを記述しているのと同様のIdentityになってしまっており、パフォーマンス的に良くないかと思います...
毎回?? ""
を書くのが煩雑でしたら(握りつぶすextensionを書くのはやや憚られますが)できることとしては、以下のようにnilを許容するextensionを生やすくらいかなと思います...空文字列渡すとxcstringsでいらないkeyが出るんですね...うーん、ここら辺はSwiftUIに対応して欲しいところではありますね
返答ありがとうございます。
Optionalの時に
?? ""
をしたくない理由としては、Xcode 15のString Catalog (xcstrings)を使っていると空文字も自動的にキーとして反応してしまうのでそれを回避したいというのがあります。うまい具合にできればいいのですが、トレードオフですかね。
xcstringsについて、なるほどです...これは嫌ですね...
そうですね...
if系modifier自体も僕としては「可能な限り使わない努力をした上で、デメリットを理解して使うなら問題ない」という意見なので
今回の例でも空文字が入るのを回避するために使うのも選択次第、って感じでしょうか...
(何か解決方法があればいいんですが...お役に立てずすみません)
この件、XCStringsについては要は
LocalizedStringKey
と認識されることがいけないので、String()
で包んでやれば良いことに気づきました。