見出し画像

【じっくりSw1ftUI71】実践(DB)編40〜第52章 SwiftData ガイド〜ここがこのマガジンを作り始めた時からオイラが学びたかった部分

さてと、前回

で、CoreDataとの連動も含めた

CloudKitを利用したデータストレージ

まではやったので、今回と次回で、

SwiftData

について紹介してく🕺

ちょっと前、

くらいから

データベース(以下、DB)

についてやってるんだけど、
今までのSwiftUIの特長

  • よりシンプルに

  • よりモダンに

  • より直観的に

からすると、永続コンテナだの、ビューコンテキストだの、アカウントのライセンスだの

事前に用意しないといけない
連携が強い
本編のサンプルも薄い
👉なんかややこしいな🧐

って

感じた、違和感を持った人、大正解

ま、理由は簡単で

でやってたUIKit、それ以前は、Objective-Cと、iOSアプリ開発は、ここ10年くらいを見ても、

Objective-C

Swift=UIKit

SwiftUI

と常に進化し続けていて、CoreData→CloudKitと、

サービスが進化する過程で常にDB周りも進化を遂げている
=UserDefaultやCoreDataは、UIKit以前から使われていたDB
👉SwiftUIの方が後付けのフレームワーク

なので、

SwiftUIに特化したDB周りを作ろう

で誕生したのが、

SwiftData

DBとかSQLってゆーと、

DBエンジニアで気難しい人が多かったり、小難しくしたがる自称さんなんかも多いので、その印象からか、

DBに苦手意識を持ってる

DB周りの基礎知識については、

にて、すっげ〜長文で説明したつもりだけど、裏を返すと、

所詮は、ただのデータ(レコード)の固まり

なので

DBって1記事で書いてること
くらいを意識しておけば
誰でも簡単に構築できて便利に使えるもの
(じゃないと普及しない)

なんだよね🧐さてと、前置きはこれくらいにして。

オイラの学ぶなんざ今回も関係ないって人は

で読んだらいいんじゃね😛
んだば、さっそく本題へ〜〜〜


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

ま、概要としても、前段で話したことを書いてる程度で〜〜〜

ここでポイント①:SwiftDataの特長としては、

  1. SwiftData :フレームワーク👈インポートすればOK

  2. SwiftUI コードとシームレスに統合され、アプリ内に永続的なデータを宣言的に保存する方法を提供👈宣言なのでコードを見れば分かる

  3. Core Data 上のレイヤーとして実装された SwiftData は、複雑なコードを記述しなくても、多くの機能にアクセスできる👈シンプルにモダンの設計思想=SwiftUIと親和性が高い

ってことくらいを抑えておけばいいんじゃね🧐❓って感じ

んだば、実際に操作を。まずは

データモデルを作ろう

って、敢えて、これまでのCoreDataとCloudKitと同様に

データモデルファイルを追加するのか❓👀💦

って勘違いしそうな見出しにしたけど、SwiftDataでは、

モデルクラスとして宣言する

みたいだね👀なので〜〜〜

新規プロジェクトを作成

今回もProductNameはテケトーに付けて
ここもこんな感じにだけしておいて、Next>Createで進んで〜〜〜

デフォルトでどんなファイルが出来上がるかなんだけど、

てな感じ

左側のプロジェクトエクスプローラーのファイル数を見ると、

非常に多く感じる

けど、各ファイルの内容を見てもらうと、上から順に
(*Xcodeを最新にアップデートしてる環境でやってます🕺画面構成やコードが人によっては、少し違うかも)

◾️Preview Assets.xcassets

空👀💦

◾️Assets.xcassets

空やん😆

◾️ContentView.swift

てな感じ
コードを書き出すと、、、
import SwiftUI
import SwiftData

struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    @Query private var items: [Item]

    var body: some View {
        NavigationSplitView {
            List {
                ForEach(items) { item in
                    NavigationLink {
                        Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))")
                    } label: {
                        Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
                    }
                }
                .onDelete(perform: deleteItems)
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                }
                ToolbarItem {
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
            }
        } detail: {
            Text("Select an item")
        }
    }

    private func addItem() {
        withAnimation {
            let newItem = Item(timestamp: Date())
            modelContext.insert(newItem)
        }
    }

    private func deleteItems(offsets: IndexSet) {
        withAnimation {
            for index in offsets {
                modelContext.delete(items[index])
            }
        }
    }
}

#Preview {
    ContentView()
        .modelContainer(for: Item.self, inMemory: true)
}

🧐?たったこれだけ?

◾️E52SwiftDataGuide.entitlements

初期値

◾️E52SwiftDataGuideApp.swift

てな感じ
コードを書くと、、、
import SwiftUI
import SwiftData

@main
struct E52SwiftDataGuideApp: App {
    var sharedModelContainer: ModelContainer = {
        let schema = Schema([
            Item.self,
        ])
        let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)

        do {
            return try ModelContainer(for: schema, configurations: [modelConfiguration])
        } catch {
            fatalError("Could not create ModelContainer: \(error)")
        }
    }()

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(sharedModelContainer)
    }
}

くらいなもん🧐

◾️Info.plist

初期値

◾️Item.swift(👈Model Class:モデルクラス)

てな感じ
コードを書くと、、、
import Foundation
import SwiftData

@Model
final class Item {
    var timestamp: Date
    
    init(timestamp: Date) {
        self.timestamp = timestamp
    }
}

ここもめっちゃシンプル🧐

ここでポイント②:オブジェクト指向言語なので〜

Appleは、MVVMの設計思想がはっきりしてるから、

役割ごとにファイルを分けるのが好き
👉見やすいし、管理しやすいからね)

なんだけど、別に上記のファイルを

モデルクラスを
てな感じで移してあげても、結果は同じ
別にItem .swiftを削除しても、エラー発生なし。

つまり、

同じプロジェクトファイル内にありさえすれば、
どこの.swiftファイルに書いてもOK

こー書くと、捻くれてる人で、

こーしたがる人もいそうだけど
もちろんこれでもエラーなし😛

ま、要はどこに書く方が、

<ファイル数も少なく、コードの変更があったときに管理しやすいか>

だけなので、オイラだったら、

ContentViewで管理するものは、ContentViewにまとめた方が管理しやすい

ってだけ〜〜〜

ここでポイント③:CoreDataやCloudKitの記事でやってたDataModelファイルがない

デフォルトで見ても分かるとおり、

コイツを作らなくていいのが最大の特長

コードとコード以外で、
二元管理になって連動させないといけなかった

から、正直、慣れ+命名規約なんかの基本がないと、

似通った名前でのアトリビュートなんかになって
非常に改修コストが面倒なことに、、、

ってことになりかねなかったからね🧐とまあ、新規プロジェクトを作成しての前提はこれくらいにして、コードを打ちながら、読み進めて行ってたんだけど

モデルコンテナを作成して、モデルコンフィグレーション

のところで、

モデルコンフィグレーション

を追加〜〜〜

let modelConfig = ModelConfiguration(isStoredInMemoryOnly: true)
let modelContainer = try? ModelContainer(for: Fruits.self, configurations: modelConfig)

とサンプルどおりに書くと、

なぜかエラー笑😆
イニシャライズされたメンバーの中にないから使えないぞと怒ってる様子

このまま先に進めようかなあ🧐と思ったんだけど、先を読んでも

毎回、ここら辺では鉄板になってるこの本の構成で、

  • ガイド(この章):思いっきり概念とかを本当にガイドしてるだけ😆

  • チュートリアル(次章):実際に動かす

って感じで、

次章のサンプルコードを実際に見ないと、理解ができない構成😛
👉次章まで読んで理解してる前提で、
いきなりガイドしてる
=話ぶっ飛びすぎ〜〜〜

実際、読み進めていくと、いきなり、どこにも出てきてない

<Visitor>?🧐

なんてものが出てきてる笑😆なんやねん?👀💦と、ここで出てるサンプルコードを、真面目に書いて、理解しようとしても

