Sponsor Link
環境&対象
- macOS Monterey 12.3
- Xcode 13.3
- iOS 15.4
Binding
@Binding を付与すると、上位ビューから渡されてきた変数を 変更することができるようになります。
見方を変えると、”Source of truth” の変数に対して、変更を加えることができるようになります。
@Binding なしで渡されると、”参照のみ”で変更することはできません。
実装的には、@Binding として渡されることで、別の箇所(source of truth) に確保されている変数への参照が渡されてくることになり、変更することができるようになります。
実際に、手を動かして Property Wrapper を作ってみるとよくわかる気がします。
[Swift] PropertyWrapper 手を動かして理解する(その1 wrappedValue)
[Swift] PropertyWrapper 手を動かして理解する(その2 wrappedValue の初期化)
[Swift] PropertyWrapper 手を動かして理解する(その3 $ もしくは projectedValue の理解)
@Binding 使用例
@Binding は、@ で始まっていることからもわかるように、Property Wrapper です。
変数に、@Binding を付与して使用します。
Apple のドキュメントにある例は以下のような例です。
struct PlayButton: View {
@Binding var isPlaying: Bool
var body: some View {
Button(isPlaying ? "Pause" : "Play") {
isPlaying.toggle()
}
}
}
struct PlayerView: View {
var episode: Episode
@State private var isPlaying: Bool = false
var body: some View {
VStack {
Text(episode.title)
.foregroundStyle(isPlaying ? .primary : .secondary)
PlayButton(isPlaying: $isPlaying) // Pass a binding.
}
}
}
PlayerView が親ビューです。親ビュー内に @State として isPlaying が “source of truth” として定義されています。
PlayerView は、Binding で isPlaying を PlayButton に渡しています。
PlayButton は、Button 操作の結果として isPlaying 変数 変更しますが、これは、PlayerView の保持する isPlaying を変更することになります。これは、PlayButton が isPlaying 変数を Binding で PlayerView から受け取っているため可能になっています。
Binding を渡す側
Binding を渡す側の視点で考えてみます。
SwiftUI に用意されている コントロール系のビュー(Button や TextField 等)は、ユーザーインプットに応じて値を返してきます。入力された値を返すために @Binding を付与された変数を引数として受け取ります。
例えば、Slider は、ユーザーからスライダーUIを経由して入力を受け取るので、Binding<Double>等 を受け取るようになっています。(実際には、BinaryFloatingPoint 型です)
以下の init では、最初の引数として Binding を受け取ります。
init<V>(value: Binding<V>, in bounds: ClosedRange<V> = 0...1, onEditingChanged: @escaping (Bool) -> Void = { _ in }) where V : BinaryFloatingPoint, V.Stride : BinaryFloatingPoint
Slider のドキュメントは、こちら。
すこし不便なケース
ビューに渡す変数をそのまま定義していれば、$ をつけて渡すだけです。先の PlayerView/PlayerButton はその例でした。
ですが、アプリの開発をすすめると 実際にはもう少し複雑なケースがでてきます。
例えば、実際には Int で管理している数値を スライダーUIを使用して変更したいときです。
Slider が受け取る Binding は、Double 等の BinaryFloatingPoint に準拠している必要があります。
Double や Float は BinaryFloatingPoint に準拠していますが、Int は、準拠していません。つまり、Int の Binding は渡すことができないということです。
そのほかにも、実際には指定された値を調整してからデータとして保持したいとか、表示する時に特定の加工を行ったものを表示したい等があります。
そんな時には、ローカルに Binding を作って渡すことができます。一般に Local Binding と呼ばれていますので、説明します。
Binding は、struct
Apple のドキュメントを見てみると、Binding は、struct であることもわかります。
initializer を見てみると、Binding を受け取るものや projectedValue を受け取るものと並んで、get/set を指定する initializer が見つかります。
init(get: @escaping () -> Value, set: @escaping (Value) -> Void)
この initializer を使って、Binding を作成することになります。
Apple のドキュメントは、こちら。
Local Binding
先ほどの Slider の例で考えます。
Int ではなく、enum で保持している変数を Slider で設定できるようにすることを考えます。
enum を Slider で変更できるようにするのがゴールです。
そのままでは Slider が enum を処理できないことはわかっているので、とりあえず、Slider 用の変数も別途定義しています。
//
// ContentView.swift
//
// Created by : Tomoaki Yagishita on 2022/03/16
// © 2022 SmallDeskSoftware
//
import SwiftUI
struct ContentView: View {
@State private var amount: Amount = .medium
@State private var userSelection: Double = 0.5
var body: some View {
VStack {
Text("Selection: \(amount.description)")
Slider(value: $userSelection)
}
.padding()
}
enum Amount: CustomStringConvertible {
static let smallRange = 0..<0.25
static let mediumRange = 0.25..<0.75
static let largeRange = 0.75...1
case small, medium, large
var description: String {
switch self {
case .small:
return "Small"
case .medium:
return "Medium"
case .large:
return "Large"
}
}
var representValue: Double {
switch self {
case .small:
return 0.15
case .medium:
return 0.5
case .large:
return 0.85
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Binding<Double> を渡している箇所に、局所的な Binding を作成して渡すことをします。(
これが、Local Binding と言われる理由だと思います。)
LocalBinding 例(body 抜粋)
var body: some View {
// define local binding
let enumBinding = Binding {
return amount.representValue
} set: { newValue in
if Amount.smallRange.contains(newValue) {
amount = .small
} else if Amount.mediumRange.contains(newValue) {
amount = .medium
} else {
amount = .large
}
}
VStack {
Text("Selection: \(amount.description)")
Slider(value: enumBinding) // pass local binding
}
.padding()
}
Binding の initializer は、1つ目の引数に getter を、2つ目の引数に setter を取ります。getter には引数はなく Value 型を返します。setter には、引数として newValue (Value型) が渡されます。
この getter と setter で enum ~ Double 間の変換を行うことで、Slider での変更を enum に反映させることができます。
上記は、[0,0.25), [0.25,0.75),[0.75,1.0] という値域を前提に、.small / .medium / .large というenum との相互変換をしています。(Slider は、デフォルトでは、0 ~ 1 の値を設定します)
body の中で let を使っていることが少し不思議かもしれませんが、これは、ResultBuilder が変数定義を許容するためです。
詳細は、以下の記事をどうぞ。
[Swift] SE-0289 Result Builder を理解するために写経してみる
なお、以下のように Slider の引数指定箇所で直接定義することも可能です。
LocalBinding を引数指定で定義
var body: some View {
VStack {
Text("Selection: \(amount.description)")
Slider(value: Binding(get: {
return amount.representValue
}, set: { newValue in
if Amount.smallRange.contains(newValue) {
amount = .small
} else if Amount.mediumRange.contains(newValue) {
amount = .medium
} else {
amount = .large
}
}))
}
.padding()
}
以下のような動作になります。
LocalBinding 例 全コード
コードの全体を再掲しておきます。
//
// ContentView.swift
//
// Created by : Tomoaki Yagishita on 2022/03/16
// © 2022 SmallDeskSoftware
//
import SwiftUI
struct ContentView: View {
@State private var amount: Amount = .medium
var body: some View {
VStack {
Text("Selection: \(amount.description)")
Slider(value: Binding(get: {
return amount.representValue
}, set: { newValue in
if Amount.smallRange.contains(newValue) {
amount = .small
} else if Amount.mediumRange.contains(newValue) {
amount = .medium
} else {
amount = .large
}
}))
}
.padding()
}
enum Amount: CustomStringConvertible {
static let smallRange = 0..<0.25
static let mediumRange = 0.25..<0.75
static let largeRange = 0.75...1
case small, medium, large
var description: String {
switch self {
case .small:
return "Small"
case .medium:
return "Medium"
case .large:
return "Large"
}
}
var representValue: Double {
switch self {
case .small:
return 0.15
case .medium:
return 0.5
case .large:
return 0.85
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
まとめ
Binding を再確認し、Local Binding の使い方を説明してみました。
- Binding は、受け取った側で source of truth を変更できる
- Local Binding を使用することで、要求される型以外のものも Binding できる
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
Sponsor Link