最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

swift - Signal SIGABRT on accessing values from SwiftData query - Stack Overflow

programmeradmin1浏览0评论

I work on an iOS app using SwiftUI and SwiftData. I added a computed property to one of my models - Parent - that uses relationship - array of Child models - data and I started getting strange problems. Let me start with models:

@Model
final class Parent {
    var name: String
    @Relationship(deleteRule: .cascade, inverse: \Child.parent)
    var children: [Child]? = []

    var streak: Int {
        // Yes, I know that's not optimal solution for such counter ;)
        guard let children = children?.sorted(using: SortDescriptor(\.date, order: .reverse)) else { return 0 }
        var date = Date.now
        let calendar = Calendar.current
        for (index, child) in children.enumerated() {
            if !calendar.isDate(child.date, inSameDayAs: date) {
                return index
            }
            date = calendar.date(byAdding: .day, value: -1, to: date) ?? .now
        }
        return children.count
    }

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

@Model
final class Child {
    var date: Date
    @Relationship(deleteRule: .nullify)
    var parent: Parent?

    init(date: Date, parent: Parent) {
        self.date = date
        self.parent = parent
    }
}

At first everything works as expected. The problem arises once I try to remove one of child from the parent instance. I remove the value from context and save changes without any problems, at least not ones that can be caught by do { } catch. But instead of refreshing UI I get an signal SIGABRT somewhere inside SwiftData internals that points to the line where I'm trying (inside View body) get a child from a Query:

struct LastSevenDaysButtons: View {
    @Environment(\.modelContext)
    private var modelContext
    @Query
    private var children: [Child]

    private let dates: [Date]
    private let parent: Parent

    init(for parent: Parent) {
        self.parent = parent

        var lastSevenDays = [Date]()
        let calendar = Calendar.current
        let firstDate = calendar.date(byAdding: .day, value: -6, to: calendar.startOfDay(for: .now)) ?? .now
        var date = firstDate
        while date <= .now {
            lastSevenDays.append(date)
            date = calendar.date(byAdding: .day, value: 1, to: date) ?? .now
        }
        dates = lastSevenDays

        let parentId = parent.persistentModelID
        _children = Query(
            filter: #Predicate {
                $0.parent?.persistentModelID == parentId && $0.date >= firstDate
            },
            sort: [SortDescriptor(\Child.date, order: .reverse)],
            animation: .default
        )
    }

    var body: some View {
        VStack {
            HStack(alignment: .top) {
                ForEach(dates, id: \.self) { date in
                    // Here is the last point on stack from my code that I see
                    let child = children.first { $0.date == date }
                    Button {
                        if let child {
                            modelContext.delete(child)
                        } else {
                            modelContext.insert(Child(date: date, parent: parent))
                        }
                        do {
                            try modelContext.save()
                        } catch {
                            print("Can't save changes for \(parent.name) on \(date.formatted(date: .abbreviated, time: .omitted)): \(error.localizedDescription)")
                        }
                    } label: {
                        Text("\(date.formatted(date: .abbreviated, time: .omitted))")
                            .foregroundStyle(child == nil ? .red : .blue)
                    }
                }
            }
        }
    }
}

The LastSevenDaysButtons View is kind of deep in a View hierarchy:

RootView -> ParentList -> ParentListItem -> LastSevenDaysButtons

However once I move insides of ParentList to RootView application works just fine, although I see and warning: === AttributeGraph: cycle detected through attribute 6912 ===.

What could be that I do wrong in here? I believe it must me something I'm missing here, but after 2 days of debug, trial and errors, I can't think clearly anymore.

I prepared minimal repro you can try yourself:

import SwiftData
import SwiftUI

@Model
final class Parent {
    var name: String
    @Relationship(deleteRule: .cascade, inverse: \Child.parent)
    var children: [Child]? = []

