[SwiftUI] Stateful な Button を作る

SwiftUI

SwiftUI の Button は、ステート(状態)をもちません。ケースによっては、状態を保持して、表示等を切り替えてくれると便利なので、そのような Button を作ってみます。

環境&対象

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

  • macOS Big Sur 11.3
  • Xcode 12.5
  • iOS 14.5

Stateful な Button

状態を持たせたいので、内部に Bool を持つような Button とします。

内部の Bool の状態に応じて表示する Label を切り替えるようにします。

また、内部の Bool の状態を取得したい時があるので、ボタンが押された時に、(変更後の Bool を引数に) 指定 closure が呼ばれるように作ります。

要件

つまり、以下が要件になります。

  • Bool を内部で保持する
  • Bool の値に応じて、異なるラベルを表示する
  • Bool の値が変更された際には、指定した closure 経由で変更後の値が取得できる

1つづつ 順番に実装していきます。

Bool を保持する View を定義

名前は、SDSStatefulButton としています。

内部に Bool を保持する Button を表示する View を定義します。


struct SDSStatefulButton : View {
    // (1)
    @State private var bValue: Bool
    // (2)
    public init(_ initialValue: Bool) {
        self._bValue = State(wrappedValue: initialValue)
    }
    
    public var body: some View {
        // (3)
        Button(action: {
            self.bValue.toggle()
        }, label: {
            Text("value: \(bValue ? "true" : "false" )")
        })
    }
}
コード解説
  1. Bool を @State な変数として定義します
  2. 外部から初期値を指定できるようにしています
  3. Button を表示しています。bValue の値に応じて Button のラベルの Text を変更しています。

以下のような ContentView で動作を確認してみます。


struct ContentView: View {
    var body: some View {
        VStack {
            SDSStatefulButton(true)
        }
        .padding()
    }
}

以下のような動作になります。

true / false でラベルが切り替わる Button

Button の定義と同じように、表示に使う View を外部から 渡せるようにします。内部の Bool 値に応じて、異なる View を表示できるように、2つの View を設定できるようにします。


// (1)
public struct SDSStatefulButton : View {
    @State private var bValue: Bool
    var trueLabel: TrueLabel
    var falseLabel: FalseLabel

    public init(_ initialValue: Bool,
         @ViewBuilder trueLabel: () -> TrueLabel,
         @ViewBuilder trueLabel: () -> FalseLabel) {
        self._bValue = State(wrappedValue: initialValue)
        // (2)
        self.trueLabel = trueLabel()
        self.falseLabel = falseLabel()
    }
    
    public var body: some View {
        Button(action: {
            self.bValue.toggle()
        }, label: {
             // (3)
            if bValue {
                trueLabel
            } else {
                falseLabel
            }
        })
    }
}
コード解説
  1. Generics を使用して、On/Off 用の View を指定します。
  2. 渡された View を内部に保持します
  3. bValue の値に応じて、適切なビューを表示します

ボタンを使う側は以下のように書くことができます。


struct ContentView: View {
    @State private var bValue: Bool = true
    var body: some View {
        VStack {
            SDSStatefulButton(true, trueLabel: {
                // (1)
                Image(systemName: "plus.app").resizable()
            }, falseLabel: {
                // (2)
                Image(systemName: "minus.square").resizable()
            })
            .frame(width: 50, height: 50)
        }
        .padding()
    }
}
コード解説
  1. true 時に表示される Label (View) を外部から指定します
  2. false 時に表示される Label (View) を外部から指定します

以下のような動作になります。

ここまでで、3つの要件のうちの最初の2つを満たせました。

値変更時に closure を呼び出す

初期化時に渡された closure を呼び出すだけです。


//
//  SDSStatefulButton.swift
//
//  Created by : Tomoaki Yagishita on 2021/06/04
//  © 2021  SmallDeskSoftware
//

import SwiftUI

public struct SDSStatefulButton : View {
    @State private var bValue: Bool
    var trueLabel: OnLabel
    var falseLabel: OffLabel
    var onChange: ((Bool) -> Void)?
    
    public init(_ initialValue: Bool,
         @ViewBuilder onLabel: () -> OnLabel,
         @ViewBuilder offLabel: () -> OffLabel,
         onChange: ((Bool) -> Void)? = nil) {
        self._bValue = State(wrappedValue: initialValue)
        self.trueLabel = onLabel()
        self.falseLabel = offLabel()
        self.onChange = onChange
    }
    
    public var body: some View {
        Button(action: {
            self.bValue.toggle()
            onChange?(self.bValue)
        }, label: {
            if bValue {
                trueLabel
            } else {
                falseLabel
            }
        })
    }
}

ボタンを使う側は以下のように書くことができます。


struct ContentView: View {
    @State private var text: String = "value"
    var body: some View {
        VStack {
            SDSStatefulButton(true, trueLabel: {
                Image(systemName: "plus.app").resizable()
            }, falseLabel: {
                Image(systemName: "minus.square").resizable()
            }, onChange: { value in
                text = String("value is changed to \(value ? "true" : "false")")
            })
            .frame(width: 50, height: 50)
            
            Text(text)
        }
        .padding()
    }
}

以下のような動作になります。

これで、内部に Bool を保持し、値によって 表示を変更し、値変更時にはclosure が呼ばれる Button を作ることができました。

まとめ:Stateful な Button の作り方

Stateful な Button の作り方
  • 変数は、@State で保持し、変更に応じてビューが更新されるようにする
  • Generics を使って、さまざまな View を Label に使用できるようにする
  • 外部から closure を渡せるようにして、値変更時には、closure を呼ぶ

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

SwiftUI おすすめ本

SwiftUI を理解するには、以下の本がおすすめです。

# SwiftUI2.0 が登場したことで少し古くなってしまいましたが、いまでも 定番本です。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です