SwiftUIにおける条件分岐の注意点

開発環境

$ Xcode 14.3
Build version 14E222b

TL;DR

SwiftUIにおける条件分岐の注意点として、以下のことが言えそう。

  • 分岐をするとViewのidentityは別物になる
  • identityが異なるため分岐間で状態は引き継がれない
  • 分岐が切り替わるごとにViewは初期化されるため、パフォーマンスの低下を引き起こす可能性がある

モチベーション

インターネットを見ていたらSwiftUIのViewのextensionとして、条件に応じたViewを返すメソッドの話題を見かけた。理解が曖昧で全て自信を持って説明できなかったので整理する。

SwiftUIのViewの拡張メソッドとしてのif-else

話題になっていたコードは以下の感じ。

import SwiftUI

extension View {
    @ViewBuilder
    func `if`<TrueContent: View, FalseContent: View>(
        _ condition: Bool,
        @ViewBuilder _ trueContent: (Self) -> TrueContent,
        @ViewBuilder `else` falseContent: (Self) -> FalseContent
    ) -> some View {
        if condition {
            trueContent(self)
        } else {
            falseContent(self)
        }
    }
}

let flag = true

let view: some View = Text("Text")
    .if(flag) {
        $0.foregroundColor(.black)
    } else: {
        $0.foregroundColor(.red)
    }

print(type(of: view))  // _ConditionalContent<Text, Text>

Viewに対して条件に応じたmodifierを付けたい時に使うことができる。
前提として、modifierを条件毎に付けることは公式から非推奨であると説明されている。

公式推奨のやり方

WWDCのセッション*1では、複数のViewを条件毎に表現したい場合は分岐させて良さそうで、同じViewの2つの状態を表現したい場合は分岐させずにmodifier内に三項演算子書いて頑張ってくれ、みたいなことを言っていた。

import SwiftUI

let flag = true

let view: some View = Text("Text")
    .foregroundColor(flag ? .black : .red)

print(type(of: view))  // Text

なぜ分岐をさせることが良くないのか

分岐をするとそれぞれのViewは異なるIdentityを持つようになるため、異なるViewとなり状態は共有されずに @State@StateObject の今までの値は無くなり初期化される。

import SwiftUI

struct ContentView: View {
    @State var count = 0
    
    var body: some View {
        if count % 2 == 0 {
            counterView
        } else{
            counterView
        }
    }
    
    private var counterView: some View {
        VStack {
            Text("\(count)")
            Button("+") {
                count += 1
            }
            ChildView()
        }
        .padding(12)
        .background(
            RoundedRectangle(cornerRadius: 10)
                .stroke(.red, lineWidth: 1)
        )
    }
}

struct ChildView: View {
    @State var count = 0
    
    var body: some View {
        VStack {
            Text("\(count)")
            Button("+") {
                count += 1
            }
        }
        .foregroundColor(.white)
        .padding(12)
        .background(
            Color.cyan.cornerRadius(10)
        )
    }
}

下の数値は上の数値が変わることによって初期値の0に戻っている。

@Stete の値が変更されるとSwiftUIはその値に依存するViewを更新するため、意図しない更新によるパフォーマンスの低下が引き起こされうる。

また、Viewの切り替わりが早いと切り替わり前の消えているはずの状態が少しの間見えてしまう。これは意図していなそうな挙動っぽい。

SwiftUIにおける条件分岐の注意点として、以下のことが言えそう。

  • 分岐をするとViewのidentityは別物になる
  • identityが異なるため分岐間で状態は引き継がれない
  • 分岐が切り替わるごとにViewは初期化されるため、パフォーマンスの低下を引き起こす可能性がある

おわりに

インターネットを見ていたおかげで曖昧な理解に気づけて良かった。Demystify SwiftUIは何回か見ていたのだけれど今回で理解が進んだ。WWDCのセッション動画はありがたいが、可能な限り図とドキュメントに残してほしい気持ちがある。動画だとテキストより理解はしやすいが疲れる。なんでだろうな。単純に使ってる器官の個数が多いからか?

参考