見出し画像

【じっくりSw1ftUI46】実践編16〜第28章  AppStorage と SceneStorage を使用した SwiftUI データの永続化

さてと、前回

Environmentプロパティ

の簡単な使い方の紹介はやったので〜〜〜〜今回は、

テキストフィールドなんかに入力した値を永続化させる初歩
👉データ管理の入り口

に入ってく🕺

毎度のことだが、オイラの学びなんか要らないって人は、

でまんまサンプルコードも載ってそうだから好きにしたらいいんじゃね👀💦
さてと、んだば早速


じっくり第28章を読んでく👓

概要

  • @AppStorage

  • @SceneStorage

てゆー

プロパティラッパー

を使えってゆーてるみたいだね。
*プロパティラッパーについて、振り返りたいって人は

ですでに記事にしてるからそっちも参照してや〜〜〜

@SceneStorage

うだうだ書くよりも動かした方が早いので、

import SwiftUI

struct Essentials28: View {
    var body: some View {
        Essentials28ContentsView()
    }
}

struct Essentials28ContentsView: View {
    
    //@SceneStorage
    @SceneStorage("menu") var menu:String = ""
    //@AppStorage
    @AppStorage("fruits") var fruits: String = ""
    
    var body: some View {
        ScrollView{
            VStack{
                TextField("",text: $menu)
                    .background(Color.orange)
            }
        }
    }
}

#Preview {
    Essentials28ContentsView()
}

てな感じのコードだと、AppStorageがUserDefaultに追加されるだけみたいなんで〜〜〜書かれているとおりにApp Groupsを

てな感じでテケトーに追加してあげて
import SwiftUI

struct Essentials28: View {
    var body: some View {
        Essentials28ContentsView()
    }
}

struct Essentials28ContentsView: View {
    //@SceneStorage
    @SceneStorage("menu") var menu:String = ""
    //@AppStorage
    @AppStorage("fruits",store: UserDefaults(suiteName: "group.iOS17.M_Kaku")) var fruits: String = ""
    
    var body: some View {
        ScrollView{
            VStack{
                TextField("項目を入力してください",text: $menu)
                TextField("果物名を入力してください",text: $fruits)
            }
        }
    }
}

#Preview {
    Essentials28ContentsView()
}

てな感じのコードで動かすと、

てな感じになるので文字を入力〜〜〜
文字を入力〜〜〜
プレビューを今まで作った他のビューに切り替えて戻すと〜〜〜
プレビューだと
AppGroupsにはめ込んだAppStorageの物しか残ってないね👀

面白いのでもう一個増やして〜〜〜

import SwiftUI

struct Essentials28: View {
    var body: some View {
        Essentials28ContentsView()
    }
}

struct Essentials28ContentsView: View {
    //@SceneStorage
    @SceneStorage("menu") var menu:String = ""
    //@AppStorage:App Groups
    @AppStorage("fruits",store: UserDefaults(suiteName: "group.iOS17.M_Kaku")) var fruits: String = ""
    //@AppStorage:UserDafaults
    @AppStorage("basket") var basket: String = ""
    
    var body: some View {
        ScrollView{
            VStack{
                TextField("項目を入力してください",text: $menu)
                TextField("果物名を入力してください",text: $fruits)
                TextField("かごを入力してください",text: $basket)
            }
        }
    }
}

#Preview {
    Essentials28ContentsView()
}

動かすと

入力後

プレビューを再表示しても〜〜〜

てな感じで

ここでポイント①:プレビューでも永続的にデータが保存されてるか

を確認したいなら、

@AppStorageを使ってUserDafaultsかApp Groupsに格納した方がいい

ってのが分かるね。

オイラの個人的な経験と感想だけど、

安全に丁寧に仕事をする職人さんで、ビュー単体の確認もせずに、いきなりシミュレータで実行して初めてデータが格納されてるって人を見たことも聞いたこともないので、

少なくともAppStorageでやった方が安全

ってのは分かるよね🧐
で次から

Storgeのデモに入ろう

って感じなんで〜〜〜

import SwiftUI

struct Essentials28ContentsView: View {
    var body: some View {
        VStack{
            TabView{
                Essentials28StandardView()
                    .tabItem {
                        Image(systemName: "sun.max")
                        Text("基本")
                    }
                E28SceneStorageView()
                    .tabItem {
                        Image(systemName: "cloud")
                        Text("SceneStorage")
                    }
                E28AppStorageAppGroupsView()
                    .tabItem {
                        Image(systemName: "cloud.rain")
                        Text("AppGroups")
                    }
                E28AppStorageUserDefaultsView()
                    .tabItem {
                        Image(systemName: "apple.logo")
                        Text("UserDefaults")
                    }
            }
        }
    }
}

struct Essentials28StandardView: View {
    //@SceneStorage
    @SceneStorage("menu") var menu:String = ""
    //@AppStorage:App Groups
    @AppStorage("fruits",store: UserDefaults(suiteName: "group.iOS17.M_Kaku")) var fruits: String = ""
    //@AppStorage:UserDafaults
    @AppStorage("basket") var basket: String = ""
    
    var body: some View {
        ScrollView{
            VStack{
                TextField("項目を入力してください",text: $menu)
                TextField("果物名を入力してください",text: $fruits)
                TextField("かごを入力してください",text: $basket)
            }
        }
    }
}

struct E28SceneStorageView: View {
    @SceneStorage("menu") private var menu: String = ""
    var body: some View {
        VStack{
            TextField("項目を入力",text: $menu)
                .padding(35)
                .font(.largeTitle)
        }
    }
}

struct E28AppStorageAppGroupsView: View {
    @AppStorage("fruits",store: UserDefaults(suiteName: "group.iOS17.M_Kaku")) private var fruits: String = ""
    var body: some View {
        VStack{
            TextField("項目を入力",text: $fruits)
                .padding(35)
                .font(.largeTitle)
        }
    }
}