却って混乱するだけ

なので、

新規プロジェクトファイルで書かれていたコードを前提に、
デフォルトのコードに関する項目を優先に、
読み進めてく
🕺

(全ての人に分かりやすいかは分からないけど、ここは冒頭のリンクを書くコードにコメントに挟みながらの方が理解しやすいかなと思うので、、、💦)

◆モデルクラス:class

//モデルクラス
/*
 SwiftData モデル クラスは、保存されるデータのスキーマを表し、Swift クラスとして宣言
 */
@Model
final class Item {
    var timestamp: Date
    
    init(timestamp: Date) {
        self.timestamp = timestamp
    }
}

◆モデルコンテナとモデル構成

import SwiftUI
import SwiftData

@main
struct E52SwiftDataGuideApp: App {
    //モデルコンテナ
    /*
     ・モデルコンテナーの目的:モデルスキーマを収集し、データモデル オブジェクトのインスタンスを格納するデータベースを生成
     ・基本的に、モデルコンテナーは、モデルスキーマと基礎となるデータベースストレージ間のインターフェイスを提供
     ・直接作成することも、SceneまたはWindowGroupにmodelContainer(for:)修飾子を適用して作成もできる。
     */
    var sharedModelContainer: ModelContainer = {
        let schema = Schema([
            Item.self,
        ])
        //モデル構成
        /*
         ・モデル構成をモデル コンテナーに適用して、永続データの保存方法とアクセス方法を構成できる。
         ・
         */
        let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)

        do {
            return try ModelContainer(for: schema, configurations: [modelConfiguration])
        } catch {
            fatalError("Could not create ModelContainer: \(error)")
        }
    }()

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        //モデルコンテナとビューを連携
        .modelContainer(sharedModelContainer)
    }
}

◆モデルコンテキストと@Queryマクロ

import SwiftUI
import SwiftData
import Foundation

//モデルクラス
/*
 ・SwiftData モデル クラスは、保存されるデータのスキーマを表し、Swift クラスとして宣言
 */
@Model
final class Item {
    var timestamp: Date
    
    init(timestamp: Date) {
        self.timestamp = timestamp
    }
}

struct ContentView: View {
    //モデルコンテキスト
    /*
     ・モデルコンテナが作成されると、SwiftUIはコンテナのモデルコンテキストへのバインディングを作成
     ・モデルコンテキストは、基になるデータの変更を追跡し、モデルオブジェクトの追加、更新、取得、削除など、保存されたデータに対する操作をアプリコードが実行するためのプログラミング インターフェースを提供
     ・モデルコンテナが作成されると、モデルコンテキストへのバインディングがアプリの環境に配置され、シーンやビュー内からアクセスできるようになる。
     */
    @Environment(\.modelContext) private var modelContext
    //@Query
    /*
     ・ストレージからオブジェクトを取得する便利な方法を提供
     👉SwiftUI の監視機能を使用して結果が常に最新であることを保証
     ・最も単純な形式では、このマクロを使って、データベースから保存されているすべての連絡先オブジェクトを取得できる。
     */
    @Query private var items: [Item]
    /*
     ・宣言されると、モデルコンテキストでfetch()メソッドを呼び出さなくても、連絡先配列が自動的に更新され、最新の連絡先が含まれるようになる。
     ・述語とソート記述子を使用して結果をフィルタリングするためにも使用できる。
     */

    var body: some View {
        NavigationSplitView {
            List {
                ForEach(items) { item in
                    NavigationLink {
                        Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))")
                    } label: {
                        Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
                    }
                }
                .onDelete(perform: deleteItems)
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                }
                ToolbarItem {
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
            }
        } detail: {
            Text("Select an item")
        }
    }

    private func addItem() {
        withAnimation {
            let newItem = Item(timestamp: Date())
            modelContext.insert(newItem)
        }
    }

    private func deleteItems(offsets: IndexSet) {
        withAnimation {
            for index in offsets {
                modelContext.delete(items[index])
            }
        }
    }
}

