Search code examples
iosswiftuienumscloudkitswiftdata

How to make CloudKit compatible when you have enum driven selectable list


For a book club app I'm writing In SwiftUI, I have a NavigationSplitView that has a selectable List on its leading column. The first item on the list is "Book" and will take the user to a book's PDF if selected. Below it is a Button with a plus icon that allows the user to add a reader of the book. Once a reader is added, their name will appear on a List just below the plus Button. If a user selects a reader, they'll be taken to their profile page.

The purpose of the whole thing is so that "Book" and the list of readers are integrated into one selectable List, such that when one item is selected, it is highlighted and the previous selected item loses its highlight.

This is the code:

import SwiftUI
import SwiftData

enum ListItem: Hashable
{
    case book
    case reader(Reader)
}

struct ProjectView: View
{
    private let project: Project
    
    @Environment(\.modelContext) private var modelContext
    
    @Query private var readers: [Reader]
    
    @State private var alertPresented = false
    @State private var userInput = ""
    @State private var selectedListItem: ListItem?
    
    init(project: Project)
    {
        self.project = project
    }
    
    var body: some View
    {
        NavigationSplitView
        {
            List(selection: self.$selectedListItem)
            {
                Text("Book").tag(ListItem.book)
                
                Divider()
                
                Button(action:
                {
                    self.alertPresented = true
                },
                label:
                {
                    Label("", systemImage: "plus")
                })
                .buttonStyle(PlainButtonStyle())
                
                .alert("Reader Name:", isPresented: self.$alertPresented)
                {
                    TextField("", text: self.$userInput)
                    
                    Button("OK")
                    {
                        let reader = Reader(name: self.userInput)
                        self.modelContext.insert(reader)
                        self.project.readers?.append(reader)
                        self.selectedListItem = ListItem.reader(reader)
                        self.userInput = ""
                    }
                    .keyboardShortcut(.defaultAction)
                    
                    Button("Cancel", role: .cancel) { }
                }
                                    
                ForEach (self.readers , id: \.self)
                {
                    reader in Text(reader.name).tag(ListItem.reader(reader))
                }
            }
            .onAppear
            {
                self.selectedListItem = ListItem.book
            }
        }
        detail:
        {
            Text("Detail")
        }
    }
}

It was working fine until I added CloudKit to my project. Now, on occasion, when I add a reader, I get a message:

Fatal error: Duplicate keys of type 'Reader' were found in a Dictionary. This usually means either that the type violates Hashable's requirements, or that member of such a dictionary were mutated after insertion.

Also, this List will not work with CloudKit and update any added readers on other devices (when other screens work fine with CloudKit). When I completely remove enum ListItem and the "Book" item at the top, and simply display a list of readers using self.readers and self.selectedReader in place of self.selectedListItem, then everything works fine. So I believe the issue is with using the enum and combining the "Book" item and the list of readers into one, but I'm not understanding why. Any help would be appreciated! For your reference, below are my Project and Reader model classes:

import Foundation
import SwiftData

@Model
class Project
{
    var name: String = ""
    @Relationship(deleteRule: .cascade, inverse: \Reader.projects) var readers : [Reader]?
    
    init(name: String)
    {
        self.name = name
        self.readers = []
    }
}

import Foundation
import SwiftData

@Model
class Reader
{
    var name: String = ""
    
    var projects: [Project]?
    
    init(name: String)
    {
        self.name = name
    }
}

Solution

  • Was able to solve the problem from workingdog support Ukraine's suggestion to change ForEach(self.readers , id: \.self) to ForEach(readers).