🫠

thread 1: "object has been deleted or invalidated."

2024/01/23に公開

スワイプして削除が失敗する???

Realmでアプリ作りを作ってみたのだけど、追加と表示はできるのに、削除が失敗する。データは消せるけど、UIのエラーが出るみたいだ???

Listに表示してるデータを削除するのをやってみたがうまくいかない???
offsetなるものが必要らしい?
https://developer.apple.com/documentation/swiftui/view/offset(x:y:)

公式によると
このビューを指定された水平距離と垂直距離だけオフセットする。

func offset(
    x: CGFloat = 0,
    y: CGFloat = 0
) -> some View

パラメータ
x
このビューをオフセットする水平距離。

y
このビューをオフセットする垂直距離。

戻り値
このビューを x と y でオフセットしたビュー。

ディスカッション
offset(x:y:)を使用すると、表示された内容をxとyのパラメータで指定された量だけずらすことができます。

下の例では、このビューによって描画された灰色の境界線が、テキストの元の位置を囲んでいます:

Text("Offset by passing horizontal & vertical distance")
    .border(Color.green)
    .offset(x: 20, y: 50)
    .border(Color.gray)

今回エラーを解決するにはこのコードが必要だった

private func deleteBook(at offsets: IndexSet) {
        offsets.map { realmManager.books[$0] }.forEach(realmManager.deleteBook)
    }

アプリを作ってみた!

パッケージは、GUIで追加する。こちらを参考にしてね。
https://zenn.dev/joo_hashi/articles/0ce2f6c4fb8615

ディレクトリは分けてます

Realemを追加したら、Bookというモデルを作りましょう!、これは公式のコードをそのまま使ってます。

import Foundation
import RealmSwift

// 本のデータを保存するモデル
class Book: Object {
   @Persisted(primaryKey: true) var _id: ObjectId
   @Persisted var name: String = ""

   convenience init(name: String) {
       self.init()
       self.name = name
   }
}

DBの操作と接続するファイル

追加、表示、削除をするクラスを作成します。ロジックだけなら作るのは難しくないように思えたがSwiftUIはUI側の配列を操作して要素を削除しないと、エラーの原因になるらしい?

import Foundation
import RealmSwift

// 状態が変更されたときに、Viewに更新を通知するクラス
// [ObservableObject]は、データの変更を監視し、それに応じてビューを更新するために使用されるプロトコル
class RealmManager: ObservableObject {
    // localRealm という名前の変数を作成し、そこにレルムを保存します。
    private(set) var localRealm: Realm?
    // [@Published]は、プロパティを監視して変更があったらViewに通知をする
    @Published var books: [Book] = []
    // initはRealmManagerが生成されたときに呼ばれる
    init() {
        // Realmを開くメソッドを実行
        openRealm()
        //  保存されている練習帳のデータを取得
        getBooks()
    }
    // Realmとの接続を開くメソッド
    func openRealm() {
        do {
            let config = Realm.Configuration(schemaVersion: 1)
            Realm.Configuration.defaultConfiguration = config
            localRealm = try Realm()
        } catch {
            print("Error opening Realm", error)
        }
    }
    // nameを追加するメソッド
    func addCounter(name: String) {
        // if letでlocalRealmがnilでないことを確認
        if let localRealm = localRealm {
            // do-catchでエラー処理
            do {
                // Realmに書き込み
                try localRealm.write {
                    let newBook = Book()
                    newBook.name = name
                    localRealm.add(newBook)
                    getBooks()
                }
            } catch {
                print("Error adding counter to Realm: \(error)")
            }
        }
    }
    // 練習帳の情報を取得するメソッド
    func getBooks() {
        // if letでlocalRealmがnilでないことを確認
        if let localRealm = localRealm {
            // Realmから全てのnameを取得
            let allBooks = localRealm.objects(Book.self)
            // booksに全てのカウンターを代入
            books = []
            // forEachでallBooksの要素をbookに代入。配列なので、appendでbooksに追加
            allBooks.forEach { book in
                books.append(book)
            }
        }
    }