struct E28AppStorageUserDefaultsView: View {
    @AppStorage("basket") private var basket: String = ""
    var body: some View {
        VStack{
            TextField("項目を入力",text: $basket)
                .padding(35)
                .font(.largeTitle)
        }
    }
}
#Preview {
    Essentials28ContentsView()
}

てな感じでさらに書き換えて動かすと〜〜〜

基本タブにさっきまでのは表示されるようにして〜〜〜
SceneStorageにはさっきまで何も入ってなかったので〜〜〜
りんごを入力して、基本タブをタップ
タブ間の間=複数ビューの間は引き継ぎ出来てますね👀💦

前回までにやった他のビューに移動=このビューを一旦廃棄

して、再表示させると、

相変わらず
どちらからも消えてんね
りんごはどこに消えた、、、
🐭🧀🤣

で他も動かしてみると〜〜〜

さっき入力した🍇はここでも引き継がれてるね
👉AppGroupsで永続データとして管理してるものを呼び出してるだけ
=当たり前😆
てな感じで書き換えて
基本タブにも反映された〜〜〜
UserDefaultsも、引き継げてるので〜〜〜
て感じに書き換えて〜〜〜
基本タブでも確認できた🕺
ビューを再表示後も同じだねえ🧐

ここからは話の毛色が変わってくるので〜〜〜

ここまでのコード

import SwiftUI

struct Essentials28ContentsView: View {
    var body: some View {
        VStack{
            TabView{
                Essentials28StandardView()
                    .tabItem {
                        Image(systemName: "sun.max")
                        Text("基本")
                    }
                E28SceneStorageView()
                    .tabItem {
                        Image(systemName: "cloud")
                        Text("SceneStorage")
                    }
                E28AppStorageAppGroupsView()
                    .tabItem {
                        Image(systemName: "cloud.rain")
                        Text("AppGroups")
                    }
                E28AppStorageUserDefaultsView()
                    .tabItem {
                        Image(systemName: "apple.logo")
                        Text("UserDefaults")
                    }
            }
        }
    }
}

struct Essentials28StandardView: View {
    //@SceneStorage
    @SceneStorage("menu") var menu:String = ""
    //@AppStorage:App Groups
    @AppStorage("fruits",store: UserDefaults(suiteName: "group.iOS17.M_Kaku")) var fruits: String = ""
    //@AppStorage:UserDafaults
    @AppStorage("basket") var basket: String = ""

    var body: some View {
        ScrollView{
            VStack{
                TextField("項目を入力してください",text: $menu)
                TextField("果物名を入力してください",text: $fruits)
                TextField("かごを入力してください",text: $basket)
            }
        }
    }
}

struct E28SceneStorageView: View {
    @SceneStorage("menu") private var menu: String = ""
    var body: some View {
        VStack{
            TextField("項目を入力",text: $menu)
                .padding(35)
                .font(.largeTitle)
        }
    }
}

struct E28AppStorageAppGroupsView: View {
    @AppStorage("fruits",store: UserDefaults(suiteName: "group.iOS17.M_Kaku")) private var fruits: String = ""
    var body: some View {
        VStack{
            TextField("項目を入力",text: $fruits)
                .padding(35)
                .font(.largeTitle)
        }
    }
}

struct E28AppStorageUserDefaultsView: View {
    @AppStorage("basket") private var basket: String = ""
    var body: some View {
        VStack{
            TextField("項目を入力",text: $basket)
                .padding(35)
                .font(.largeTitle)
        }
    }
}
#Preview {
    Essentials28ContentsView()
}

カスタマイズされたデータの格納

と、ここでなんか物凄い大事なこと書いてんね🤣

@AppStorage および @SceneStorageに保存できるのは、特定の型の値のみ

具体的には、以下の6つ

  1. Bool

  2. Int

  3. Double

  4. String

  5. URL

  6. Data

んで、他のデータ型を使いたい時なんだけど

なぜか知らないけど、ここも他の技術書のご多分に漏れず、

分かってる前提で全体コードを端折り過ぎてるだけのサンプル

になっているので、

何をしたいかの概念は分かってもどこに書き込めばいいかが見えないソースコードになってんね👀💦

例えば、構造体で定義した値を取り扱うとき

struct CustomerInfo {
    var CustomerFirstName: String
    var CustomerLastName: String
}

て感じで

はめ込むとエラー👀💦
イニシャライザに一致しないってエラーで致命的なエラーみたいだね👀

まずは、

struct CustomerInfo: Encodable,Decodable {
    var CustomerFirstName: String
    var CustomerLastName: String
}

でエンコードとデコードのプロトコルを構造体にセットして〜〜〜

//カスタムデータ
    @AppStorage("customInfo") var customerData = Data()

で書き換えてあげて

まずはエラー解消笑😆

で、まだはっきしゆーて具体的には名前を呼び出しすらできてないので〜〜〜

のコードを実際にはめ込んでまずは検証

構造体を追加
変数名を追加

ま、ここまではさっきと同様にエラーが起きないね

同じエラーが起きたね
プロトコルを追記したコード
プロパティラッパー付きに書き換え
はめ込んだけど、そもそも

エンコード対象のusernameって、
AppStorageのキーワードだから他の変数名じゃないとおかしくないか?
誤ってそうじゃね?👀💦

以降のサンプルコードも全てそのまま、はめ込んでみたけど、
そもそもおかしなところを直しても、てな感じにになってっし

念の為、場所的におかしいのは承知でif文以降を全て、

ってやってみたけど、そらそーなるってエラーになってる

つまり、このコード自体が、

  • きちんと動作検証もせずに、動かしてる

  • iOS16以前のサンプルコードをそのまま流用してるだけ

👉使い物にならない

ってのが分かったねえ🧐

なので〜〜〜

なんかを参考に〜〜〜

struct E28CustomInfo: Codable, Identifiable {
    var id = UUID() // 一意の識別子を提供
    let customerName: String //顧客名
    let customerNumber: Int //顧客番号
}

