[SwiftUI] Binding のおさらいと Local Binding

SwiftUI2021

     
Binding をあらためて説明して、@Binding をローカルに作る Local Binding を説明します。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Monterey 12.3
  • Xcode 13.3
  • iOS 15.4

Binding

@Binding を付与すると、上位ビューから渡されてきた変数を 変更することができるようになります。

見方を変えると、"Source of truth" の変数に対して、変更を加えることができるようになります。
@Binding なしで渡されると、”参照のみ”で変更することはできません。

実装的には、@Binding として渡されることで、別の箇所(source of truth) に確保されている変数への参照が渡されてくることになり、変更することができるようになります。

実際に、手を動かして Property Wrapper を作ってみるとよくわかる気がします。
Swift[Swift] PropertyWrapper 手を動かして理解する(その1 wrappedValue) Swift[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 であることもわかります。

# Property Wrapper は、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 で変更できるようにするのがゴールです。

SliderExample

そのままでは 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 の値を設定します)

MEMO
body の中で let を使っていることが少し不思議かもしれませんが、これは、ResultBuilder が変数定義を許容するためです。
詳細は、以下の記事をどうぞ。
Swift[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 を再確認し、Local Binding の使い方
  • Binding は、受け取った側で source of truth を変更できる
  • Local Binding を使用することで、要求される型以外のものも Binding できる

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。