    // 練習帳の情報を削除するメソッド
func deleteBook(book: Book) {
    // if letでlocalRealmがnilでないことを確認
    if let localRealm = localRealm, !book.isInvalidated {
        // do-catchでエラー処理
        do {
            // Realmからbookを削除
            try localRealm.write {
                localRealm.delete(book)
                getBooks()
            }
        } catch {
            print("Error deleting book from Realm: \(error)")
        }
    }
}

}

Uiのコードを書く

追加、表示、削除をするUIを作っていきましょう!

本の名前を追加するだけのForm。15文字未満しか入力できないようにバリデーションがあって、ライトモードとダークモードに対応してる。

import SwiftUI
import RealmSwift

// 本の追加ページ
struct CreateBook: View {
    @EnvironmentObject var realmManager: RealmManager
    @Environment(\.presentationMode) var presentationMode
    @State private var name: String = ""
    @State private var showingAlert = false

    var body: some View {
        NavigationView {
            Form {
                Section(header: Text("Book Information")) {
                    TextField("Name", text: $name)
                        .onChange(of: name) { newValue in
                            if newValue.count > 15 {
                                name = String(newValue.prefix(15))
                                showingAlert = true
                            }
                        }
                }
            }
            .navigationBarTitle("練習帳を作成", displayMode: .inline)
            .navigationBarItems(trailing: Button(action: {
                realmManager.addCounter(name: name)
                name = ""
                self.presentationMode.wrappedValue.dismiss()
            }) {
                Text("Save")
            })
            .alert(isPresented: $showingAlert) {
                Alert(title: Text("Warning"), message: Text("本のタイトルは15文字未満です"), dismissButton: .default(Text("OK")))
            }
        }
    }
}

struct CreateBook_Previews: PreviewProvider {
    static var previews: some View {
        CreateBook().environmentObject(RealmManager())
    }
}


追加されたデータの表示と削除ができるページ:

このコードでは、まずListビューでrealmManager.books配列の各要素を表示しています。そして、.onDelete(perform: deleteBook)修飾子を使用して、スワイプして削除する機能を追加しています。

import SwiftUI

// 本の情報の表示と削除をするページ
struct ReadBook: View {
    @EnvironmentObject var realmManager: RealmManager

    var body: some View {
        NavigationView {
            List {
                ForEach(realmManager.books.indices, id: \.self) { index in
                    Text(realmManager.books[index].name)
                }
                .onDelete(perform: deleteBook)
            }
            .navigationBarTitle("練習帳", displayMode: .inline)
            .navigationBarItems(trailing: NavigationLink(destination: CreateBook().environmentObject(realmManager)) {
                Image(systemName: "plus")
            })
        }
    }

    private func deleteBook(at offsets: IndexSet) {
        offsets.map { realmManager.books[$0] }.forEach(realmManager.deleteBook)
    }
}

struct ReadBook_Previews: PreviewProvider {
    static var previews: some View {
        ReadBook().environmentObject(RealmManager())
    }
}

deleteBook(at offsets: IndexSet)メソッドは、削除するbookオブジェクトをrealmManager.books配列から取得し、それをrealmManager.deleteBookメソッドに渡して削除しています。

エントリーポイントになるファイルも設定しておく

import SwiftUI

@main
struct ExerciseBookApp: App {
    // Realmにデータを追加すると画面が更新される様にする。
    @StateObject var realmManager = RealmManager()

    var body: some Scene {
        WindowGroup {
            ReadBook()
                .environmentObject(realmManager)
        }
    }
}



最後に

簡単なアプリを作るだけで難しかったです💦
まさか、データを消したら配列の要素の削除も必要とは知らなかった💦
なんで気づいたかというと、リリースした個人アプリで同じ機能を作ったことがあったので、それをそのまま使えば解決できました😅
皆さんもSwiftUIでアプリを作るのを楽しんでね💚💙💛

Discussion