見出し画像

【じっくりSw1ftUI38】実践編8〜第22章 SwiftUIの状態プロパティ、監視可能な状態、および環境オブジェクト②CombineとObservedObject

さてと、今週もよろしく🕺前回は、

でSwiftUIフレームワークで使うプロパティラッパーの基本

@Stateと@Binding

を軽くやったので〜〜〜今回は〜〜〜

@ObservedObject

あたりをやってく🕺

見た感じ次回あたりで終わりそうかな

なので、

今週もひとつ、しばしお付き合いを!!!
合言葉は、
個人知を世界知に!
💃裾野さえ広がればなんでも良き🕺

と、いつもどおりオイラの学び直しなんて要らね‼️って人は、

にサンプルコードなんかも含めて転がってるからそっち見ればいいんじゃね👀
って感じで早速じっくりやってく💦

第22章の続きをじっくり読んでく

前回やったStateとBindingって、

やり取りを設定してるビューでは、変数とかデータを引き渡してくれる

んだけど、

他のビューに移っちゃうと、変数とかデータが失われちゃう

んだよね💦

👉複数のビューを使う時(殆どのアプリがそう)に、面倒

そこで、

ObservableObjectプロトコルを使ったクラスを設ける
👉変数なりデータなりを管理しやすくしたモノ
=監視オブジェクト

ってイメージかな🧐

iOS17以前だとCombineフレームワーク

なんかをインポートしてやる必要があったみたいだけど、
iOS17から

Observationフレームワークを使って、
よりシンプルに簡単に作成できるようになった

ってことが言いたいみたいだね👀💦

だがしかし、

  1. 以前の方法に触れることで、Observationフレームワークの背景を知っておく

  2. 古いモノの方がネット上にいろんなサンプルが転がっている

から、

Combineから学習してこ

って流れになってるね🌱

じゃ早速、実際に動かしてく

前回までのコード

import SwiftUI
import WebKit

//ビュー管理構造体
struct ListiOSApp17DevelopmentEssentialsCh22: Identifiable {
    var id: Int
    var title: String
    var view: ViewEnumiOSApp17DevelopmentEssentialsCh22
}
//遷移先の画面を格納する列挙型
enum ViewEnumiOSApp17DevelopmentEssentialsCh22{
    case Sec1
}
//各項目に表示するリスト項目
let dataiOSApp17DevelopmentEssentialsCh22: [ListiOSApp17DevelopmentEssentialsCh22] = [
    ListiOSApp17DevelopmentEssentialsCh22(id: 1, title: essentialsChapter22_1SubTitle, view: .Sec1)
]
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)
            })
        }
    }
}
#Preview {
    iOSApp17DevelopmentEssentialsCh22()
}
//Essentials22.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()
}

//コード
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 essentialsChapter22NavigationTitle = "第22章"
let essentialsChapter22Title = "第22章 SwiftUIの状態プロパティ、監視可能な状態、および環境オブジェクト"
let essentialsChapter22_1SubTitle = "第1節 StateとBinding"

//ポイント
let pointEssentials22_1 = """
インターフェース上でデータを取り扱うプロパティでSwiftUIで用意されてるオプションは主に4つで、それらはすべて
・状態
・監視
・環境
を制御しながら
インターフェースの表示や動作の状態を管理するもの

Bindingを使う時は、Bindingされる親ビューにBindingで引き継ぐ用の変数なんかもないとエラーになるから気をつけて
"""
//URL
let urlEssentials22_1 = "https://note.com/m_kakudo/n/ne0c79a7ef90d"

に〜〜〜まずは、

//4節で追加
import Combine

を、

てな感じで追加して〜〜〜

次は、クラスを追加してく〜〜〜

//4節で追加
class Essentials22FruitsData : ObservableObject {
    @Published var fruitsName = ""
    @Published var fruitsNumber = 0
    
    init() {
        updateFruitsData()
    }
    
    func updateFruitsData(){
       fruitsNumber += 1
    }
}

てな感じのコードを

テケトーに追加

ここでポイント①:クラスを追加する位置

色んな舌足らずな日本の市販本(すでにめっちゃ古い)なんかだと、

それぞれの学者や執筆者なんかが、
その人独自のやり方 = Swift共通のやり方

