macosswiftuiswiftdatamenubarextra

Using SwiftData with a MenuExtra


I have a script which will add to a SwiftData store if the data doesn’t already exist. I’ve got the logic working, for an ordinary window but I can’t get it to work on a Menu Extra. Here is a sample:

import SwiftUI
import SwiftData

@main
struct XBApp: App {
    var bundleID: String = "whatever"

    
    var body: some Scene {
        MenuBarExtra("Something", systemImage: "questionmark.bubble") {
//      WindowGroup {
            VStack {
                XBContent(bundleID: bundleID)
                    .padding(0)
            }
            .modelContainer(for: XBData.self)
        }
    }
}

@Model
class XBData {
    @Attribute(.unique) var bundle: String
    var note: String
    
    init(bundle: String, note: String = "Note") {
        self.bundle = bundle
        self.note = note
    }
}

struct XBList: View {
    @Query(sort: \XBData.bundle, animation: .easeInOut) var allXBarData: [XBData]
    @Environment(\.modelContext) private var modelContext
    
    var body: some View {
        VStack {
            Text("Everything So Far …")
            List {
                ForEach(allXBarData) { xbd in
                    HStack {
                        Text(xbd.bundle)
                        Spacer()
                        Text(xbd.note)
                        Spacer()
                        Button("", systemImage: "trash", action: {
                            modelContext.delete(xbd)
                        })
                        .buttonStyle(.bordered)
                    }
                }
            }
        }
        .modelContainer(for: XBData.self)
    }
}

struct XBContent: View {
    var bundleID: String = ""
    @Environment(\.modelContext) private var modelContext
    @State var xBData: XBData?
    @Query(sort: \XBData.bundle, animation: .easeInOut) var allXBarData: [XBData]

    func loadData(bundleID: String) -> XBData? {
        print("\(#fileID):\(#line) - \(bundleID)")
//      let filter = FetchDescriptor<XBData>(predicate: #Predicate { $0.bundle == bundleID })
        let filter = FetchDescriptor<XBData>(predicate: #Predicate { $0.bundle == bundleID })
        do {
            var xbd = try modelContext.fetch(filter).first
            print("\(#fileID):\(#line) - \(xbd == nil)")
            if xbd == nil {
                print("\(#fileID):\(#line) - \(xbd!.note)")
                xbd = XBData(bundle: bundleID, note: "Newish Note: \(bundleID)")
                modelContext.insert(xbd!)
                try modelContext.save()
            }
            else {
                print("\(#fileID):\(#line) - \(xbd!.note)")
            }
            return xbd
        } catch {
            print("\(#line) oops")
            return nil
        }
    }
    
    init(bundleID: String) {
        self.bundleID = bundleID
    }
    
    var body: some View {
        VStack {
            Text(bundleID)
//          XBList()
            List {
                ForEach(allXBarData) { xbd in
                    HStack {
                        Text(xbd.bundle)
                        Spacer()
                        Text(xbd.note)
                        Spacer()
                        Button("", systemImage: "trash", action: {
                            modelContext.delete(xbd)
                        })
                        .buttonStyle(.bordered)
                    }
                }
            }
        }
        .modelContainer(for: XBData.self)
        .onAppear {
            xBData = loadData(bundleID: bundleID)!
            xBData!.note = "Newish Note: \(bundleID)"
            try? modelContext.save()
        }
    }
}

Sorry about the length of the sample.

Presuming an item with an id of whatever doesn’t exist, it’s supposed to add it and display it in the list.

In the App struct, I’ve commented out the WindowGroup. If I switch to that and not the MenuBarExtra, it works as expected.

I also get the very helpful message:

Can't find or decode reasons
Failed to get or decode unavailable reasons

However, I get that with either the WindowGroup or the MenuBarExtra, so I don’t know whether it’s related.

What is the trick getting this to work with a MenuBarExtra?


Solution

  • Try this approach to use MenuBarExtra

     @main
     struct XBApp: App {
         var bundleID: String = "whatever"
         
