macosswiftuiswiftdata

Initialising a local variable in SwiftUI


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:

enter image description here

If I switch to Firefox and open the app, I get:

enter image description here

Note that the “Original” is the value for the previous time. If I open on Firefox again, I get:

enter image description here

… 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)!
        }
    }
}

Solution

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