見出し画像

【じっくりSw1ftUI39】(🌟プロパティラッパーまとめ有🌟)実践編9〜第22章 SwiftUIの状態プロパティ、監視可能な状態、および環境オブジェクト③Bindableとか環境オブジェクトとか

さてと、今週もビールを片手に、ひとつよろしゅう🙇‍♂️
前回は、

で、監視オブジェクトまでやったので(サンプルコードが間違ってるから苦労した💦👀)、今回は

Bindableとか環境オブジェクト

なんかをやってく〜〜〜🕺

3週間に分けてやった

SwiftUIフレームワークにおけるプロパティラッパーの使いかた

も今回でラストかなと🧐

簡単なようで実は一番難しく、難しいようで理解すれば簡単な
👉概念モデルを入れとかないと次章以降でやるステッパーとかの各部品で
何をやっているのかが分からない
(ま、いろんな機能を色々動かしてみて初めて本当の理解に繋がるところもあるが、、、)

んで、回数を分けてちゃんと作ったって感じ〜〜〜〜

ま、毎度のことオイラの学びなんざ関係なしって方は、

でも読んで適当に自分でやれば良いんじゃね❓👀
では早速

じっくり第22章の7節以降を読んでく👓

ここではまず、他のビューから受け取った値でもやり取りできるぜ👍
ってことを示したいみたいだね👀💦

まずは7節の完成コード

struct Essentials22_3BindableView: View {
    @Bindable var fruitsData : Essentials22ObservableFruitsData = Essentials22ObservableFruitsData()
    
    var body: some View {
        VStack{
            TextField("果物名を入力", text: $fruitsData.fruitsMenu)
                .background(Color.gray)
            Essentials22_3ScrollView(fruitsAmount: $fruitsData.fruitsAmount)
            HStack{
                Button(action: {
                    fruitsData.minusOne()
                }, label: {
                    Text("-")
                })
                .font(.largeTitle)
                .frame(width: 50, height: 50)
                .background(Color.orange)
                .padding()
                Text("\(fruitsData.fruitsMenu)")
                    .font(.largeTitle)
                Button(action: {
                    fruitsData.plusOne()
                }, label: {
                    Text("+")
                })
                .font(.largeTitle)
                .frame(width: 50, height: 50)
                .background(Color.orange)
                .padding()
            }
        }
    }
}

struct Essentials22_3ScrollView: View {
    //フルーツの購入数
    @Binding var fruitsAmount : Int
    var body: some View {
        Text("\(fruitsAmount)")
            .font(.system(size: 200))
    }
}
テケトーにTextFieldに入力して、ーを3回タップ
みたいな感じ

if文とかで、fruitsAmount==0の時は、ーを押せない

みたいなこともできなくはないけど、プロットの流れに脱線するのでここではやらない藁🤣
👉まあ、やりたかったら挑戦してみて〜〜〜お買い物アプリなんかでーなんて出すことはないんだけど、、、、

ここでポイント①@Bindableを外してみると

てな感じでFatalなエラーになるので要注意🕺

理由:

監視オブジェクトの状態からの結び付けの際に必要な作法だから

さてと、お次は環境オブジェクト

ここもまずは完成系から

//第8節で追加
@Observable class Essentials22_3FruitsFlavor {
    //ここは整数型だとスライダーでエラー
    var flavor = 0.0
}
struct Essentials22_3FruitsFlavorView: View {
    let flavorSetting = Essentials22_3FruitsFlavor()
    var body: some View {
        VStack {
            Essentials22_3FlavorSliderView()
            Essentials22_3FlavorDisplayView()
        }
        .background(Color.green)
        //ここがないとクラッシュ
        .environment(flavorSetting)
    }
}

struct Essentials22_3FlavorDisplayView: View{
    @Environment(Essentials22_3FruitsFlavor.self) var flavorSetting: Essentials22_3FruitsFlavor
    
    var body: some View {
        VStack {
            Text("完熟度: \(flavorSetting.flavor)")
        }
    }
}
struct Essentials22_3FlavorSliderView: View {
    @Environment(Essentials22_3FruitsFlavor.self) var flavorSetting: Essentials22_3FruitsFlavor
  
    var body: some View {
        @Bindable var flavorSetting = flavorSetting
        Slider(value: $flavorSetting.flavor, in: 0...100)
            .background(Color.cyan)
    }
}

動かすと、

スライドバーを右にスライドさせると、、、

ここでポイント:環境オブジェクト

  1. 環境オブジェクトは、監視可能なオブジェクトと同じ方法で宣言する。主な違いは、オブジェクトは宣言されているビューの環境に格納されるため、ビューからビューに渡さずにすべての子ビューからアクセスできる。

  2. 2 番目のビューもデータクラスにアクセスする必要があ理、プロパティへのバインディングを作成する必要がある場合、 @Bindable プロパティ ラッパーを使用する必要あり。

  3. 大元の表示ビューに.environment(引数)がないと起動直後に次の診断が表示されてクラッシュする。

って感じかな👀💦

以上藁🤣

今回のまとめコード