struct E28CustomeDataView: View {
    private let arrayKey = "顧客情報保存"
    @State private var savedCustomer: [E28CustomInfo] = []
    @State private var customerName: String = ""
    @State private var customerNo: String = ""
    init() {
        _savedCustomer = State(initialValue: loadCustomerInfo())
    }
    var body: some View {
        VStack {
            HStack {
                TextField("顧客No", text: $customerNo)
                    .padding()
                    .keyboardType(.numberPad)
                TextField("顧客名", text: $customerName)
                    .padding()

            }
            Button("顧客情報追加") {
                addCustomer()
            }
            .padding()
            List {
                ForEach(savedCustomer) { customer in
                    VStack(alignment: .leading) {
                        Text("顧客番号:\(customer.customerNumber)")
                        Text(customer.customerName)
                            .foregroundColor(.gray)
                    }
                }
                .onDelete(perform: deleteCustomer)
            }
            .padding()
            Spacer()
        }
        .padding()
    }
    private func loadCustomerInfo() -> [E28CustomInfo] {
        if let data = UserDefaults.standard.data(forKey: arrayKey),
           let products = try? JSONDecoder().decode([E28CustomInfo].self, from: data) {
            return products
        }
        return []
    }
    private func addCustomer() {
        if let number = Int(customerNo) {
            let newCustomer = E28CustomInfo(customerName: customerName, customerNumber: number)
            savedCustomer.append(newCustomer)
            saveCustomer()
            customerName = ""
            customerNo = ""
        }
    }
    private func deleteCustomer(at offsets: IndexSet) {
        savedCustomer.remove(atOffsets: offsets)
        saveCustomer()
    }
    private func saveCustomer() {
        if let encodedData = try? JSONEncoder().encode(savedCustomer) {
            UserDefaults.standard.set(encodedData, forKey: arrayKey)
        }
    }
}

で、エンコード、デコードもしっかり入った上で。。。

てな感じで
顧客番号と顧客名を入力してみよう
顧客情報追加ボタンをタップ
リストに表示された〜〜〜

ちょっと色々気に入らないので〜〜〜

struct E28CustomInfo: Codable, Identifiable {
    var id = UUID() // 一意の識別子を提供
    let customerName: String //顧客名
    let customerNumber: Int //顧客番号
}

struct E28CustomeDataView: View {
    private let arrayKey = "顧客情報保存"
    @State private var savedCustomer: [E28CustomInfo] = []
    @State private var customerName: String = ""
    @State private var customerNo: String = ""
    init() {
        _savedCustomer = State(initialValue: loadCustomerInfo())
    }
    var body: some View {
        VStack {
            HStack {
                TextField("ID", text: $customerNo)
                    .padding()
                    .keyboardType(.numberPad)
                TextField("なまえ", text: $customerName)
                    .padding()

            }
            Button("顧客情報追加") {
                addCustomer()
            }
            .padding()
            List {
                ForEach(savedCustomer) { customer in
                    HStack{
                        Text("No.\(customer.customerNumber):")
                            .foregroundColor(.gray)
                        Text(customer.customerName)
                    }
                }
                .onDelete(perform: deleteCustomer)
            }
            .padding()
            Spacer()
        }
        .padding()
    }
    private func loadCustomerInfo() -> [E28CustomInfo] {
        if let data = UserDefaults.standard.data(forKey: arrayKey),
           let products = try? JSONDecoder().decode([E28CustomInfo].self, from: data) {
            return products
        }
        return []
    }
    private func addCustomer() {
        if let number = Int(customerNo) {
            let newCustomer = E28CustomInfo(customerName: customerName, customerNumber: number)
            savedCustomer.append(newCustomer)
            saveCustomer()
            customerName = ""
            customerNo = ""
        }
    }
    private func deleteCustomer(at offsets: IndexSet) {
        savedCustomer.remove(atOffsets: offsets)
        saveCustomer()
    }
    private func saveCustomer() {
        if let encodedData = try? JSONEncoder().encode(savedCustomer) {
            UserDefaults.standard.set(encodedData, forKey: arrayKey)
        }
    }
}

てな感じにして

レイアウトもそれなりになったので〜〜〜
追加されたリスト項目を左にスワイプして
出てくるDeleteをタップ
削除までできた〜〜〜

今回のコード(まとめ)

import SwiftUI

struct Essentials28ContentsView: View {
    var body: some View {
        VStack{
            TabView{
                Essentials28StandardView()
                    .tabItem {
                        Image(systemName: "sun.max")
                        Text("基本")
                    }
                E28SceneStorageView()
                    .tabItem {
                        Image(systemName: "cloud")
                        Text("SceneStorage")
                    }
                E28AppStorageAppGroupsView()
                    .tabItem {
                        Image(systemName: "cloud.rain")
                        Text("AppGroups")
                    }
                E28AppStorageUserDefaultsView()
                    .tabItem {
                        Image(systemName: "apple.logo")
                        Text("UserDefaults")
                    }
                E28CustomeDataView()
                    .tabItem {
                        Image(systemName: "note")
                        Text("カスタムリスト")
                    }
            }
        }
    }
}

struct Essentials28StandardView: View {
    //@SceneStorage
    @SceneStorage("menu") var menu:String = ""
    //@AppStorage:App Groups
    @AppStorage("fruits",store: UserDefaults(suiteName: "group.iOS17.M_Kaku")) var fruits: String = ""
    //@AppStorage:UserDafaults
    @AppStorage("basket") var basket: String = ""
    var body: some View {
        
        ScrollView{
            VStack{
                TextField("項目を入力してください",text: $menu)
                TextField("果物名を入力してください",text: $fruits)
                TextField("かごを入力してください",text: $basket)
            }
        }
    }
}

struct E28SceneStorageView: View {
    @SceneStorage("menu") private var menu: String = ""
    var body: some View {
        VStack{
            TextField("項目を入力",text: $menu)
                .padding(35)
                .font(.largeTitle)
        }
    }
}