みたいな感じな書き振りで、
「クラスは、ここに入れるのが普通!」
「クラスは、こーするのがSwiftのやり方だ!」
みたいに豪語する人も多いし、それを読んで鵜呑みにする人もいるんだけど、

Swiftはオブジェクト志向言語 + 静的プログラミング言語

なので、実は、

どこに書こう(追加しよう)が自由

クラスだけを集めたクラスファイルを設けて、別ファイルで管理しようが、
すぐに使う構造体の上に近接しておこうが

管理しやすく、呼び出しさえできれば
どこでも問題なし!!!

なぜなら、

再利用=オブジェクト志向言語の醍醐味

だからね🕺

オブジェクト志向言語の醍醐味を分かっていない人に限って、不用意に豪語したりしてるから、気をつけてね
(*これが時たま書いてる「実務に入って何年経っても教科書どおりにしかコードが書けない人」に繋がる)

オイラはデザインの人で、

デザインの原則(①整理、②近接、③反復、④強調 + 足し算より引き算)

なんかを普段、コードや組み込みなんかをやる時も意識してるので、

  • 22章で使うなら22章のファイル内で書く

  • インポートしたフレームワーク、共通で使う部分(総論部)、個別で使う部分(構造体やビュー)なんかでやった方が管理がしやすい

ので、

個人的に、実際の構造体の前にクラスを置いた(追加した)だけの話

さらにコードがたまってきて、別のやり方の方が管理しやすいって判断したら、

同じプロジェクトファイル内のどこかに整理する

だけの話

👉コードの配置にこうしないといけないなんて決まりなんざない
=自分とか組織単位で管理しやすい方法は違うし、それに対応できるようにプラットフォーム(開発環境)は作ってる

しね👀それよりは、

ここでポイント②:コードの記法に気をつけよう

これは、このマガジンを始めた時からどこかで書こう、どこかで書こうって思っていたことなんだけど、

さっき書いたコード部分

//4節で追加
class Essentials22FruitsData : ObservableObject {
    @Published var fruitsName = ""
    @Published var fruitsNumber = 0
    
    init() {
        updateFruitsData()
    }
    
    func updateFruitsData(){
       fruitsNumber += 1
    }
}

//ビュー管理構造体
struct ListiOSApp17DevelopmentEssentialsCh22: Identifiable {
    var id: Int
    var title: String
    var view: ViewEnumiOSApp17DevelopmentEssentialsCh22
}

が分かりやすいので見てもらうとわかるとおり、

  • クラス名とか構造体名、プロトコル名 : 大文字始まりで書く(アッパーケース)

  • 関数名や引数(変数や定数) : 小文字始まりで書く(ロワーケース)

が、

SwfitであれRPA(UiPath)であれ基本

だから、そこはちゃんと意識しといてね。
(*日本語対応してるVBAやGoogleAppsScriptだと、そもそもクラス名なんかは日本語で書く人も実務では多いし普通なので、例外)

こー書くと、COBOLとかインフラ周りなんかのレガシーな開発に慣れた人たちから、

「ハンガリアン記法に基づいたスネーク記法にすべきだ!」

みたいな声も聞こえてきそうだけど、今時、わざわざアッパーケースでアンダーバーで単語単位に繋ぐなんてやると、さっきのコードでも

//4節で追加
class Essentials22FruitsData : ObservableObject {
    @Published var fruitsName = ""
    @Published var fruitsNumber = 0
    
    init() {
        updateFruitsData()
    }
    
    func updateFruitsData(){
       fruitsNumber += 1
    }
}
//4節で追加
class Essentials22_FruitsData : ObservableObject {
    @Published var Fruits_Name = ""
    @Published var Fruits_Number = 0
    
    init() {
        Update_Fruits_Data()
    }
    
    func Update_Fruits_Data(){
       Fruits_Number += 1
    }
}

どっちが読みやすいかって話だし、仮に後者が読みやすいとしても、

  • 自分が慣れてるからって、スネーク記法とキャメル記法が混在するコードが読みやすいか?

  • じゃあ、開発環境で設けてるデフォルトのObservableObjectみたいなプロトコル名はどうすんだ?

って話になってくるからね👀💦ま、これまでにオイラが色々書いたコードで、

  • 「なんでこれは小文字始まりなんだろう」

  • 「なんでここは大文字なんだろう」