//第7節で追加
struct Essentials22_3Contents: View {
    var body: some View {
        ScrollView{
            VStack {
                Essentials22_3BindableView()
                Essentials22_3FruitsFlavorView()
            }
        }
    }
}
struct Essentials22_3BindableView: View {
    @Bindable var fruitsData : Essentials22ObservableFruitsData = Essentials22ObservableFruitsData()
    var body: some View {
        VStack{
            TextField("果物名を入力", text: $fruitsData.fruitsMenu)
                .background(Color.gray)
            Essentials22_3ScrollView(fruitsAmount: $fruitsData.fruitsAmount)
            HStack{
                Button(action: {
                    fruitsData.minusOne()
                }, label: {
                    Text("-")
                })
                .font(.largeTitle)
                .frame(width: 50, height: 50)
                .background(Color.orange)
                .padding()
                Text("\(fruitsData.fruitsMenu)")
                    .font(.largeTitle)
                Button(action: {
                    fruitsData.plusOne()
                }, label: {
                    Text("+")
                })
                .font(.largeTitle)
                .frame(width: 50, height: 50)
                .background(Color.orange)
                .padding()
            }
        }
    }
}
struct Essentials22_3ScrollView: View {
    //フルーツの購入数
    @Binding var fruitsAmount : Int
    var body: some View {
        Text("\(fruitsAmount)")
            .font(.system(size: 200))
    }
}
//第8節で追加
@Observable class Essentials22_3FruitsFlavor {
    //ここは整数型だとスライダーでエラー
    var flavor = 0.0
}
struct Essentials22_3FruitsFlavorView: View {
    let flavorSetting = Essentials22_3FruitsFlavor()
    var body: some View {
        VStack {
            Essentials22_3FlavorSliderView()
            Essentials22_3FlavorDisplayView()
        }
        .background(Color.green)
        //ここがないとクラッシュ
        .environment(flavorSetting)
    }
}
struct Essentials22_3FlavorDisplayView: View{
    @Environment(Essentials22_3FruitsFlavor.self) var flavorSetting: Essentials22_3FruitsFlavor
    var body: some View {
        VStack {
            Text("完熟度: \(flavorSetting.flavor)")
                .font(.largeTitle)
        }
    }
}
struct Essentials22_3FlavorSliderView: View {
    @Environment(Essentials22_3FruitsFlavor.self) var flavorSetting: Essentials22_3FruitsFlavor
    var body: some View {
        @Bindable var flavorSetting = flavorSetting
        Slider(value: $flavorSetting.flavor, in: 0...5)
            .background(Color.cyan)
    }
}

Apple公式

と、

から扱ってきたプロパティラッパーについて、もう一回ここできちんと整理しとく〜〜〜〜
(後から、追記する方が面倒くさいので、まとめられるものは今の段階でまとめとくだけ💦👀
元々、ここまで終わってからまとめようと思ってたので、、、)

SwiftUIで主に使うプロパティラッパーのまとめ

大きなポイントとしては、

  1. Observationフレームワークは、ユーザーインターフェイスの外部にあり、アプリ内のSwiftUIビュー構造のサブセットでのみ必要なデータに使える。このアプローチを使用する場合、データを表すクラスに @Observable マクロを適用する必要あり。

  2. ビュー宣言で監視可能なオブジェクトのプロパティにバインドするには、プロパティで@Bindableプロパティラッパーを使う必要あり。

  3. 環境オブジェクトは、ユーザー インターフェイスの外部にあるデータに最適なソリューションを提供するが、多くのビューでアクセスが必要。

  4. 監視可能なオブジェクトと同じ方法で宣言するが、環境オブジェクトバインディングは@Environmentプロパティラッパーを使用して、SwiftUIViewファイルで宣言。子ビューにアクセスできるようになる前に、environment()修飾子を使用してビュー階層に挿入される前に、環境オブジェクトも初期化する必要あり。

ってことらしいけど、より詳しく。

◾️状態プロパティ:@State

  • 状態プロパティは、トグル ボタンが有効かどうか、テキストフィールドに入力されているテキスト、ピッカービューでの現在の選択内容など、ビューレイアウトにローカルな状態を保存するためにのみ使用。

  • 状態プロパティは、StringやInt 値などの単純なデータ型を格納するために使用され、@Stateプロパティラッパーを使用して宣言。

  • 状態値はそれを囲んでいるビューに対してローカルであるため、プライベートプロパティとして宣言する必要

◾️状態バインディング:@Binding

  • 状態プロパティは、それが宣言されているビューおよびすべての子ビューに対してローカルだが、ビューに1つ以上のサブビューが含まれており、それらのサブビューも同じ状態プロパティにアクセスする必要がある場合がある。

  • @Bindingプロパティラッパーを使用してプロパティを宣言することで解決できる。

◾️観測可能なオブジェクト:@Observable

  • 状態プロパティは、ビューの状態をローカルに保存する方法を提供し、ローカル ビューでのみ使用できるため、サブビューで状態バインディングが実装されていない限り、他のビューからアクセスすることはできない。

  • 状態プロパティも一時的なもので、親ビューが消えると状態も失われる。

  • 一方、Observableオブジェクトは、外部にあり、複数のビューからアクセスできる永続データを表す。

  • Observable オブジェクトは、ObservableObjectプロトコルに準拠するクラスの形式をとる。

  • 監視可能なオブジェクトの実装はデータの性質とソースに応じてアプリケーション固有になる。

  • 通常は時間の経過とともに変化することがわかっている1つ以上のデータ値の収集と管理を担当する。

  • 監視可能なオブジェクトは、タイマーや通知などのイベントを処理できる。

  • 監視可能なオブジェクトは、それが担当するデータ値を公開プロパティとして公開する。(クラスで宣言してるんだからそりゃそうだ👀💦)

  • その後、オブザーバーオブジェクトはパブリッシャーをサブスクライブし、パブリッシュされたプロパティが変更されるたびに更新を受け取る。

  • 上で概説した状態プロパティと同様に、これらの公開プロパティにバインドして、SwiftUI ビューは自動的に更新され、監視可能なオブジェクトに格納されているデータの変更を反映する。

