I am building a MenuBar Extra which relates to the current application. I get the bundle id from the current application and use it to fetch or create new data. I am trying to pass data from one View to another for editing.
I want to keep a copy of the original value in the editing view for comparison.
I have created a local @State
variable to store the data, and assigned it in the .onAppear
part.
The problem is when the though the editing view correctly shows the data for the current value of the bundle id, the copy shows the data for the previous value.
Added for clarity:
For example, when I open the MenuBar app with XCode as the focus, I get:
If I switch to Firefox and open the app, I get:
Note that the “Original” is the value for the previous time. If I open on Firefox again, I get:
… so not it’s up to date.
I can’t think of any other way of initialising the variable that works.
How can I initialise the local variable correctly?
import SwiftUI
import SwiftData
@main
struct XBApp: App {
@State var bundleID: String = ""
var body: some Scene {
MenuBarExtra("Something", systemImage: "questionmark.bubble") {
VStack {
SampleContent(bundleID: $bundleID)
.modelContainer(for: SampleData.self)
.padding(0)
}
.onAppear {
bundleID = NSWorkspace.shared.frontmostApplication!.bundleIdentifier!
dbug(bundleID)
}
}
.menuBarExtraStyle(.window)
}
}
@Model
class SampleData {
@Attribute(.unique) var bundle: String
var data: String
init(bundle: String, data: String) {
self.bundle = bundle
self.data = data
}
}
struct EditSampleData: View {
@Environment(\.modelContext) private var modelContext
@Bindable var sampleData: SampleData
@State var original: String = ""
var body: some View {
Form {
Text(sampleData.bundle)
Text(original) // out of step - shows previous version
TextField("Data", text: $sampleData.data)
}
.onAppear {
original = sampleData.data
}
}
}
struct SampleContent: View {
@Binding var bundleID: String
@Environment(\.modelContext) private var modelContext
@State var sampleData: SampleData?
func loadData(bundleID: String) -> SampleData? {
let filter = FetchDescriptor<SampleData>(predicate: #Predicate { $0.bundle == bundleID })
do {
if let sd = try modelContext.fetch(filter).first {
return sd
} else {
let sd = SampleData(bundle: bundleID, data: "New Data: \(bundleID)")
modelContext.insert(sd)
return sd
}
} catch {
return nil
}
}
var body: some View {
VStack {
if let sd = sampleData {
Text(sd.bundle)
EditSampleData(sampleData: sd)
}
}
.onAppear {
sampleData = loadData(bundleID: bundleID)!
}
}
}
Not really clear what you want to achieve with this setup,
but to make Text(original)
display the changes that you make to sampleData.data
,
try this approach:
Text(original) // <-- no longer out of step
TextField("Data", text: $sampleData.data)
.onChange(of: sampleData.data) {
original = sampleData.data
}
EDIT-1
Try this approach declaring the original
one step up from the EditSampleData
, such as:
struct EditSampleData: View {
@Environment(\.modelContext) private var modelContext
@Bindable var sampleData: SampleData
let original: String // <--- here
init(sampleData: SampleData, original: String) { // <--- here
self.sampleData = sampleData
self.original = original
}
var body: some View {
Form {
Text(sampleData.bundle)
Text(original)
TextField("Data", text: $sampleData.data)
}
}
}
struct SampleContent: View {
@Binding var bundleID: String
@Environment(\.modelContext) private var modelContext
@State var sampleData: SampleData?
@State var original: String = "" // <--- here
var body: some View {
VStack {
if let sd = sampleData {
Text(sd.bundle)
EditSampleData(sampleData: sd, original: original) // <--- here
}
}
.onAppear {
if let xdata = loadData(bundleID: bundleID) { // <--- here
sampleData = xdata
original = xdata.data
}
}
}
func loadData(bundleID: String) -> SampleData? {
let filter = FetchDescriptor<SampleData>(predicate: #Predicate { $0.bundle == bundleID })
do {
if let sd = try modelContext.fetch(filter).first {
return sd
} else {
let sd = SampleData(bundle: bundleID, data: "New Data: \(bundleID)")
modelContext.insert(sd)
return sd
}
} catch {
return nil
}
}
}