って思った人もいたかも知れないけど、テケトーに書いてるように見えて、

実はちゃんとコードの書き方ひとつとってもちゃんと理由があって、
ちゃんと意識してやってる

ことだからね。

なんでもかんでも自由に出来なくはないけど、
何でもかんでも自由にするとコードの管理が大変になる
💦
👉書けないのではなく、書かない=型破りと型無しの違い

ま、ここら辺は、オイラがこれ以上ここで解説するよりも、

なんかを参考にしてみてね〜〜〜
さてと、本題に戻って、

クラスデータを使うビューを追加

struct Essentials22CombinedView: View {
    @ObservedObject var fruitsData: Essentials22FruitsData = Essentials22FruitsData(fruitsName:"🍎")
    var body: some View {
        HStack{
            Text("\(fruitsData.fruitsName)は、\(fruitsData.fruitsNumber)です")
            Button(action: {Essentials22FruitsData.updateFruitsData()
            },label: {
                Text("個数追加")
            })
        }
    }
}

を追加してみると

エラー発生
Argument passed to call that takes no arguments
引数をとらない呼び出しに引数が渡されました

って意味のわからないエラー

じゃ、本編のサンプルコードをまんま貼り付けて検証してみると、、、

class DemoData : ObservableObject {
    @Published var playerName = ""
    @Published var score = 0
    init() {
        updateData()
    }
    func updateData() {
        score += 1
    }
}
struct CContentView: View {
    @ObservedObject var demoData : DemoData = DemoData(player: "John")
    var body: some View {
        VStack {
            Text("\(demoData.playerName)'s Score = \(demoData.score)")
            Button(action: {
                demoData.updateData()
            }, label: {
                Text("Update")
            })
            .padding()
        }
    }
}
同じエラー発生じゃん笑🤣

サンプルコードがミスっちゃってますね〜〜〜〜
👉これSwiftを初めてやる人ででここまで勉強して買ったのに、これだと

詐欺

じゃんってレベルだね👀💦

とまあ、愚痴っても仕方ないので〜〜〜

そもそもAppleが公式でリリースしてるドキュメントで、Combineフレームワークを見ていくと

てな感じで色々書いてんだけど、これだとサンプルコードもないし分かりにくいので〜〜〜

なんかを参考に、、、

class Counter: ObservableObject {
    @Published var title = ""
    @Published var count = 0

}

struct CContentView: View {
    @StateObject private var counter = Counter()
    var body: some View {
        VStack {
            TextField(text: $counter.title) {
                Text("Counter Title")
            }
            Button {
                counter.count += 1
            } label: {
                Text("\(counter.count)")
            }
        }
    }
}

だと、

てな感じでイケることまではわかったから〜〜〜

次に、@ObservedObjectの公式ドキュメントを参考に〜〜〜

class DataModel: ObservableObject {
    @Published var name = "Some Name"
    @Published var isEnabled = false
}
struct MyView: View {
    @StateObject private var model = DataModel()
    var body: some View {
        Text(model.name)
        MySubView(model: model)
    }
}
struct MySubView: View {
    @ObservedObject var model: DataModel
    var body: some View {
        Toggle("Enabled", isOn: $model.isEnabled)
    }
}

で見てみても、

てな感じで状態の変化を引き継いでるだけで、
Textビューの中身自体も打ち方も違うのがわかるね👀💦
👉本のサンプルが如何に悪いかがわかる💦

ここでポイント③:必ず動かして検証する

これはここまでの記事で何度も言ってることだけど、

市販で日本なり世界なりで発行されてる本ですら、この状態👀💦

これが

入門書とかSwiftどころかプログラミングを初めてやる人向け
本ではなく、実際の製品であったら?
👉阿鼻叫喚と非難轟々の嵐=金返せって感じだよね〜〜〜

そこで、上記までを参考にちゃんと動くサンプルを個人的に作ってみた

//4節で追加
//データモデルを管理するクラス
class Essentials22FruitsData: ObservableObject {
    @Published var title = ""
    @Published var fruitsAmount = 0
    @Published var isEnabled = false
}

//4〜5節で追加
//クラスをやり取りして、実際の操作を行うビュー構造体
//4〜6節で追加
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)
        }
    }
}

