今回はプロパティの共有とその更新についてみていきます.
環境は,
- macOS Catalina 10.15.7
- Xcode 12.0.1
- Swift 5.3
- iOS 14.0
です.
構造体は値の変更ができない?
基本的に構造体はプロパティの変更や更新ができません.
例えば, ランダムな数を出力するRandomNumberを作成してもエラーが出てしてしまいます.

更新できないことを回避するためにはmutating
をつけます. これで, 値の更新が可能となり, 欲しかった出力結果を得ることができます.

Stateとは
SwiftUIのViewも構造体ですので, 値の変更や更新ができません. ただ,
- 生年月日を入力する
- 欲しい商品の個数を入力する
- テキストを入力する
など, アプリでは値の更新を頻繁に行います. そこでSwiftUIでは, 構造体であるViewでも値の更新を可能にするために@State
というプロパティラッパーが用意されています. (mutating
ではないんですね…)
@State
を用いると,
- 値の更新が可能になる
- SwifuUIがプロパティの管理を行い, 値が更新されるたびにViewを再表示する
ようになります.
SwiftUI manages the storage of any property you declare as a state. When the state value changes, the view invalidates its appearance and recomputes the body. Use the state as the single source of truth for a given view.
https://developer.apple.com/documentation/swiftui/stateより.
この@State
は,
- 宣言されたView内との中でしかSubView参照することができない.
@State
は外からアクセスができないようPrivate属性での利用が推奨されている.
と注意するべきで点もあります. とくに最初の「View内でしか参照することができない」は大切です. 例えば, クラスを作成してそこで設定されたプロパティの値を更新するといったことはできません.
この場合は@ObservedObject
という別のプロパティラッパーが必要になります.
具体的な例を見ていきます.
View内での@Stateの利用
ボタンが押された回数をカウントするものです.
一般的に, ボタンが押された回数をカウントする「num」は, 構造体の中で設定されたプロパティなので値の更新を行うことができません.
そこで@State
をつけると, 値の更新が可能になり, SwiftUIが管理を行ってくれます.
SwiftUIに管理された「num」の値が更新されるたびに, Viewは再計算され表示されます.


コード全体は次のようになっています.
import SwiftUI
struct ContentView: View {
//ボタンが押された回数をカウントする
@State private var num = 0
var body: some View {
VStack(spacing: 20.0) {
//ボタンの作成
Button(action: {
//ボタンが押されるたびに+1していく
self.num += 1
}) {
Text("ボタンを押してください")
.fontWeight(.bold)
.foregroundColor(Color.blue)
.padding(10)
}
.background(Color.yellow)
.border(Color.blue)
//ボタンを押された回数を表示
Text("\(num)")
.font(.largeTitle)
}
.font(.title)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
SubView内での@Stateの利用
@State
でラップされたプロパティをSubView側で受け取る場合, そのことを明示するために@Binding
をつけます. @Binding
でView内で定義されたプロパティにアクセスすることができるようになります.
先ほどのボタンをCustomViewで表示してみます. 挙動は変わりません.
import SwiftUI
struct ContentView: View {
//ボタンが押された回数をカウントする
@State private var num = 0
var body: some View {
VStack(spacing: 20.0) {
//ボタンの作成
PressedButton(count: $num)
//ボタンを押された回数を表示
Text("\(num)")
.font(.largeTitle)
}
.font(.title)
}
}
struct PressedButton: View {
@Binding var count: Int
var body: some View {
Button(action: {
//ボタンが押されるたびに+1していく
self.count += 1
}) {
Text("ボタンを押してください")
.fontWeight(.bold)
.foregroundColor(Color.blue)
.padding(10)
}
.background(Color.yellow)
.border(Color.blue)
}
}
SubView側のcountをprivate
にすると,

とエラーになります. こちら側はprivate
でなくてもよいみたいです.
もちろん複数の値を引き渡すことも可能です.
import SwiftUI
struct ContentView: View {
//ボタンが押された回数をカウントする
@State private var num1 = 0
@State private var num2 = 0
var body: some View {
VStack(spacing: 20.0) {
//ボタンの作成
PressedButton(count1: $num1, count2: $num2)
//ボタンを押された回数を表示
Text("\(num1)")
.font(.largeTitle)
Text("\(num2)")
.font(.largeTitle)
}
.font(.title)
}
}
struct PressedButton: View {
@Binding var count1: Int
@Binding var count2: Int
var body: some View {
Button(action: {
//ボタンが押されるたびに+1していく
self.count1 += 1
self.count2 += 3
}) {
Text("ボタンを押してください")
.fontWeight(.bold)
.foregroundColor(Color.blue)
.padding(10)
}
.background(Color.yellow)
.border(Color.blue)
}
}

ファイルを分割する
おまけです. ボタンをCustomViewにしたので, ファイルを分割することもできます. Flutterなどとは違い, ファイルを読み込む必要はありません. 勝手に認識をしてくれます. これはどういう仕組みなんでしょうか…
ここでは「ContentView.swift」と「PressedButton.swift」という2つのファイルに分割してみました. それぞれのファイルは以下の通りです.
import SwiftUI
struct ContentView: View {
//ボタンが押された回数をカウントする
@State private var num = 0
var body: some View {
VStack(spacing: 20.0) {
//ボタンの作成
PressedButton(count: $num)
//ボタンを押された回数を表示
Text("\(num)")
.font(.largeTitle)
}
.font(.title)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import SwiftUI
struct PressedButton: View {
@Binding var count: Int
//イニシャライザ
init(count: Binding<Int>) {
self._count = count
}
var body: some View {
Button(action: {
//ボタンが押されるたびに+1していく
self.count += 1
}) {
Text("ボタンを押してください")
.fontWeight(.bold)
.foregroundColor(Color.blue)
.padding(10)
}
.background(Color.yellow)
.border(Color.blue)
}
}

今回は必要ありませんが, イニシャライザでプロパティを初期化する場合, 引数はBinding<Int>型になります. プロパティに値を代入するときは, _(アンダースコア)をつけます. つけないと,

とエラーになってしまいます.
これは「self.count」と「count」の型が違うためです. Swiftは型にうるさい言語です. 型が違うので代入できません.「self.count」はInt型で「count」と「self._count」はBinding<Int>型になります.

stack overflowに似たような話がありました.
このあたりは少しややこしそうです.
孫View(SubViewの中にもう一つSubViewがある状態)の場合
@Binding
で値を引き継ぐことで引継ぎが可能になります. ただ, コードとしては複雑になり, 見栄えがよい物とは言えません.
import SwiftUI
struct ContentView: View {
//ボタンが押された回数をカウントする
@State private var num = 0
var body: some View {
VStack(spacing: 40.0) {
//ボタンの作成
PressedButton(count: $num)
//SubView1
SubView1(count1: $num)
}
.font(.title)
}
}
//ボタン
struct PressedButton: View {
@Binding var count: Int
var body: some View {
Button(action: {
//ボタンが押されるたびに+1していく
self.count += 1
}) {
Text("ボタンを押してください")
.fontWeight(.bold)
.foregroundColor(Color.blue)
.padding(10)
}
.background(Color.yellow)
.border(Color.blue)
}
}
//SubView1の定義
struct SubView1:View {
@Binding var count1 : Int
var body: some View {
VStack{
SubView2(count2: $count1)
}
.padding(15)
.background(Color.blue)
}
}
//SubView2の定義
struct SubView2:View {
@Binding var count2 : Int
var body: some View {
VStack {
Text("ここに \(count2) が出力されます")
}
.padding(5)
.background(Color.pink)
}
}

コメント