*参考:Combine(あまり使わないほうが今はいいかも💦)

  • Combineフレームワークは、複数のパブリッシャーを1つのストリームにマージすることから、サブスクライバーの要件に合わせてパブリッシュされたデータを変換することまで、さまざまなタスクを実行するためのカスタム パブリッシャーを構築するためのプラットフォームを提供してる。

  • これにより、元のパブリッシャーと結果として得られるサブスクライバーの間で、複雑なエンタープライズ レベルのデータ処理チェーンを実装できるようになる。

  • しかし、通常、組み込みのパブリッシャー タイプの1つで、ほとんどの要件に必要なものはすべて足りる。

  • 監視可能なオブジェクト内に公開されたプロパティを実装する最も簡単な方法は、プロパティを宣言するときに@Publishedプロパティラッパーを使用すること。

  • このラッパーは、ラップされたプロパティ値が変更されるたびに、すべてのサブスクライバーに更新を送信する。

  • サブスクライバーは、@ObservedObjectまたは@StateObjectプロパティラッパーを使用して、監視可能なオブジェクトをサブスクライブする。

  • サブスクライブされると、そのビューとその子ビューは、この章の前半で状態プロパティで使用したのと同じ手法を使用して、公開されたプロパティにアクセスする。

◾️状態オブジェクトの結合:@StateObjectと@ObservedObjectの違い

  • StateObjectプロパティラッパー (@StateObject) は、@ObservedObjectラッパーの代替としてiOS14で導入された。

  • 状態オブジェクトと監視オブジェクトの主な違い:監視オブジェクト参照は宣言されているビューによって所有されていないため、使用中に SwiftUI システムによって破棄または再作成されるリスクがある(例:ビューが再レンダリングされた結果などです)。

  • @ObservedObjectの代わりに@StateObjectを使用すると、参照が宣言されているビューによって所有されることが保証され、参照がまだ必要である間は、宣言されているローカル ビューまたは子によってSwiftUIによって破棄されなくなる。

◾️観察フレームワークの使用:@Observable

  • Combineフレームワークの代わりに Observationを使用すると、上で概説したのと同じ動作が提供されますが、コードはより単純になる。

  • 宣言の前に@Observableマクロを付ける。

  • マクロがこれを自動的に処理するため、@Publishedプロパティラッパーを使用する必要になった。

  • @StateObjectプロパティラッパーが使用されていた場合、@Stateに置き換えできる。

◾️観察と@Bindable:@Observableと@Bindable

  • Observableで指定したが値が見つからない場合、修正するには、@Bindableプロパティラッパーをデータ宣言に適用する必要がある。

  • このプロパティラッパーは、監視可能なオブジェクトのプロパティからバインディングを作成する必要がある場合に使う。

◾️環境オブジェクト:@Environment(上記と重複)

  1. 環境オブジェクトは、監視可能なオブジェクトと同じ方法で宣言する。主な違いは、オブジェクトは宣言されているビューの環境に格納されるため、ビューからビューに渡さずにすべての子ビューからアクセスできる。

  2. 使い過ぎると、却って複雑になる可能性あり。👉使うことに合理性があるかで判断(何事もそうじゃね👀💦)。

  3. 2 番目のビューもデータクラスにアクセスする必要があ理、プロパティへのバインディングを作成する必要がある場合、 @Bindableプロパティ ラッパーを使用する必要あり。

  4. 大元の表示ビューに.environment(引数)がないと起動直後に次の診断が表示されてクラッシュする。

暗記しようとか覚えようよりも全体像を把握するのに使ってね〜〜〜
覚えなくても、動かしてればいずれは肌感覚で身につくので〜〜〜

さてと、次回は

SwiftUIフレームワークを使った実践編の長〜〜〜〜い導入編も終わったので、いよいよ

色んなビューを作ってより実践的に遊ぶ
第23章 
iOS17SwiftUI サンプルチュートリアル

に入ってく。このリンクを少し覗くだけで、今回までで念入りにやってたプロパティラッパーもすぐに出てくるので、

何で数週間に渡ってやってたかわかる

と思う。

記事公開後、

さてと、いつもどおり

で今回のコードもアプリにまとめとく🕺

てな
てな
てなの
てな感じ👀

サンプルコード(第22章をまとめて〜〜〜)

import SwiftUI
import WebKit
//4節で追加
import Combine

//タイトル
let essentialsChapter22NavigationTitle = "第22章"
let essentialsChapter22Title = "第22章 SwiftUIの状態プロパティ、監視可能な状態、および環境オブジェクト"
let essentialsChapter22_1SubTitle = "第1節 StateとBinding"
let essentialsChapter22_2SubTitle = "第2節 CombineとObservatedObject"
let essentialsChapter22_3SubTitle = "第3節 合わせ技とEnvironmentObject"