struct E28AppStorageAppGroupsView: View {
    @AppStorage("fruits",store: UserDefaults(suiteName: "group.iOS17.M_Kaku")) private var fruits: String = ""
    var body: some View {
        VStack{
            TextField("項目を入力",text: $fruits)
                .padding(35)
                .font(.largeTitle)
        }
    }
}

struct E28AppStorageUserDefaultsView: View {
    @AppStorage("basket") private var basket: String = ""
    var body: some View {
        VStack{
            TextField("項目を入力",text: $basket)
                .padding(35)
                .font(.largeTitle)
        }
    }
}
#Preview {
    Essentials28ContentsView()
}

struct E28CustomInfo: Codable, Identifiable {
    var id = UUID() // 一意の識別子を提供
    let customerName: String //顧客名
    let customerNumber: Int //顧客番号
}

struct E28CustomeDataView: View {
    private let arrayKey = "顧客情報保存"
    @State private var savedCustomer: [E28CustomInfo] = []
    @State private var customerName: String = ""
    @State private var customerNo: String = ""
    init() {
        _savedCustomer = State(initialValue: loadCustomerInfo())
    }
    var body: some View {
        VStack {
            HStack {
                TextField("ID", text: $customerNo)
                    .padding()
                    .keyboardType(.numberPad)
                TextField("なまえ", text: $customerName)
                    .padding()

            }
            Button("顧客情報追加") {
                addCustomer()
            }
            .padding()
            List {
                ForEach(savedCustomer) { customer in
                    HStack{
                        Text("No.\(customer.customerNumber):")
                            .foregroundColor(.gray)
                        Text(customer.customerName)
                    }
                }
                .onDelete(perform: deleteCustomer)
            }
            .padding()
            Spacer()
        }
        .padding()
    }
    private func loadCustomerInfo() -> [E28CustomInfo] {
        if let data = UserDefaults.standard.data(forKey: arrayKey),
           let products = try? JSONDecoder().decode([E28CustomInfo].self, from: data) {
            return products
        }
        return []
    }
    private func addCustomer() {
        if let number = Int(customerNo) {
            let newCustomer = E28CustomInfo(customerName: customerName, customerNumber: number)
            savedCustomer.append(newCustomer)
            saveCustomer()
            customerName = ""
            customerNo = ""
        }
    }
    private func deleteCustomer(at offsets: IndexSet) {
        savedCustomer.remove(atOffsets: offsets)
        saveCustomer()
    }
    private func saveCustomer() {
        if let encodedData = try? JSONEncoder().encode(savedCustomer) {
            UserDefaults.standard.set(encodedData, forKey: arrayKey)
        }
    }
}

Apple公式

さてと、次回は

ここ最近のデータとか環境みたいなコードとかビューの裏側の話と打って変わって、ビューのデザインについて入ってく

第29章 SwiftUI スタック配置と配置ガイド

に入ってく。ここ最近やった

プロパティラッパーによるデータのやり取り
入力なんかをしたデータ永続化の手法

がわかっていないと、どんだけビューを綺麗に作っても、操作一回きりで終わってしまうアプリしか作れないからね🧐

じゃ、また次回🕺

記事公開後、

いつもどおり、

でやった操作を〜〜〜

てな
てな
てなで
ハイ、でけた〜〜〜🕺

サンプルコード

◾️Essentials28.swift

import SwiftUI
import WebKit

//タイトル
let essentialsChapter28NavigationTitle = "第28章"
let essentialsChapter28Title = "第28章 AppStorageとSceneStorageを使用した SwiftUIデータの永続化"
let essentialsChapter28SubTitle = "第1節 AppStorageとSceneStorageを使用した SwiftUIデータの永続化"