#Preview {
    ContentView()
        .modelContainer(for: Item.self, inMemory: true)
}

てな感じ。でまあ、ここまでで一旦、

新規プロジェクトのコードをプレビューで見てみると、、、

デフォルト=初期

右上の+をタップ

タップした時間が追加された
何回も押した後〜〜〜
Editをタップして、左のーでDelete

てな感じで、

コードだけでDBを管理できる

その他

◆PredicateとFetchDescriptor:#Predicate、.fetch()

  • Predicate:データベースから一致するデータを取得するための条件を定義し、true または false に評価される論理式の形式。

  • FetchDescriptor:Predicateが宣言されると、FetchDescriptorを作成するために使う。

👉フェッチ結果をフィルタリングするだけでなく、フェッチ記述子を使用して返された一致を並べ替えもできる。
SortDescriptor は、フェッチ結果のソート順序を指定するためにも使う。

◆モデルリレーションシップ:@Relationshipマクロ

  • @Relationshipマクロを使用して宣言。

  • リレーションが確立されると、プロパティにアクセスして、関連付けられているリストにアクセスできる。

  • モデル オブジェクトが削除されると、デフォルトの動作では、関連するオブジェクトはすべてデータベースに残る。👉削除しても、そのすべてがデータベースに残る。

  • 削除するには、【削除ルール】の指定が必要。

【削除ルール】

  • cascade– 関連するすべてのオブジェクトを削除

  • deny – 他のオブジェクトとの関係を含むオブジェクトの削除を防止

  • noAction – 関連オブジェクトを変更せずに、削除されたオブジェクトへの参照をそのまま残す。

  • nullify – 関連オブジェクトは削除されないが、削除されたオブジェクトへの参照は無効になる。

◆モデルアトリビュート:@Attributesマクロ

  • モデルクラス内の個々のプロパティに動作を適用。

  • 一般的な使用法は、一意のプロパティを指定。(例)姓の重複を防止

  • @Transientマクロを使って、モデルクラス内のプロパティをデータベースへの保存から除外できる。

本編の内容としては以上👀💦

まとめ

ガイドって言いながら、次章のコードを前提に書いてるものだから、

初めてやる人は混乱の極みだろうねえ🧐

オイラもSwiftDataは扱うのが今回が初めてだから、結局、

次章で動かしながらじゃないと具体的にはイメージ湧かないし、、、💦
👉各概念とポイントだけをまとめてあげるだけでここはいいかなと。

ま、ただここまで読めば分かると思うけど

SwiftData:SwiftUIの旨み=コードベースで余計なデータモデルを使わずに、シンプルなコードだけで、データベースを管理できるんだ!

だけここではイメージできれば十分じゃないかな🧐ま、次回の記事の振り返りで、今回のコードで使っていないマクロなんかも含めて、要点は網羅したけどね👀💦

Apple公式

さてと、次回は

今回の本編で出てきたサンプルコードを実際に動かしながら見ていく

第53章 SwiftDataチュートリアル

を見てく🕺

記事公開後、

今回も新規プロジェクトファイルのコードを使って、解説してるだけなんで、以前は、恒例になってた記事公開は、今回もなし。

今回は以上。

次回でいよいよ、【実践編】もラストだねえ🧐

でつぶやいたとおり、来週は3連休だし、時間もありそうなので、

この【じっくりSw1ftUI】シリーズで書いた一連の記事のここがポイントをひとつの記事にまとめてみようかねえ🧐
プロットは本の流れに沿っているだけで、各記事を読んでみてもらうと分かるけど、

  • サンプルのコード:本のコードをベースにオリジナルに書き換えたコード

  • ここがポイント:本編には一切、そんな項目はなくオイラがこれまでの経験なんかを踏まえて書いた完全オリジナル

なので〜〜〜〜
明日は朝から、2024年衆院選の投票に行って、後は嫁とデートなんで

また来週かな🤣
💃では皆さんも良き週末を🕺

しっかし、やったことがない人向けの、わかりやすい本ってホント少ないなあ🧐

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