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?
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
}
}