//コード
let codeEssentials28 = """
struct Essentials28ContensView: View {
    var body: some View {
        VStack{
            TabView{
                Essentials28StandardView()
                    .tabItem {
                        Image(systemName: "sun.max")
                        Text("基本")
                    }
                E28SceneStorageView()
                    .tabItem {
                        Image(systemName: "cloud")
                        Text("SceneStorage")
                    }
                E28AppStorageAppGroupsView()
                    .tabItem {
                        Image(systemName: "cloud.rain")
                        Text("AppGroups")
                    }
                E28AppStorageUserDefaultsView()
                    .tabItem {
                        Image(systemName: "apple.logo")
                        Text("UserDefaults")
                    }
                E28CustomeDataView()
                    .tabItem {
                        Image(systemName: "note")
                        Text("カスタムリスト")
                    }
            }
        }
    }
}

struct Essentials28StandardView: View {
    //@SceneStorage
    @SceneStorage("menu") var menu:String = ""
    //@AppStorage:App Groups
    @AppStorage("fruits",store: UserDefaults(suiteName: "group.iOS17.M_Kaku")) var fruits: String = ""
    //@AppStorage:UserDafaults
    @AppStorage("basket") var basket: String = ""
    var body: some View {
        
        ScrollView{
            VStack{
                TextField("項目を入力してください",text: $menu)
                TextField("果物名を入力してください",text: $fruits)
                TextField("かごを入力してください",text: $basket)
            }
        }
    }
}

struct E28SceneStorageView: View {
    @SceneStorage("menu") private var menu: String = ""
    var body: some View {
        VStack{
            TextField("項目を入力",text: $menu)
                .padding(35)
                .font(.largeTitle)
        }
    }
}

struct E28AppStorageAppGroupsView: View {
    @AppStorage("fruits",store: UserDefaults(suiteName: "group.iOS17.M_Kaku")) private var fruits: String = ""
    var body: some View {
        VStack{
            TextField("項目を入力",text: $fruits)
                .padding(35)
                .font(.largeTitle)
        }
    }
}

struct E28AppStorageUserDefaultsView: View {
    @AppStorage("basket") private var basket: String = ""
    var body: some View {
        VStack{
            TextField("項目を入力",text: $basket)
                .padding(35)
                .font(.largeTitle)
        }
    }
}
#Preview {
    Essentials28ContensView()
}

struct E28CustomInfo: Codable, Identifiable {
    var id = UUID() // 一意の識別子を提供
    let customerName: String //顧客名
    let customerNumber: Int //顧客番号
}

struct E28CustomeDataView: View {
    private let arrayKey = "顧客情報保存"
    @State private var savedCustomer: [E28CustomInfo] = []
    @State private var customerName: String = ""
    @State private var customerNo: String = ""
    init() {
        _savedCustomer = State(initialValue: loadCustomerInfo())
    }
    var body: some View {
        VStack {
            HStack {
                TextField("ID", text: $customerNo)
                    .padding()
                    .keyboardType(.numberPad)
                TextField("なまえ", text: $customerName)
                    .padding()

            }
            Button("顧客情報追加") {
                addCustomer()
            }
            .padding()
            List {
                ForEach(savedCustomer) { customer in
                    HStack{
                        Text("No.\\(customer.customerNumber):")
                            .foregroundColor(.gray)
                        Text(customer.customerName)
                    }
                }
                .onDelete(perform: deleteCustomer)
            }
            .padding()
            Spacer()
        }
        .padding()
    }
    private func loadCustomerInfo() -> [E28CustomInfo] {
        if let data = UserDefaults.standard.data(forKey: arrayKey),
           let products = try? JSONDecoder().decode([E28CustomInfo].self, from: data) {
            return products
        }
        return []
    }
    private func addCustomer() {
        if let number = Int(customerNo) {
            let newCustomer = E28CustomInfo(customerName: customerName, customerNumber: number)
            savedCustomer.append(newCustomer)
            saveCustomer()
            customerName = ""
            customerNo = ""
        }
    }
    private func deleteCustomer(at offsets: IndexSet) {
        savedCustomer.remove(atOffsets: offsets)
        saveCustomer()
    }
    private func saveCustomer() {
        if let encodedData = try? JSONEncoder().encode(savedCustomer) {
            UserDefaults.standard.set(encodedData, forKey: arrayKey)
        }
    }
}
"""

//ポイント
let pointEssentials28 = """
プレビューでも永続的にデータが保存されてるか
を確認したいなら、
@AppStorageを使ってUserDafaultsかApp Groupsに格納した方がいい
ってのが分かるね。
オイラの個人的な経験と感想だけど、
安全に丁寧に仕事をする職人さんで、ビュー単体の確認もせずに、いきなりシミュレータで実行して初めてデータが格納されてるって人を見たことも聞いたこともないので、
少なくともAppStorageでやった方が安全
ってのは分かるよね🧐

@AppStorageおよび@SceneStorageに保存できるのは、特定の型の値のみ。具体的には、以下の6つ
◻︎Bool
◻︎Int
◻︎Double
◻︎String
◻︎URL
◻︎Data
以外を扱うときはカスタムデータ用の処理を追加する必要あり
"""
//URL
let urlEssentials28 = "https://note.com/m_kakudo/n/n1001be3acd76"

//ビュー管理構造体
struct ListiOSApp17DevelopmentEssentialsCh28: Identifiable {
    var id: Int
    var title: String
    var view: ViewEnumiOSApp17DevelopmentEssentialsCh28
}
//遷移先の画面を格納する列挙型
enum ViewEnumiOSApp17DevelopmentEssentialsCh28{
    case Sec1
}
//各項目に表示するリスト項目
let dataiOSApp17DevelopmentEssentialsCh28: [ListiOSApp17DevelopmentEssentialsCh28] = [
    ListiOSApp17DevelopmentEssentialsCh28(id: 1, title: essentialsChapter28SubTitle, view: .Sec1),
]
struct iOSApp17DevelopmentEssentialsCh28: View {
    var body: some View {
        VStack {
            Divider()
            List (dataiOSApp17DevelopmentEssentialsCh28) { data in
                self.containedViewiOSApp17DevelopmentEssentialsCh28(dataiOSApp17DevelopmentEssentialsCh28: data)
            }
            .edgesIgnoringSafeArea([.bottom])
        }
        .navigationTitle(essentialsChapter28NavigationTitle)
        .navigationBarTitleDisplayMode(.inline)
    }
    //タップ後に遷移先へ遷移させる関数
    func containedViewiOSApp17DevelopmentEssentialsCh28(dataiOSApp17DevelopmentEssentialsCh28: ListiOSApp17DevelopmentEssentialsCh28) -> AnyView {
        switch dataiOSApp17DevelopmentEssentialsCh28.view {
        case .Sec1:
            return AnyView(NavigationLink (destination: Essentials28()) {
                Text(dataiOSApp17DevelopmentEssentialsCh28.title)
            })
        }
    }
}
#Preview {
    iOSApp17DevelopmentEssentialsCh28()
}

struct Essentials28: View {
    var body: some View {
        VStack{
            TabView {
                Essentials28ContensView()
                    .tabItem {
                        Image(systemName: contentsImageTab)
                        Text(contentsTextTab)
                    }
                Essentials28Code()
                    .tabItem {
                        Image(systemName: codeImageTab)
                        Text(codeTextTab)
                    }
                Essentials28Points()
                    .tabItem {
                        Image(systemName: pointImageTab)
                        Text(pointTextTab)
                    }
                Essentials28WEB()
                    .tabItem {
                        Image(systemName: webImageTab)
                        Text(webTextTab)
                    }
            }
        }
    }
}
#Preview {
    Essentials28()
}

struct Essentials28Code: View {
    var body: some View {
        ScrollView{
            Text(codeEssentials28)
        }
    }
}
#Preview {
    Essentials28Code()
}
struct Essentials28Points: View {
    var body: some View {
        ScrollView{
            Text(pointEssentials28)
        }
    }
}
#Preview {
    Essentials28Points()
}
struct Essentials28WebView: 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 Essentials28WEB: View {
    private var url:URL = URL(string: urlEssentials28)!
    var body: some View {Essentials28WebView(searchURL: url)
    }
}
#Preview {
    Essentials28WEB()
}