         var body: some Scene {
             MenuBarExtra("Something", systemImage: "questionmark.bubble") {
                 VStack {
                     XBContent(bundleID: bundleID)
                 }
             }
             .modelContainer(for: XBData.self)  // <--- here
             .menuBarExtraStyle(.window)
         }
     }
     
    

    Note, in your func loadData do not use ! in your code, specially in if xbd == nil { print("\(#fileID):\(#line) - \(xbd!.note)") ...

    Note also, stop using .modelContainer(for: XBData.self) everywhere, just one in your XBApp is enough.

    Note, when you do modelContext.insert(xbd!), it automatically save the data, you don't have to use try modelContext.save()

    EDIT-1

    This is the full code that works very well for me, tested on macOS 15.

    import SwiftUI
    import SwiftData
    
    @main
    struct XBApp: App {
        var bundleID: String = "whatever"
        
        var body: some Scene {
            MenuBarExtra("Something", systemImage: "questionmark.bubble") {
                VStack {
                    XBContent(bundleID: bundleID)
                }
            }
            .modelContainer(for: XBData.self)  // <--- here
            .menuBarExtraStyle(.window)
        }
    }
    
    struct XBList: View {
        @Environment(\.modelContext) private var modelContext
        @Query(sort: \XBData.bundle, animation: .easeInOut) var allXBarData: [XBData]
        
        
        var body: some View {
            VStack {
                Text("Everything So Far …")
                List {
                    ForEach(allXBarData) { xbd in
                        HStack {
                            Text(xbd.bundle)
                            Spacer()
                            Text(xbd.note)
                            Spacer()
                            Button("", systemImage: "trash", action: {
                                modelContext.delete(xbd)
                            })
                            .buttonStyle(.bordered)
                        }
                    }
                }
            }
        }
    }
    
    struct XBContent: View {
        var bundleID: String = ""
        
        @Environment(\.modelContext) private var modelContext
        
        @State private var xBData: XBData?
        
        @Query(sort: \XBData.bundle, animation: .easeInOut) var allXBarData: [XBData]
        
        func loadData(bundleID: String) -> XBData? {
            print("\(#fileID):\(#line) - \(bundleID)")
            let filter = FetchDescriptor<XBData>(predicate: #Predicate { $0.bundle == bundleID })
            do {
                var xbd = try modelContext.fetch(filter).first
                print("\(#fileID):\(#line) ---> xbd is nil \(xbd == nil)")
                if xbd == nil {
                    xbd = XBData(bundle: bundleID, note: "A Newish Note: \(bundleID)")
                    modelContext.insert(xbd!)
                    // try modelContext.save()
                    print("\(#fileID):\(#line) ---> \(xbd!.note)")
                }
                else {
                    print("\(#fileID):\(#line) - \(xbd!.note)")
                }
                return xbd
            } catch {
                print("\(#line) oops")
                return nil
            }
        }
        
        var body: some View {
            VStack {
                
                // for testing, adding some test XBData
                Button(action: {
                    let randm = String(UUID().uuidString.prefix(5))
                    let newItem = XBData(bundle: randm, note: randm)
                    modelContext.insert(newItem)
                }) {
                    Label("Add Item", systemImage: "plus")
                }.buttonStyle(.bordered)
                    .padding(10)
                
                Text(bundleID)
                
                XBList()
                
            }
            .onAppear {
                if let xb = loadData(bundleID: bundleID) {
                    xBData = xb
                    xBData!.note = "B Newish Note: \(bundleID)"
                    print("\(#fileID):\(#line) ---> \(xBData!.note)")
                    //try? modelContext.save()
                }
            }
        }
    }
    
    #Preview {
        XBContent(bundleID: "whatever")
            .modelContainer(for: XBData.self, inMemory: true)
    }
    
    @Model
    class XBData {
        @Attribute(.unique) var bundle: String
        var note: String
        
        init(bundle: String, note: String = "Note") {
            self.bundle = bundle
            self.note = note
        }
    }