//コード
let codeEssentials22_1 = """
struct Essentials22_1Contents: View {
    var body: some View {
        ScrollView{
            Essentials22_1StateView()
        }
    }
}
 #Preview  {
    Essentials22_1Contents()
}

struct Essentials22_1StateView: View {
    //ここにプロパティラッパーと初期値を設定
    @State private var fruitsName = ""
    //トグルボタン用のプロパティラッパーを追加して、初期値を設定
    @State private var fruitsBasket = false

    var body: some View {
        ScrollView{
            VStack{
                //プロパティラッパーを$付きで設定
                TextField("果物名を入力", text: $fruitsName)
                    .background(Color.yellow)
                //引き継いだプロパティラッパーを文字列として表示
                HStack{
                    Text("入力されたのは、")
                        .padding()
                    Text("\\(fruitsName)")
                        .padding()
                    Text("です")
                        .padding()
                }
                .aspectRatio(contentMode: .fill)
                .border(Color.black)
                //トグルボタンを追加
                Toggle(isOn: $fruitsBasket) {
                    Text("かごを付けますか?")
                }
                Essentials22_1FruitsBasketView(fruitsBasket: $fruitsBasket)
            }
        }
    }
}

struct Essentials22_1FruitsBasketView: View {
    @Binding var fruitsBasket: Bool
    var body: some View {
        //トグルのOnとOff時の設定を追加
        Text(fruitsBasket ? "🧺" : "")
            .font(.largeTitle)
    }
}
"""
let codeEssentials22_2 = """
//4節で追加
class Essentials22FruitsData: ObservableObject {
    @Published var title = ""
    @Published var fruitsAmount = 0
    @Published var isEnabled = false
}

//4〜5節で追加
struct Essentials22_2CombinedView: View {
    @StateObject private var stateobjectFruitsData = Essentials22FruitsData()
    var body: some View {
        VStack {
            Essentials22_2ObservedView(observedFruitsData: stateobjectFruitsData)
            TextField(text: $stateobjectFruitsData.title) {
                Text("個数を入力してください")
            }
            HStack{
                Button {
                    stateobjectFruitsData.fruitsAmount -= 1
                } label: {
                    Text("-")
                        .font(.largeTitle)
                }
                .foregroundColor(.green)
                .frame(width: 50,height: 50)
                .background(Color.yellow)
                Text("\\(stateobjectFruitsData.title) の個数は、\\(stateobjectFruitsData.fruitsAmount)個です。")
                Button {
                    stateobjectFruitsData.fruitsAmount += 1
                } label: {
                    Text("+")
                        .font(.largeTitle)
                }
                .foregroundColor(.green)
                .frame(width: 50,height: 50)
                .background(Color.yellow)
            }
            Spacer()
        }
    }
}

//4〜5節で追加
struct Essentials22_2ObservedView: View {
    @ObservedObject var observedFruitsData: Essentials22FruitsData
    var body: some View {
        HStack{
            Text("編集モード:\\(observedFruitsData.isEnabled)")
            Toggle("", isOn: $observedFruitsData.isEnabled)
        }
    }
}

//6節で追加
@Observable class Essentials22ObservableFruitsData {
    var fruitsMenu = ""
    var fruitsAmount = 0
    //初期化
    init(fruitsMenu: String = "", fruitsAmount: Int = 0) {
        self.fruitsMenu = fruitsMenu
    }
    //関数
    func minusOne(){
        fruitsAmount -= 1
    }
    func plusOne(){
        fruitsAmount += 1
    }
}

struct Essentials22ObservableFruitsView:View {
    var observablefruitsData: Essentials22ObservableFruitsData = Essentials22ObservableFruitsData(fruitsMenu: "🍎")
    var body: some View {
        HStack {
            Button(action: {
                observablefruitsData.minusOne()
            }, label: {
                Text("-")
                    .font(.largeTitle)

            })
            .frame(width: 50,height: 50)
            .foregroundStyle(Color.green)
            .background(Color.yellow)
            Text("\\(observablefruitsData.fruitsMenu)を \\(observablefruitsData.fruitsAmount)つ買いました")
            Button(action: {
                observablefruitsData.plusOne()
            }, label: {
                Text("+")
                    .font(.largeTitle)

            })
            .frame(width: 50,height: 50)
            .foregroundStyle(Color.green)
            .background(Color.yellow)
            .padding()
        }
    }
}
"""
let codeEssentials22_3 = """
//第7節で追加
struct Essentials22_3Contents: View {
    var body: some View {
        ScrollView{
            VStack {
                Essentials22_3BindableView()
                Essentials22_3FruitsFlavorView()
            }
        }
    }
}
struct Essentials22_3BindableView: View {
    @Bindable var fruitsData : Essentials22ObservableFruitsData = Essentials22ObservableFruitsData()
    var body: some View {
        VStack{
            TextField("果物名を入力", text: $fruitsData.fruitsMenu)
                .background(Color.gray)
            Essentials22_3ScrollView(fruitsAmount: $fruitsData.fruitsAmount)
            HStack{
                Button(action: {
                    fruitsData.minusOne()
                }, label: {
                    Text("-")
                })
                .font(.largeTitle)
                .frame(width: 50, height: 50)
                .background(Color.orange)
                .padding()
                Text("\\(fruitsData.fruitsMenu)")
                    .font(.largeTitle)
                Button(action: {
                    fruitsData.plusOne()
                }, label: {
                    Text("+")
                })
                .font(.largeTitle)
                .frame(width: 50, height: 50)
                .background(Color.orange)
                .padding()
            }
        }
    }
}
struct Essentials22_3ScrollView: View {
    //フルーツの購入数
    @Binding var fruitsAmount : Int
    var body: some View {
        Text("\\(fruitsAmount)")
            .font(.system(size: 200))
    }
}
//第8節で追加
@Observable class Essentials22_3FruitsFlavor {
    //ここは整数型だとスライダーでエラー
    var flavor = 0.0
}
struct Essentials22_3FruitsFlavorView: View {
    let flavorSetting = Essentials22_3FruitsFlavor()
    var body: some View {
        VStack {
            Essentials22_3FlavorSliderView()
            Essentials22_3FlavorDisplayView()
        }
        .background(Color.green)
        //ここがないとクラッシュ
        .environment(flavorSetting)
    }
}
struct Essentials22_3FlavorDisplayView: View{
    @Environment(Essentials22_3FruitsFlavor.self) var flavorSetting: Essentials22_3FruitsFlavor
    var body: some View {
        VStack {
            Text("完熟度: \\(flavorSetting.flavor)")
                .font(.largeTitle)
        }
    }
}
struct Essentials22_3FlavorSliderView: View {
    @Environment(Essentials22_3FruitsFlavor.self) var flavorSetting: Essentials22_3FruitsFlavor
    var body: some View {
        @Bindable var flavorSetting = flavorSetting
        Slider(value: $flavorSetting.flavor, in: 0...5)
            .background(Color.cyan)
    }
}
"""
//ポイント
let pointEssentials22_1 = """
インターフェース上でデータを取り扱うプロパティでSwiftUIで用意されてるオプションは主に4つで、それらはすべて
・状態
・監視
・環境
を制御しながら
インターフェースの表示や動作の状態を管理するもの

Bindingを使う時は、Bindingされる親ビューにBindingで引き継ぐ用の変数なんかもないとエラーになるから気をつけて
"""
let pointEssentials22_2 = """
◾️クラスを追加する位置
Swiftはオブジェクト志向言語 + 静的プログラミング言語
なので、実は、
どこに書こう(追加しよう)が自由
クラスだけを集めたクラスファイルを設けて、別ファイルで管理しようが、
すぐに使う構造体の上に近接しておこうが
管理しやすく、呼び出しさえできれば
どこでも問題なし!!!
デザインの原則(①整理、②近接、③反復、④強調 + 足し算より引き算)
なんかを普段、コードや組み込みなんかをやる時も意識してるので、
・22章で使うなら22章のファイル内で書く
・インポートしたフレームワーク、共通で使う部分(総論部)、個別で使う部分(構造体やビュー)なんかでやった方が管理がしやすい
ので、個人的に、実際の構造体の前にクラスを置いた(追加した)だけの話
さらにコードがたまってきて、別のやり方の方が管理しやすいって判断したら、
同じプロジェクトファイル内のどこかに整理する
だけの話
👉コードの配置にこうしないといけないなんて決まりなんざない
=自分とか組織単位で管理しやすい方法は違うし、それに対応できるようにプラットフォーム(開発環境)は作ってる

◾️コードの記法に気をつけよう
クラス名とか構造体名、プロトコル名 : 大文字始まりで書く(アッパーケース)
関数名や引数(変数や定数) : 小文字始まりで書く(ロワーケース)

◾️必ず動かして検証する
市販で日本なり世界なりで発行されてる本ですら、この状態👀💦

これが入門書とかSwiftどころかプログラミングを初めてやる人向け
本ではなく、実際の製品であったら?
👉阿鼻叫喚と非難轟々の嵐=金返せって感じだよね〜〜〜
"""
let pointEssentials22_3 = """
SwiftUIで主に使うプロパティラッパーのまとめ
⭐️暗記しようとか覚えようよりも全体像を把握するのに使ってね〜〜〜
⭐️覚えなくても、動かしてればいずれは肌感覚で身につくので〜〜〜

◾️状態プロパティ:@State
・状態プロパティは、トグル ボタンが有効かどうか、テキストフィールドに入力されているテキスト、ピッカービューでの現在の選択内容など、ビューレイアウトにローカルな状態を保存するためにのみ使用。
・状態プロパティは、StringやInt 値などの単純なデータ型を格納するために使用され、@Stateプロパティラッパーを使用して宣言。
・状態値はそれを囲んでいるビューに対してローカルであるため、プライベートプロパティとして宣言する必要

◾️状態バインディング:@Binding
・状態プロパティは、それが宣言されているビューおよびすべての子ビューに対してローカルだが、ビューに1つ以上のサブビューが含まれており、それらのサブビューも同じ状態プロパティにアクセスする必要がある場合がある。
・@Bindingプロパティラッパーを使用してプロパティを宣言することで解決できる。

◾️観測可能なオブジェクト:@Observable
・状態プロパティは、ビューの状態をローカルに保存する方法を提供し、ローカルビューでのみ使用できるため、サブビューで状態バインディングが実装されていない限り、他のビューからアクセスすることはできない。
・状態プロパティも一時的なもので、親ビューが消えると状態も失われる。
・一方、Observableオブジェクトは、外部にあり、複数のビューからアクセスできる永続データを表す。
・Observableオブジェクトは、ObservableObjectプロトコルに準拠するクラスの形式をとる。
・監視可能なオブジェクトの実装はデータの性質とソースに応じてアプリケーション固有になる。
・通常は時間の経過とともに変化することがわかっている 1つ以上のデータ値の収集と管理を担当する。
・監視可能なオブジェクトは、タイマーや通知などのイベントを処理できる。
・監視可能なオブジェクトは、それが担当するデータ値を公開プロパティとして公開する。(クラスで宣言してるんだからそりゃそうだ👀💦)
・その後、オブザーバーオブジェクトはパブリッシャーをサブスクライブし、パブリッシュされたプロパティが変更されるたびに更新を受け取る。
・上で概説した状態プロパティと同様に、これらの公開プロパティにバインドして、SwiftUIビューは自動的に更新され、監視可能なオブジェクトに格納されているデータの変更を反映する。
*参考:Combine(あまり使わないほうが今はいいかも💦)
・Combineフレームワークは、複数のパブリッシャーを1つのストリームにマージすることから、サブスクライバーの要件に合わせてパブリッシュされたデータを変換することまで、さまざまなタスクを実行するためのカスタム パブリッシャーを構築するためのプラットフォームを提供してる。
・これにより、元のパブリッシャーと結果として得られるサブスクライバーの間で、複雑なエンタープライズレベルのデータ処理チェーンを実装できるようになる。
・しかし、通常、組み込みのパブリッシャータイプの1つで、ほとんどの要件に必要なものはすべて足りる。
・監視可能なオブジェクト内に公開されたプロパティを実装する最も簡単な方法は、プロパティを宣言するときに@Publishedプロパティラッパーを使用すること。
・このラッパーは、ラップされたプロパティ値が変更されるたびに、すべてのサブスクライバーに更新を送信する。
・サブスクライバーは、@ObservedObjectまたは@StateObjectプロパティラッパーを使用して、監視可能なオブジェクトをサブスクライブする。

・サブスクライブされると、そのビューとその子ビューは、この章の前半で状態プロパティで使用したのと同じ手法を使用して、公開されたプロパティにアクセスする。

◾️状態オブジェクトの結合:@StateObjectと@ObservedObjectの違い
・StateObjectプロパティラッパー (@StateObject)は、@ObservedObjectラッパーの代替としてiOS14で導入された。
・状態オブジェクトと監視オブジェクトの主な違い:監視オブジェクト参照は宣言されているビューによって所有されていないため、使用中にSwiftUIシステムによって破棄または再作成されるリスクがある(例:ビューが再レンダリングされた結果などです)。
・@ObservedObjectの代わりに@StateObjectを使用すると、参照が宣言されているビューによって所有されることが保証され、参照がまだ必要である間は、宣言されているローカルビューまたは子によって SwiftUI によって破棄されなくなる。

◾️観察フレームワークの使用:@Observable
・Combineフレームワークの代わりに Observationを使用すると、上で概説したのと同じ動作が提供されますが、コードはより単純になる。
・宣言の前に@Observableマクロを付ける。
・マクロがこれを自動的に処理するため、@Publishedプロパティ ラッパーを使用する必要になった。
・@StateObjectプロパティラッパーが使用されていた場合、@Stateに置き換えできる。

◾️観察と@Bindable:@Observableと@Bindable
・Observableで指定したが値が見つからない場合、修正するには、@Bindableプロパティラッパーをデータ宣言に適用する必要がある。
・このプロパティラッパーは、監視可能なオブジェクトのプロパティからバインディングを作成する必要がある場合に使う。

◾️環境オブジェクト:@Environment(上記と重複)
・環境オブジェクトは、監視可能なオブジェクトと同じ方法で宣言する。主な違いは、オブジェクトは宣言されているビューの環境に格納されるため、ビューからビューに渡さずにすべての子ビューからアクセスできる。
・使い過ぎると、却って複雑になる可能性あり。👉使うことに合理性があるかで判断(何事もそうじゃね👀💦)。
・2番目のビューもデータクラスにアクセスする必要があ理、プロパティへのバインディングを作成する必要がある場合、@Bindableプロパティ ラッパーを使用する必要あり。
・大元の表示ビューに.environment(引数)がないと起動直後に次の診断が表示されてクラッシュする。
"""
//URL
let urlEssentials22_1 = "https://note.com/m_kakudo/n/ne0c79a7ef90d"
let urlEssentials22_2 = "https://note.com/m_kakudo/n/ndce829c36ef5"
let urlEssentials22_3 = "https://note.com/m_kakudo/n/ne47bfe24f3c3"