struct Essentials28ContensView: View {
    var body: some View {
        VStack{
            TabView{
                Essentials28StandardView()
                    .tabItem {
                        Image(systemName: "sun.max")
                        Text("基本")
                    }
                E28SceneStorageView()
                    .tabItem {
                        Image(systemName: "cloud")
                        Text("SceneStorage")
                    }
                E28AppStorageAppGroupsView()
                    .tabItem {
                        Image(systemName: "cloud.rain")
                        Text("AppGroups")
                    }
                E28AppStorageUserDefaultsView()
                    .tabItem {
                        Image(systemName: "apple.logo")
                        Text("UserDefaults")
                    }
                E28CustomeDataView()
                    .tabItem {
                        Image(systemName: "note")
                        Text("カスタムリスト")
                    }
            }
        }
    }
}

struct Essentials28StandardView: View {
    //@SceneStorage
    @SceneStorage("menu") var menu:String = ""
    //@AppStorage:App Groups
    @AppStorage("fruits",store: UserDefaults(suiteName: "group.iOS17.M_Kaku")) var fruits: String = ""
    //@AppStorage:UserDafaults
    @AppStorage("basket") var basket: String = ""
    var body: some View {
        
        ScrollView{
            VStack{
                TextField("項目を入力してください",text: $menu)
                TextField("果物名を入力してください",text: $fruits)
                TextField("かごを入力してください",text: $basket)
            }
        }
    }
}

struct E28SceneStorageView: View {
    @SceneStorage("menu") private var menu: String = ""
    var body: some View {
        VStack{
            TextField("項目を入力",text: $menu)
                .padding(35)
                .font(.largeTitle)
        }
    }
}

struct E28AppStorageAppGroupsView: View {
    @AppStorage("fruits",store: UserDefaults(suiteName: "group.iOS17.M_Kaku")) private var fruits: String = ""
    var body: some View {
        VStack{
            TextField("項目を入力",text: $fruits)
                .padding(35)
                .font(.largeTitle)
        }
    }
}

struct E28AppStorageUserDefaultsView: View {
    @AppStorage("basket") private var basket: String = ""
    var body: some View {
        VStack{
            TextField("項目を入力",text: $basket)
                .padding(35)
                .font(.largeTitle)
        }
    }
}
#Preview {
    Essentials28ContensView()
}

struct E28CustomInfo: Codable, Identifiable {
    var id = UUID() // 一意の識別子を提供
    let customerName: String //顧客名
    let customerNumber: Int //顧客番号
}

struct E28CustomeDataView: View {
    private let arrayKey = "顧客情報保存"
    @State private var savedCustomer: [E28CustomInfo] = []
    @State private var customerName: String = ""
    @State private var customerNo: String = ""
    init() {
        _savedCustomer = State(initialValue: loadCustomerInfo())
    }
    var body: some View {
        VStack {
            HStack {
                TextField("ID", text: $customerNo)
                    .padding()
                    .keyboardType(.numberPad)
                TextField("なまえ", text: $customerName)
                    .padding()

            }
            Button("顧客情報追加") {
                addCustomer()
            }
            .padding()
            List {
                ForEach(savedCustomer) { customer in
                    HStack{
                        Text("No.\(customer.customerNumber):")
                            .foregroundColor(.gray)
                        Text(customer.customerName)
                    }
                }
                .onDelete(perform: deleteCustomer)
            }
            .padding()
            Spacer()
        }
        .padding()
    }
    private func loadCustomerInfo() -> [E28CustomInfo] {
        if let data = UserDefaults.standard.data(forKey: arrayKey),
           let products = try? JSONDecoder().decode([E28CustomInfo].self, from: data) {
            return products
        }
        return []
    }
    private func addCustomer() {
        if let number = Int(customerNo) {
            let newCustomer = E28CustomInfo(customerName: customerName, customerNumber: number)
            savedCustomer.append(newCustomer)
            saveCustomer()
            customerName = ""
            customerNo = ""
        }
    }
    private func deleteCustomer(at offsets: IndexSet) {
        savedCustomer.remove(atOffsets: offsets)
        saveCustomer()
    }
    private func saveCustomer() {
        if let encodedData = try? JSONEncoder().encode(savedCustomer) {
            UserDefaults.standard.set(encodedData, forKey: arrayKey)
        }
    }
}

◾️EssentialsMenu.swift

//フレームワーク
import SwiftUI
import WebKit

