Open3

SwiftUI: var bodyの中にif文を書くとエラーになる

kabeyakabeya

SwiftUIでViewを定義する際、var body: some View内に、@ViewBuilder構文でビューの定義を書いていきます。

SwiftUIのif文てどう動いているのか?

例えば、"Hello, World!"の前にチェックボックスを付けるとします。

struct TestView: View {
    var isChecked: Bool = false
    
    var body: some View {
        HStack {
            if isChecked {
                Image(systemName: "checkmark.square")
            }
            else {
                Image(systemName: "square")
            }
            Text("Hello, World!")
        }
    }
}

上記コードでは、isCheckedの値に応じて、チェックONかチェックOFFかが表示されます。

さて、いまここでImageにいくつかオプションを指定したいけど2箇所に書くのは冗長だから、変わる部分だけifに入れよう、ということで以下のように書き直すとします。

struct TestView: View {
    var isChecked: Bool = false
    
    var body: some View {
        HStack {
            var systemName = "square"
            if isChecked {
                // ↓エラー:Type '()' cannot conform to 'View'
                systemName = "checkmark.square"
            }
            Image(systemName: systemName)
                .foregroundColor(.blue)
                .background(.green)
            Text("Hello, World!")
        }
    }
}

これはエラーになってしまいます。

@ViewBuilder構文では、宣言以外のすべての式が、Viewを返す必要があります。
@ViewBuilder構文により、

  • 通常の式はbuildExpression関数の呼び出し
  • ブロックはbuildBlock関数の呼び出し
  • if文はbuildIf関数の呼び出し
  • if else文はbuildEther関数の呼び出し

にそれぞれ変換されます。if文自体がクロージャを引数に取る関数のようなイメージです。
式にせよブロックにせよクロージャにせよ、いずれもViewを返す必要があります。

systemName = "checkmark.square"の行は、Viewを返していないのでエラーになります。

もし書き換えるとすればどうすればいいのか

struct TestView: View {
    var isChecked: Bool = false
    
    var body: some View {
        HStack {
            var systemName = "square"
            if isChecked {
                // ↓こうするとエラーにならない
                let _ = systemName = "checkmark.square"
            }
            Image(systemName: systemName)
                .foregroundColor(.blue)
                .background(.green)
            Text("Hello, World!")
        }
    }
}

if文のなかをlet _ = のような宣言に書き換えることで式と見なされなくなり、@ViewBuilder構文の対象にならなくなります。
つまりこのif文は、buildIfに変換されなくなります。
そして期待通りに動作します。

ちなみに

Viewを作る行でも、宣言文で受けると@ViewBuilder変換されません。

struct TestView: View {
   var body: some View {
        let _ = Text("Hello, World!")
    }
}

これだと「Hello, World!」は表示されません。

あと、let _ = という特殊な変数で受けてますが、普通の変数名でも構いません。
ただしその場合は、この変数使われてないよ?という警告が出ます。

kabeyakabeya

if 〜 else if 〜 else 〜 else ifの数に上限があるのか試してみました。
結論としては、おそらく制限がない、です。13個でも問題ありませんでした。

@ViewBuilder内に書けるViewは10個までという制限がありますが、こっちは制限がないようです。
if 〜 else { if 〜 else 〜 }のように分解される感じになっているのでしょうか。

kabeyakabeya

とはいえ?ifを使うよりは、以下のように書いた方がいいですね。

Image(systemName: isChecked ? "checkmark.square" : "square" )

ifの中の条件が複雑になるようであれば、関数にしてしまったほうがいいでしょう。