//ビュー管理構造体
struct ListiOSApp17DevelopmentEssentialsCh22: Identifiable {
    var id: Int
    var title: String
    var view: ViewEnumiOSApp17DevelopmentEssentialsCh22
}
//遷移先の画面を格納する列挙型
enum ViewEnumiOSApp17DevelopmentEssentialsCh22{
    case Sec1
    case Sec2
    case Sec3
}
//各項目に表示するリスト項目
let dataiOSApp17DevelopmentEssentialsCh22: [ListiOSApp17DevelopmentEssentialsCh22] = [
    ListiOSApp17DevelopmentEssentialsCh22(id: 1, title: essentialsChapter22_1SubTitle, view: .Sec1),
    ListiOSApp17DevelopmentEssentialsCh22(id: 2, title: essentialsChapter22_2SubTitle, view: .Sec2),
    ListiOSApp17DevelopmentEssentialsCh22(id: 3, title: essentialsChapter22_3SubTitle, view: .Sec3),
]
struct iOSApp17DevelopmentEssentialsCh22: View {
    var body: some View {
        VStack {
            Divider()
            List (dataiOSApp17DevelopmentEssentialsCh22) { data in
                self.containedViewiOSApp17DevelopmentEssentialsCh22(dataiOSApp17DevelopmentEssentialsCh22: data)
            }
            .edgesIgnoringSafeArea([.bottom])
        }
        .navigationTitle(essentialsChapter22NavigationTitle)
        .navigationBarTitleDisplayMode(.inline)
    }
    //タップ後に遷移先へ遷移させる関数
    func containedViewiOSApp17DevelopmentEssentialsCh22(dataiOSApp17DevelopmentEssentialsCh22: ListiOSApp17DevelopmentEssentialsCh22) -> AnyView {
        switch dataiOSApp17DevelopmentEssentialsCh22.view {
        case .Sec1:
            return AnyView(NavigationLink (destination: Essentials22_1()) {
                Text(dataiOSApp17DevelopmentEssentialsCh22.title)
            })
        case .Sec2:
            return AnyView(NavigationLink (destination: Essentials22_2()) {
                Text(dataiOSApp17DevelopmentEssentialsCh22.title)
            })
        case .Sec3:
            return AnyView(NavigationLink (destination: Essentials22_3()) {
                Text(dataiOSApp17DevelopmentEssentialsCh22.title)
            })
        }
    }
} #Preview  {
    iOSApp17DevelopmentEssentialsCh22()
}
//Essentials22_1.swift
struct Essentials22_1: View {
    var body: some View {
        VStack{
            TabView {
                Essentials22_1Contents()
                    .tabItem {
                        Image(systemName: contentsImageTab)
                        Text(contentsTextTab)
                    }
                Essentials22_1Code()
                    .tabItem {
                        Image(systemName: codeImageTab)
                        Text(codeTextTab)
                    }
                Essentials22_1Points()
                    .tabItem {
                        Image(systemName: pointImageTab)
                        Text(pointTextTab)
                    }
                Essentials22_1WEB()
                    .tabItem {
                        Image(systemName: webImageTab)
                        Text(webTextTab)
                    }
            }
        }
    }
} #Preview  {
    Essentials22_1()
}