//ビュー管理構造体
struct ListiOSApp17DevelopmentEssentials: Identifiable {
    var id: Int
    var title: String
    var view: ViewEnumiOSApp17DevelopmentEssentials
}
//遷移先の画面を格納する列挙型
enum ViewEnumiOSApp17DevelopmentEssentials {
    case Ch1
    //じっくり13で追加
    case Ch2
    //じっくり14で追加
    case Ch3
    //じっくり15で追加
    case Ch4
    //じっくり16で追加
    case Ch5
    //じっくり17で追加
    case Ch6
    //じっくり18で追加
    case Ch7
    //じっくり19で追加
    case Ch8
    //じっくり20、21で追加
    case Ch9
    //じっくり22、23で追加
    case Ch10
    //じっくり24で追加
    case Ch11
    //じっくり25で追加
    case Ch12
    //じっくり26で追加
    case Ch13
    //じっくり27,28で追加
    case Ch14
    //じっくり29で追加
    case Ch15
    //じっくり31で追加
    case Ch16
    //じっくり32で追加
    case Ch17
    //じっくり33で追加
    case Ch18
    //じっくり34で追加
    case Ch19
    //じっくり35で追加
    case Ch20
    //じっくり36で追加
    case Ch21
    //じっくり37で追加
    case Ch22
    //じっくり40で追加
    case Ch23
    //じっくり41で追加
    case Ch24
    //じっくり43で追加
    case Ch25
    //じっくり44で追加
    case Ch26
    //じっくり45で追加
    case Ch27
    //じっくり46で追加
    case Ch28
}
//各項目に表示する文字列
let dataiOSApp17DevelopmentEssentials: [ListiOSApp17DevelopmentEssentials] = [
    ListiOSApp17DevelopmentEssentials(id: 1, title: essentialsChapter1Title, view: .Ch1),
    //じっくり13で追加
    ListiOSApp17DevelopmentEssentials(id: 2, title: essentialsChapter2Title, view: .Ch2),
    //じっくり13で追加
    ListiOSApp17DevelopmentEssentials(id: 3, title: essentialsChapter3Title, view: .Ch3),
    //じっくり15で追加
    ListiOSApp17DevelopmentEssentials(id: 4, title: essentialsChapter4Title, view: .Ch4),
    //じっくり16で追加
    ListiOSApp17DevelopmentEssentials(id: 5, title: essentialsChapter5Title, view: .Ch5),
    //じっくり17で追加
    ListiOSApp17DevelopmentEssentials(id: 6, title: essentialsChapter6Title, view: .Ch6),
    //じっくり18で追加
    ListiOSApp17DevelopmentEssentials(id: 7, title: essentialsChapter7Title, view: .Ch7),
    //じっくり19で追加
    ListiOSApp17DevelopmentEssentials(id: 8, title: essentialsChapter8Title, view: .Ch8),
    //じっくり20、21で追加
    ListiOSApp17DevelopmentEssentials(id: 9, title: essentialsChapter9Title, view: .Ch9),
    //じっくり22、23で追加
    ListiOSApp17DevelopmentEssentials(id: 10, title: essentialsChapter10Title, view: .Ch10),
    //じっくり24で追加
    ListiOSApp17DevelopmentEssentials(id: 11, title: essentialsChapter11Title, view: .Ch11),
    //じっくり25で追加
    ListiOSApp17DevelopmentEssentials(id: 12, title: essentialsChapter12Title, view: .Ch12),
    //じっくり26で追加
    ListiOSApp17DevelopmentEssentials(id: 13, title: essentialsChapter13Title, view: .Ch13),
    //じっくり27,28で追加
    ListiOSApp17DevelopmentEssentials(id: 14, title: essentialsChapter14Title, view: .Ch14),
    //じっくり29で追加
    ListiOSApp17DevelopmentEssentials(id: 15, title: essentialsChapter15Title, view: .Ch15),
    //じっくり31で追加
    ListiOSApp17DevelopmentEssentials(id: 16, title: essentialsChapter16Title, view: .Ch16),
    //じっくり32で追加
    ListiOSApp17DevelopmentEssentials(id: 17, title: essentialsChapter17Title, view: .Ch17),
    //じっくり33で追加
    ListiOSApp17DevelopmentEssentials(id: 18, title: essentialsChapter18Title, view: .Ch18),
    //じっくり34で追加
    ListiOSApp17DevelopmentEssentials(id: 19, title: essentialsChapter19Title, view: .Ch19),
    //じっくり35で追加
    ListiOSApp17DevelopmentEssentials(id: 20, title: essentialsChapter20Title, view: .Ch20),
    //じっくり36で追加
    ListiOSApp17DevelopmentEssentials(id: 21, title: essentialsChapter21Title, view: .Ch21),
    //じっくり37で追加
    ListiOSApp17DevelopmentEssentials(id: 22, title: essentialsChapter22Title, view: .Ch22),
    //じっくり40で追加
    ListiOSApp17DevelopmentEssentials(id: 23, title: essentialsChapter23Title, view: .Ch23),
    //じっくり41で追加
    ListiOSApp17DevelopmentEssentials(id: 24, title: essentialsChapter24Title, view: .Ch24),
    //じっくり43で追加
    ListiOSApp17DevelopmentEssentials(id: 25, title: essentialsChapter25Title, view: .Ch25),
    //じっくり44で追加
    ListiOSApp17DevelopmentEssentials(id: 26, title: essentialsChapter26Title, view: .Ch26),
    //じっくり47で追加
    ListiOSApp17DevelopmentEssentials(id: 27, title: essentialsChapter27Title, view: .Ch27),
    //じっくり48で追加
    ListiOSApp17DevelopmentEssentials(id: 28, title: essentialsChapter28Title, view: .Ch28),
]

