SwiftUI AppStorage, UserDefaults and ObservableObject

Viewed 1655

I have two different views, ContentView and CreateView. In CreateView, I get user's inputs by textfield, and once user clicks on Save button, the inputs will be stored in AppStorage. Then, I want to display the saved inputs on ContentView. Here, I tried to use State & Binding but it didn't work out well. How would I use the variable, that is created in CreateView, in ContentView? what property should I use..

Thanks


Here's the updated questions with the code...

struct ContentView: View {
    // MARK: - PROPERTY
    @ObservedObject var appData: AppData
    let createpage = CreatePage(appData: AppData())
    
    var body: some View {
        HStack {
            NavigationLink("+ create a shortcut", destination: CreatePage(appData: AppData()))
                .padding()
            Spacer()
        } //: HStack - link to creat page
        VStack {
            Text("\(appData.shortcutTitle) - \(appData.shortcutOption)")
        }
        
    }
    
    struct CreatePage: View {
        // MARK: - PROPERTY
        @AppStorage("title") var currentShortcutTitle: String?
        @AppStorage("option") var currentOption: String?
        @Environment(\.presentationMode) var presentationMode
        @ObservedObject var appData: AppData
        
        var body: some View {
            NavigationView{
                ScrollView{
                    Text("Create a ShortCut")
                        .padding()
                    
                    HStack {
                        TextField("what is the title?", text: $appData.titleInput)
                            .textFieldStyle(RoundedBorderTextFieldStyle())
                            //.frame(width: 150, height: 60, alignment: .center)
                            .border(Color.black)
                            .padding()
                    } //: HStack - Textfield - title
                    .padding()
                    
                    HStack (spacing: 10) {
                        TextField("options?", text: $appData.optionInput)
                            .textFieldStyle(RoundedBorderTextFieldStyle())
                            .frame(width: 80, height: 40, alignment: .leading)
                            .padding()
                    } //: HStack - Textfield - option
                    .padding()
                    
                    Button(action: {
                        self.appData.shortcutTitle = self.appData.titleInput
                        self.appData.shortcutOption = self.appData.optionInput
                        UserDefaults.standard.set(appData.shortcutTitle, forKey: "title")
                        UserDefaults.standard.set(appData.shortcutOption, forKey: "option")
                        presentationMode.wrappedValue.dismiss()
                    }, label: {
                        Text("Save")
                            .padding()
                            .frame(width: 120, height: 80)
                            .border(Color.black)
                    }) //: Button - save
                    .padding(.top, 150)
                } //: Scroll View
            } //: Navigation View
        } //: Body
        
        class AppData: ObservableObject {
            @Published var shortcutTitle : String = "Deafult Shortcut"
            @Published var shortcutOption : String = "Default Option"
            @Published var titleInput : String = ""
            @Published var optionInput : String = ""
        }

So the problem here is that

  1. when I put new inputs on CreatePage and tab the save button, the new inputs do not appear on ContentView page.The output keeps showing the default values of title and option, not user inputs.
  2. If user makes a new input and hit the save button, I want to store them in AppStorage, and want the data to be kept on ContentView (didn't make the UI yet). Am I using the AppStorage and UserDefaults in a right direction?

If anyone have insights on these issues.. would love to take your advice or references.

3 Answers

You're creating instances of AppData in multiple places. In order to share data, you have to share one instance of AppData.

I'm presuming that you create AppData in a parent view of ContentView since you have @ObservedObject var appData: AppData defined at the top of the view (without = AppData()). This is probably in your WindowGroup where you also must have a NavigationView.

I removed the next (let createpage = CreatePage(appData: AppData())) because it does nothing. And in the NavigationLink, I passed the same instance of AppData.


struct ContentView: View {
    // MARK: - PROPERTY
    @StateObject var appData: AppData = AppData() //Don't need to have `= AppData()` if you already create it in a parent view
    
    var body: some View {
        // I'm assuming there's a NavigationView in a parent view
        VStack { //note that I've wrapped the whole view in a VStack to avoid having two root nodes (which can perform differently in NavigationView depending on the platform)
            HStack {
                NavigationLink("+ create a shortcut", destination: CreatePage(appData: appData))
                    .padding()
                Spacer()
            } //: HStack - link to creat page
            VStack {
                Text("\(appData.shortcutTitle) - \(appData.shortcutOption)")
            }
        }
    }
}

struct CreatePage: View {
    // MARK: - PROPERTY
    @AppStorage("title") var currentShortcutTitle: String?
    @AppStorage("option") var currentOption: String?
    @Environment(\.presentationMode) var presentationMode
    @ObservedObject var appData: AppData
    
    var body: some View {
        NavigationView{
            ScrollView{
                Text("Create a ShortCut")
                    .padding()
                
                HStack {
                    TextField("what is the title?", text: $appData.titleInput)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                        //.frame(width: 150, height: 60, alignment: .center)
                        .border(Color.black)
                        .padding()
                } //: HStack - Textfield - title
                .padding()
                
                HStack (spacing: 10) {
                    TextField("options?", text: $appData.optionInput)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                        .frame(width: 80, height: 40, alignment: .leading)
                        .padding()
                } //: HStack - Textfield - option
                .padding()
                
                Button(action: {
                    appData.shortcutTitle = appData.titleInput
                    appData.shortcutOption = appData.optionInput
                    presentationMode.wrappedValue.dismiss()
                }, label: {
                    Text("Save")
                        .padding()
                        .frame(width: 120, height: 80)
                        .border(Color.black)
                }) //: Button - save
                .padding(.top, 150)
            } //: Scroll View
        } //: Navigation View
    } //: Body
}

Regarding @AppStorage and UserDefaults, it's a little hard to tell what your intent is at this point with those. But, you shouldn't need to declare AppStorage and call UserDefaults on the same key -- @AppStorage writes to UserDefaults for you. Read more at https://www.hackingwithswift.com/quick-start/swiftui/what-is-the-appstorage-property-wrapper

you can use a singleton ObservableObject that conforms to NSObject so you can observe everything even older apple objects like progress.

class appData : NSObject , ObservableObject {
    static let shared = appData()
    @Published var localItems = Array<AVPlayerItem>()
    @Published var fractionCompleted : Double = 0
    @Published var downloaded : Bool = false
    @Published var langdentifier = UserDefaults.standard.value(forKey: "lang") as? String ?? "en" {
        didSet {
            print("AppState isLoggedIn: \(langIdentifier)")
        }
    }
    var progress : Progress?
    override init() {
    }
}

then u can use it anywhere in your code like this:

appData.shared.langIdentifier == "en" ? .leading : .trailing

You should be able to simply put AppStorage objects in the ObservableClass and call them from there. There should be no need to put AppStorage in the View and then read from UserDefaults in the class.

class AppData: ObservableObject {
    @AppStorage(keyForValue) var valueForKey: ValueType = defaultValue
    ...
}

Of course, you could make @Published property in the class and define the getter and setter for it so it reads and writes directly to the UserDefaults but at that point, you're just creating more work than simply using AppStorage from the beginning directly in the class.

Related