struct Essentials22_1Contents: View {
    var body: some View {
        ScrollView{
            Essentials22_1StateView()
        }
    }
}
 #Preview  {
    Essentials22_1Contents()
}

struct Essentials22_1StateView: View {
    //ここにプロパティラッパーと初期値を設定
    @State private var fruitsName = ""
    //トグルボタン用のプロパティラッパーを追加して、初期値を設定
    @State private var fruitsBasket = false
    
    var body: some View {
        ScrollView{
            VStack{
                //プロパティラッパーを$付きで設定
                TextField("果物名を入力", text: $fruitsName)
                    .background(Color.yellow)
                //引き継いだプロパティラッパーを文字列として表示
                HStack{
                    Text("入力されたのは、")
                        .padding()
                    Text("\(fruitsName)")
                        .padding()
                    Text("です")
                        .padding()
                }
                .aspectRatio(contentMode: .fill)
                .border(Color.black)
                //トグルボタンを追加
                Toggle(isOn: $fruitsBasket) {
                    Text("かごを付けますか?")
                }
                Essentials22_1FruitsBasketView(fruitsBasket: $fruitsBasket)
            }
        }
    }
}

struct Essentials22_1FruitsBasketView: View {
    @Binding var fruitsBasket: Bool
    var body: some View {
        //トグルのOnとOff時の設定を追加
        Text(fruitsBasket ? "🧺" : "")
            .font(.largeTitle)
    }
}