    var streak: Int {
        guard let children = children?.sorted(using: SortDescriptor(\.date, order: .reverse)) else { return 0 }
        var date = Date.now
        let calendar = Calendar.current
        for (index, child) in children.enumerated() {
            if !calendar.isDate(child.date, inSameDayAs: date) {
                return index
            }
            date = calendar.date(byAdding: .day, value: -1, to: date) ?? .now
        }
        return children.count
    }

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

extension Parent {
    static func createExamples(in context: ModelContext) {
        let calendar = Calendar.current
        for i in 0 ..< 10 {
            let parent = Parent(name: "Parent \(i)")
            var date = calendar.startOfDay(for: .now)
            for _ in 0 ..< 10 {
                context.insert(Child(date: date, parent: parent))
                date = calendar.date(byAdding: .day, value: -1, to: date) ?? .now
            }
            context.insert(parent)
        }
        do {
            try context.save()
        } catch {
            fatalError("Couldn't create examples: \(error.localizedDescription)")
        }
    }
}

@Model
final class Child {
    var date: Date
    @Relationship(deleteRule: .nullify)
    var parent: Parent?

    init(date: Date, parent: Parent) {
        self.date = date
        self.parent = parent
    }
}

class ModelContainerPrivider {
    var modelContainer: ModelContainer

    @MainActor
    var modelContex: ModelContext {
        modelContainer.mainContext
    }

    init(isStoredInMemoryOnly: Bool = true) {
        let configuration = ModelConfiguration(isStoredInMemoryOnly: isStoredInMemoryOnly)
        let schema = Schema([Parent.self, Child.self])
        do {
            modelContainer = try ModelContainer(for: schema, configurations: [configuration])
        } catch {
            fatalError("Couldn't create model container: \(error.localizedDescription)")
        }
    }
}

struct ContentView: View {
    var body: some View {
        NavigationStack {
            ParentList()
        }
    }
}

struct ParentList: View {
    @Query(sort: [SortDescriptor(\Parent.name)], animation: .default)
    private var parents: [Parent]

    var body: some View {
        List {
            ForEach(parents) { parent in
                ParentListItem(for: parent)
            }
        }
    }
}

struct ParentListItem: View {
    @Bindable
    private var parent: Parent

    init(for parent: Parent) {
        self.parent = parent
    }

    var body: some View {
        VStack(alignment: .leading) {
            NavigationLink(value: parent) {
                Text(parent.name)
                Text("Streak: \(parent.streak)")
            }
            LastSevenDaysButtons(for: parent)
        }
        .buttonStyle(.plain)
        .id(parent.persistentModelID)
    }
}

struct LastSevenDaysButtons: View {
    @Environment(\.modelContext)
    private var modelContext
    @Query
    private var children: [Child]

    private let dates: [Date]
    private let parent: Parent

    init(for parent: Parent) {
        self.parent = parent

        var lastSevenDays = [Date]()
        let calendar = Calendar.current
        let firstDate = calendar.date(byAdding: .day, value: -6, to: calendar.startOfDay(for: .now)) ?? .now
        var date = firstDate
        while date <= .now {
            lastSevenDays.append(date)
            date = calendar.date(byAdding: .day, value: 1, to: date) ?? .now
        }
        dates = lastSevenDays

        let parentId = parent.persistentModelID
        _children = Query(
            filter: #Predicate {
                $0.parent?.persistentModelID == parentId && $0.date >= firstDate
            },
            sort: [SortDescriptor(\Child.date, order: .reverse)],
            animation: .default
        )
    }

    var body: some View {
        VStack {
            HStack(alignment: .top) {
                ForEach(dates, id: \.self) { date in
                    let child = children.first { $0.date == date }
                    Button {
                        if let child {
                            modelContext.delete(child)
                        } else {
                            modelContext.insert(Child(date: date, parent: parent))
                        }
                        do {
                            try modelContext.save()
                        } catch {
                            print("Can't save changes for \(parent.name) on \(date.formatted(date: .abbreviated, time: .omitted)): \(error.localizedDescription)")
                        }
                    } label: {
                        Text("\(date.formatted(date: .abbreviated, time: .omitted))")
                            .foregroundStyle(child == nil ? .red : .blue)
                    }
                }
            }
        }
    }
}

@main
struct PlaygroundApp: App {
    @State
    private var provider = ModelContainerPrivider(isStoredInMemoryOnly: true)

    init() {
        Parent.createExamples(in: provider.modelContex)
    }

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

#Preview {
    let provider = ModelContainerPrivider()
    Parent.createExamples(in: provider.modelContex)
    return ContentView()
        .modelContainer(provider.modelContainer)
}
发布评论

评论列表(0)

  1. 暂无评论