では動かしませう

てな感じ

@Observableオブジェクトでやってみると、

//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()
        }
    }
}

ここのコードはほぼ本編のまんまで〜〜〜
動かしてみると、

新たに追加した+ボタンを押してみると〜〜
うん、ここは普通に動いたね

*おそらく、こんなお粗末なことになっている原因としては、

  • Observableオブジェクトの方を伝えたかったので、先に作った(6節のコード)

  • Combineのサンプルもいるなと後から追記(3〜5節)

  • 原理的には、プロパティラッパーを変えるだけでイケるはずだから、最低限のコードのみを変更(4節の問題のコード)

  • ちゃんと動くかどうかの検証どころかXcodeに書いて試すことすらしなかった👈これをやっておけば、コード自体が今の環境でバグってることに気づけたはず

てな感じだろうね。

てゆーてるが、

  1. 基本的に情報が新しい

  2. 全体的にはちゃんと動くコードが圧倒的に多い

  3. 章とか節ごとに独立したコードが多いので、作り込ませた挙句に最後の最後で動かないみたいな構成になっていない

などなど、過去に出ていた日本国内の市販本に比べたら、

圧倒的に良い本なんだよね🧐

さてと、今日も長くなってきたので、一旦以上だね🙇‍♂️

今回のコードまとめ

//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()
        }
    }
}

Apple公式

さてと、次回は

@Observationの応用と(プロジェクト環境全体で管理させる)@Environmentオブジェクト

についてやってく〜〜〜🕺

大分話がすでにややこしいと思ってる人もいるかも知れないけど、

  • 次章以降でやるモディファイアなんかではこの章の状態変数の取り扱いが非常に絡んでくる

  • 👉この知識に触れておくことが、アプリ開発の前提になる

ので、残り少しだし、後少しお付き合いを〜〜〜🕺
(てか、一番肝心なところのサンプルコードが薄い+提示してるコードも実は間違ってますだと、非プログラミング経験者だと中々ここで心折れるだろうし、折れない方が逞しいレベルで折れる人が普通だと思う🤔)

記事公開後、

さてと、いつもどおり

を参考に〜〜〜

てな
てな
感じで〜
ハイ、いつもの出来上がり〜〜

サンプルコード

◾️Essentials22.swift

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 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 pointEssentials22_1 = """
インターフェース上でデータを取り扱うプロパティでSwiftUIで用意されてるオプションは主に4つで、それらはすべて
・状態
・監視
・環境
を制御しながら
インターフェースの表示や動作の状態を管理するもの

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

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

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

これが入門書とかSwiftどころかプログラミングを初めてやる人向け
本ではなく、実際の製品であったら?
👉阿鼻叫喚と非難轟々の嵐=金返せって感じだよね〜〜〜
"""
//URL
let urlEssentials22_1 = "https://note.com/m_kakudo/n/ne0c79a7ef90d"
let urlEssentials22_2 = "https://note.com/m_kakudo/n/ndce829c36ef5"

//ビュー管理構造体
struct ListiOSApp17DevelopmentEssentialsCh22: Identifiable {
    var id: Int
    var title: String
    var view: ViewEnumiOSApp17DevelopmentEssentialsCh22
}
//遷移先の画面を格納する列挙型
enum ViewEnumiOSApp17DevelopmentEssentialsCh22{
    case Sec1
    case Sec2
}
//各項目に表示するリスト項目
let dataiOSApp17DevelopmentEssentialsCh22: [ListiOSApp17DevelopmentEssentialsCh22] = [
    ListiOSApp17DevelopmentEssentialsCh22(id: 1, title: essentialsChapter22_1SubTitle, view: .Sec1),
    ListiOSApp17DevelopmentEssentialsCh22(id: 2, title: essentialsChapter22_2SubTitle, view: .Sec2)
]
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)
            })
        }
    }
}
#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_1WebView(searchURL: url)
    }
}
#Preview {
    Essentials22_2WEB()
}

本編で書いてたとおり、章の中の節が分かれると、こっちの方が管理しやすかったので、クラスの位置とかletの位置とかをこちらに変更した〜〜〜
でも、

オブジェクト志向言語だから動くこともわかるでしょ👀💦

以上。ではまた次回🕺

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