struct Essentials22_1Code: View {
    var body: some View {
        ScrollView{
            Text(codeEssentials22_1)
        }
    }
} #Preview  {
    Essentials22_1Code()
}
struct Essentials22_1Points: View {
    var body: some View {
        ScrollView{
            Text(pointEssentials22_1)
        }
    }
} #Preview  {
    Essentials22_1Points()
}

struct Essentials22_1WebView: UIViewRepresentable {
    let searchURL: URL
    func makeUIView(context: Context) -> WKWebView {
        let view = WKWebView()
        let request = URLRequest(url: searchURL)
        view.load(request)
        return view
    }
    func updateUIView(_ uiView: WKWebView, context: Context) {
        
    }
}
struct Essentials22_1WEB: View {
    private var url:URL = URL(string: urlEssentials22_1)!
    var body: some View {Essentials22_1WebView(searchURL: url)
    }
} #Preview  {
    Essentials22_1WEB()
}

//22_2
struct Essentials22_2: View {
    var body: some View {
        VStack{
            TabView {
                Essentials22_2Contents()
                    .tabItem {
                        Image(systemName: contentsImageTab)
                        Text(contentsTextTab)
                    }
                Essentials22_2Code()
                    .tabItem {
                        Image(systemName: codeImageTab)
                        Text(codeTextTab)
                    }
                Essentials22_2Points()
                    .tabItem {
                        Image(systemName: pointImageTab)
                        Text(pointTextTab)
                    }
                Essentials22_2WEB()
                    .tabItem {
                        Image(systemName: webImageTab)
                        Text(webTextTab)
                    }
            }
        }
    }
} #Preview  {
    Essentials22_2()
}

struct Essentials22_2Contents: View {
    var body: some View {
        ScrollView{
            VStack {
                Essentials22_2CombinedView()
                Essentials22ObservableFruitsView()
            }
        }
    }
}
 #Preview  {
    Essentials22_2Contents()
}

//4節で追加
class Essentials22FruitsData: ObservableObject {
    @Published var title = ""
    @Published var fruitsAmount = 0
    @Published var isEnabled = false
}

//4〜5節で追加
struct Essentials22_2CombinedView: View {
    @StateObject private var stateobjectFruitsData = Essentials22FruitsData()
    var body: some View {
        VStack {
            Essentials22_2ObservedView(observedFruitsData: stateobjectFruitsData)
            TextField(text: $stateobjectFruitsData.title) {
                Text("個数を入力してください")
            }
            HStack{
                Button {
                    stateobjectFruitsData.fruitsAmount -= 1
                } label: {
                    Text("-")
                        .font(.largeTitle)
                }
                .foregroundColor(.green)
                .frame(width: 50,height: 50)
                .background(Color.yellow)
                Text("\(stateobjectFruitsData.title) の個数は、\(stateobjectFruitsData.fruitsAmount)個です。")
                Button {
                    stateobjectFruitsData.fruitsAmount += 1
                } label: {
                    Text("+")
                        .font(.largeTitle)
                }
                .foregroundColor(.green)
                .frame(width: 50,height: 50)
                .background(Color.yellow)
            }
            Spacer()
        }
    }
}

//4〜5節で追加
struct Essentials22_2ObservedView: View {
    @ObservedObject var observedFruitsData: Essentials22FruitsData
    var body: some View {
        HStack{
            Text("編集モード:\(observedFruitsData.isEnabled)")
            Toggle("", isOn: $observedFruitsData.isEnabled)
        }
    }
}

//6節で追加
@Observable class Essentials22ObservableFruitsData {
    var fruitsMenu = ""
    var fruitsAmount = 0
    //初期化
    init(fruitsMenu: String = "", fruitsAmount: Int = 0) {
        self.fruitsMenu = fruitsMenu
    }
    //関数
    func minusOne(){
        fruitsAmount -= 1
    }
    func plusOne(){
        fruitsAmount += 1
    }
}

struct Essentials22ObservableFruitsView:View {
    var observablefruitsData: Essentials22ObservableFruitsData = Essentials22ObservableFruitsData(fruitsMenu: "🍎")
    var body: some View {
        HStack {
            Button(action: {
                observablefruitsData.minusOne()
            }, label: {
                Text("-")
                    .font(.largeTitle)

            })
            .frame(width: 50,height: 50)
            .foregroundStyle(Color.green)
            .background(Color.yellow)
            Text("\(observablefruitsData.fruitsMenu)を \(observablefruitsData.fruitsAmount)つ買いました")
            Button(action: {
                observablefruitsData.plusOne()
            }, label: {
                Text("+")
                    .font(.largeTitle)

            })
            .frame(width: 50,height: 50)
            .foregroundStyle(Color.green)
            .background(Color.yellow)
            .padding()
        }
    }
}

