SwiftUI入門-勉強メモ007-【プロパティ共有その1 State】

今回はプロパティの共有とその更新についてみていきます.

環境は,

  • macOS Catalina 10.15.7
  • Xcode 12.0.1
  • Swift 5.3
  • iOS 14.0

です.

目次

構造体は値の変更ができない?

基本的に構造体はプロパティの変更や更新ができません.

例えば, ランダムな数を出力するRandomNumberを作成してもエラーが出てしてしまいます.

SwiftUI

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

SwiftUI

Stateとは

SwiftUIのViewも構造体ですので, 値の変更や更新ができません. ただ,

  • 生年月日を入力する
  • 欲しい商品の個数を入力する
  • テキストを入力する

など, アプリでは値の更新を頻繁に行います. そこでSwiftUIでは, 構造体であるViewでも値の更新を可能にするために@Stateというプロパティラッパーが用意されています. (mutatingではないんですね…)

@Stateを用いると,

@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は,

  1. 宣言されたView内との中でしかSubView参照することができない.
  2. @Stateは外からアクセスができないようPrivate属性での利用が推奨されている.

と注意するべきで点もあります. とくに最初の「View内でしか参照することができない」は大切です. 例えば, クラスを作成してそこで設定されたプロパティの値を更新するといったことはできません.

この場合は@ObservedObjectという別のプロパティラッパーが必要になります.

具体的な例を見ていきます.

View内での@Stateの利用

ボタンが押された回数をカウントするものです.

一般的に, ボタンが押された回数をカウントする「num」は, 構造体の中で設定されたプロパティなので値の更新を行うことができません.

そこで@Stateをつけると, 値の更新が可能になり, SwiftUIが管理を行ってくれます.

 SwiftUIに管理された「num」の値が更新されるたびに, Viewは再計算され表示されます.

SwiftUI
SwiftUI

コード全体は次のようになっています.

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)
    }
}
SwiftUI

ファイルを分割する

おまけです. ボタンを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)
    }
}
SwiftUI

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

SwiftUI

とエラーになってしまいます.

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

SwiftUI

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)
    }
}
SwiftUI

コメント

コメントする

目次
閉じる