struct iOSApp17DevelopmentEssentials: View {
    var body: some View {
        VStack {
            Divider()
            List (dataiOSApp17DevelopmentEssentials) { data in
                self.containedViewiOSApp17DevelopmentEssentials(dataiOSApp17DevelopmentEssentials: data)
            }
            .edgesIgnoringSafeArea([.bottom])
        }
        .navigationTitle("iOS開発の章目次")
        .navigationBarTitleDisplayMode(.inline)
    }
    //タップ後に遷移先へ遷移させる関数
    func containedViewiOSApp17DevelopmentEssentials(dataiOSApp17DevelopmentEssentials: ListiOSApp17DevelopmentEssentials) -> AnyView {
        switch dataiOSApp17DevelopmentEssentials.view {
        case .Ch1:
            return AnyView(NavigationLink (destination: iOSApp17DevelopmentEssentialsCh1()) {
                Text(dataiOSApp17DevelopmentEssentials.title)
            })
            //じっくり13で追加
        case .Ch2:
            return AnyView(NavigationLink (destination: iOSApp17DevelopmentEssentialsCh2()) {
                Text(dataiOSApp17DevelopmentEssentials.title)
            })
            //じっくり13で追加
        case .Ch3:
            return AnyView(NavigationLink (destination: iOSApp17DevelopmentEssentialsCh3()) {
                Text(dataiOSApp17DevelopmentEssentials.title)
            })
            //じっくり15で追加
        case .Ch4:
            return AnyView(NavigationLink (destination: iOSApp17DevelopmentEssentialsCh4()) {
                Text(dataiOSApp17DevelopmentEssentials.title)
            })
            //じっくり16で追加
        case .Ch5:
            return AnyView(NavigationLink (destination: iOSApp17DevelopmentEssentialsCh5()) {
                Text(dataiOSApp17DevelopmentEssentials.title)
            })
            //じっくり17で追加
        case .Ch6:
            return AnyView(NavigationLink (destination: iOSApp17DevelopmentEssentialsCh6()) {
                Text(dataiOSApp17DevelopmentEssentials.title)
            })
            //じっくり18で追加
        case .Ch7:
            return AnyView(NavigationLink (destination: iOSApp17DevelopmentEssentialsCh7()) {
                Text(dataiOSApp17DevelopmentEssentials.title)
            })
            //じっくり19で追加
        case .Ch8:
            return AnyView(NavigationLink (destination: iOSApp17DevelopmentEssentialsCh8()) {
                Text(dataiOSApp17DevelopmentEssentials.title)
            })
            //じっくり20、21で追加
        case .Ch9:
            return AnyView(NavigationLink (destination: iOSApp17DevelopmentEssentialsCh9()) {
                Text(dataiOSApp17DevelopmentEssentials.title)
            })
            //じっくり22、23で追加
        case .Ch10:
            return AnyView(NavigationLink (destination: iOSApp17DevelopmentEssentialsCh10()) {
                Text(dataiOSApp17DevelopmentEssentials.title)
            })
            //じっくり24で追加
        case .Ch11:
            return AnyView(NavigationLink (destination: iOSApp17DevelopmentEssentialsCh11()) {
                Text(dataiOSApp17DevelopmentEssentials.title)
            })
            //じっくり25で追加
        case .Ch12:
            return AnyView(NavigationLink (destination: iOSApp17DevelopmentEssentialsCh12()) {
                Text(dataiOSApp17DevelopmentEssentials.title)
            })
            //じっくり26で追加
        case .Ch13:
            return AnyView(NavigationLink (destination: iOSApp17DevelopmentEssentialsCh13()) {
                Text(dataiOSApp17DevelopmentEssentials.title)
            })
            //じっくり27,28で追加
        case .Ch14:
            return AnyView(NavigationLink (destination: iOSApp17DevelopmentEssentialsCh14()) {
                Text(dataiOSApp17DevelopmentEssentials.title)
            })
            //じっくり29で追加
        case .Ch15:
            return AnyView(NavigationLink (destination: iOSApp17DevelopmentEssentialsCh15()) {
                Text(dataiOSApp17DevelopmentEssentials.title)
            })
            //じっくり31で追加
        case .Ch16:
            return AnyView(NavigationLink (destination: iOSApp17DevelopmentEssentialsCh16()) {
                Text(dataiOSApp17DevelopmentEssentials.title)
            })
            //じっくり32で追加
        case .Ch17:
            return AnyView(NavigationLink (destination: iOSApp17DevelopmentEssentialsCh17()) {
                Text(dataiOSApp17DevelopmentEssentials.title)
            })
            //じっくり33で追加
        case .Ch18:
            return AnyView(NavigationLink (destination: iOSApp17DevelopmentEssentialsCh18()) {
                Text(dataiOSApp17DevelopmentEssentials.title)
            })
            //じっくり34で追加
        case .Ch19:
            return AnyView(NavigationLink (destination: iOSApp17DevelopmentEssentialsCh19()) {
                Text(dataiOSApp17DevelopmentEssentials.title)
            })
            //じっくり35で追加
        case .Ch20:
            return AnyView(NavigationLink (destination: iOSApp17DevelopmentEssentialsCh20()) {
                Text(dataiOSApp17DevelopmentEssentials.title)
            })
            //じっくり36で追加
        case .Ch21:
            return AnyView(NavigationLink (destination: iOSApp17DevelopmentEssentialsCh21()) {
                Text(dataiOSApp17DevelopmentEssentials.title)
            })
            //じっくり37で追加
        case .Ch22:
            return AnyView(NavigationLink (destination: iOSApp17DevelopmentEssentialsCh22()) {
                Text(dataiOSApp17DevelopmentEssentials.title)
            })
            //じっくり40で追加
        case .Ch23:
            return AnyView(NavigationLink (destination: iOSApp17DevelopmentEssentialsCh23()) {
                Text(dataiOSApp17DevelopmentEssentials.title)
            })
            //じっくり41で追加
        case .Ch24:
            return AnyView(NavigationLink (destination: iOSApp17DevelopmentEssentialsCh24()) {
                Text(dataiOSApp17DevelopmentEssentials.title)
            })
            //じっくり43で追加
        case .Ch25:
            return AnyView(NavigationLink (destination: iOSApp17DevelopmentEssentialsCh25()) {
                Text(dataiOSApp17DevelopmentEssentials.title)
            })
            //じっくり44で追加
        case .Ch26:
            return AnyView(NavigationLink (destination: iOSApp17DevelopmentEssentialsCh26()) {
                Text(dataiOSApp17DevelopmentEssentials.title)
            })
            //じっくり45で追加
        case .Ch27:
            return AnyView(NavigationLink (destination: iOSApp17DevelopmentEssentialsCh27()) {
                Text(dataiOSApp17DevelopmentEssentials.title)
            })
            //じっくり46で追加
        case .Ch28:
            return AnyView(NavigationLink (destination: iOSApp17DevelopmentEssentialsCh28()) {
                Text(dataiOSApp17DevelopmentEssentials.title)
            })
        }
    }
}

#Preview {
    iOSApp17DevelopmentEssentials()
}

以上。
ま、これを使うと応用で

アプリの顧客の購入リストなんかもこういう機能を使って作ってる
ってのが分かるでしょ🧐

さてと、温泉に〜〜〜😛
じゃまたね〜〜〜

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