struct Essentials22_2Code: View {
    var body: some View {
        ScrollView{
            Text(codeEssentials22_2)
        }
    }
} #Preview  {
    Essentials22_2Code()
}
struct Essentials22_2Points: View {
    var body: some View {
        ScrollView{
            Text(pointEssentials22_2)
        }
    }
} #Preview  {
    Essentials22_2Points()
}

struct Essentials22_2WebView: UIViewRepresentable {
    let searchURL: URL
    func makeUIView(context: Context) -> WKWebView {
        let view = WKWebView()
        let request = URLRequest(url: searchURL)
        view.load(request)
        return view
    }
    func updateUIView(_ uiView: WKWebView, context: Context) {
        
    }
}
struct Essentials22_2WEB: View {
    private var url:URL = URL(string: urlEssentials22_2)!
    var body: some View {Essentials22_2WebView(searchURL: url)
    }
} #Preview  {
    Essentials22_2WEB()
}

//Essentials22_3.swift
struct Essentials22_3: View {
    var body: some View {
        VStack{
            TabView {
                Essentials22_3Contents()
                    .tabItem {
                        Image(systemName: contentsImageTab)
                        Text(contentsTextTab)
                    }
                Essentials22_3Code()
                    .tabItem {
                        Image(systemName: codeImageTab)
                        Text(codeTextTab)
                    }
                Essentials22_3Points()
                    .tabItem {
                        Image(systemName: pointImageTab)
                        Text(pointTextTab)
                    }
                Essentials22_3WEB()
                    .tabItem {
                        Image(systemName: webImageTab)
                        Text(webTextTab)
                    }
            }
        }
    }
} #Preview  {
    Essentials22_3()
}
struct Essentials22_3Code: View {
    var body: some View {
        ScrollView{
            Text(codeEssentials22_3)
        }
    }
} #Preview  {
    Essentials22_3Code()
}
struct Essentials22_3Points: View {
    var body: some View {
        ScrollView{
            Text(pointEssentials22_3)
        }
    }
} #Preview  {
    Essentials22_3Points()
}

struct Essentials22_3WebView: UIViewRepresentable {
    let searchURL: URL
    func makeUIView(context: Context) -> WKWebView {
        let view = WKWebView()
        let request = URLRequest(url: searchURL)
        view.load(request)
        return view
    }
    func updateUIView(_ uiView: WKWebView, context: Context) {
        
    }
}
struct Essentials22_3WEB: View {
    private var url:URL = URL(string: urlEssentials22_3)!
    var body: some View {Essentials22_3WebView(searchURL: url)
    }
} #Preview  {
    Essentials22_3WEB()
}
//第7節で追加
struct Essentials22_3Contents: View {
    var body: some View {
        ScrollView{
            VStack {
                Essentials22_3BindableView()
                Essentials22_3FruitsFlavorView()
            }
        }
    }
}
struct Essentials22_3BindableView: View {
    @Bindable var fruitsData : Essentials22ObservableFruitsData = Essentials22ObservableFruitsData()
    var body: some View {
        VStack{
            TextField("果物名を入力", text: $fruitsData.fruitsMenu)
                .background(Color.gray)
            Essentials22_3ScrollView(fruitsAmount: $fruitsData.fruitsAmount)
            HStack{
                Button(action: {
                    fruitsData.minusOne()
                }, label: {
                    Text("-")
                })
                .font(.largeTitle)
                .frame(width: 50, height: 50)
                .background(Color.orange)
                .padding()
                Text("\(fruitsData.fruitsMenu)")
                    .font(.largeTitle)
                Button(action: {
                    fruitsData.plusOne()
                }, label: {
                    Text("+")
                })
                .font(.largeTitle)
                .frame(width: 50, height: 50)
                .background(Color.orange)
                .padding()
            }
        }
    }
}
struct Essentials22_3ScrollView: View {
    //フルーツの購入数
    @Binding var fruitsAmount : Int
    var body: some View {
        Text("\(fruitsAmount)")
            .font(.system(size: 200))
    }
}
//第8節で追加
@Observable class Essentials22_3FruitsFlavor {
    //ここは整数型だとスライダーでエラー
    var flavor = 0.0
}
struct Essentials22_3FruitsFlavorView: View {
    let flavorSetting = Essentials22_3FruitsFlavor()
    var body: some View {
        VStack {
            Essentials22_3FlavorSliderView()
            Essentials22_3FlavorDisplayView()
        }
        .background(Color.green)
        //ここがないとクラッシュ
        .environment(flavorSetting)
    }
}
struct Essentials22_3FlavorDisplayView: View{
    @Environment(Essentials22_3FruitsFlavor.self) var flavorSetting: Essentials22_3FruitsFlavor
    var body: some View {
        VStack {
            Text("完熟度: \(flavorSetting.flavor)")
                .font(.largeTitle)
        }
    }
}
struct Essentials22_3FlavorSliderView: View {
    @Environment(Essentials22_3FruitsFlavor.self) var flavorSetting: Essentials22_3FruitsFlavor
    var body: some View {
        @Bindable var flavorSetting = flavorSetting
        Slider(value: $flavorSetting.flavor, in: 0...5)
            .background(Color.cyan)
    }
}

ま、ポイントがここまで長いと、

データベースでまとめた方が早いかWEBでやった方が良いんじゃね?

て感じだけど、

この本の流れに合わせて、じっくり理解してほしいってのもあるので、
いずれ出てくるまでこのままな感じかな
🧐

さてと、今日は後、

でも書いたとおりなんだが、生憎の雨で

かりんの散歩

には行けないので、

💃おっさん酒飲みながら華麗に咲き誇ろう🕺
みなさんも残り少ないけど、良き週末を!!!

この記事が気に入ったらサポートをしてみませんか?