🐕

(SwiftUI)Binding<t?>をBinding<t>に変換する

2022/01/30に公開

はじめに

以下の環境で動作確認しました。

  • Xcode 13.2.1 (13C100)
  • iOS 15.1

(問題)Binding<t?>Binding<t>に変換する

以下のコードについて考えます。

struct ContentView: View {
  @State var sizes = [Int: CGFloat]()

  // ...省略...

}

さて、self.sizes[index]として要素を取り出すと、その型はBinding<CGFloat?>になります。Binding<CGFloat>に変換するにはどうすれば良いでしょうか?

Optional型から具体的な型を取り出す方法は3種類あります。

  1. オプショナルバインディング
  2. ??演算子を利用してデフォルト値を設定する
  3. !演算子を利用して強制的にunwrapする

しかし、これらの方法はt?tに変換する方法であり、Binding<t?>に対しては利用できません。

(解決策)その場でBinding型の値を作る

Binding<t?>からBinding<t>に変換することはできません。そこで、Binding型の値を作ることで対応します。

Binding型にはイニシャライザとしてinit(get: () -> Value, set: (Value) -> Void)が定義されています。今回はこれを利用します。

Binding(get: { self.sizes[index] ?? デフォルト値 }, set: { self.sizes[index] = $0 })

このように実装することでBinding<CGFloat?>からBinding<CGFloat>に変換できます。

※上記は解決策のひとつです。もっとシンプルな解決策をご存知の場合はコメントいただけると助かります。

(おまけ)@Bindingに割り当てる

1秒ごとに「Hello, World!」の文字の大きさがランダムに変化するアプリを作ってみました。Xcodeを起動して新規プロジェクトを作成したら、ContentView.swiftに以下のコードを貼り付けてください。

import SwiftUI

struct HelloWorld: View {
  @Binding var size: CGFloat

  var body: some View {
    Text("Hello, World!")
      .font(.system(size: self.size))
  }
  init(size: Binding<CGFloat>) {
    self._size = size
  }
}

struct ContentView: View {
  @State var sizes = [Int: CGFloat]()

  var body: some View {
    VStack {
      ForEach(0..<5) { index in
        HelloWorld(
          size: Binding(
            get: { self.sizes[index] ?? 16 }, set: { self.sizes[index] = $0 }))
      }
    }
    .onAppear {
      Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
        let i = Int.random(in: 0..<5)
        let size = Int.random(in: 16...64)

        self.sizes[i] = CGFloat(size)
      }
    }
  }
}

実行すると4行の「Hello, World!」が表示されます。そして、1秒ごとにそれぞれの文字の大きさが16から64の範囲でランダムに変化します。

参考資料

  1. Binding | Apple Developer
  2. SwiftUI: how to use dictionary as @Binding?

Discussion