0

Progressing on UserDefaults insanity in SwiftUI, after a previous post on basic UserDefaults which basically exposed the need to use a String() wrapper around UserDefaults values...

I am now stomped by the data flow :

The idea is to present a DatePicker, set to a UserDefaults value registered in AppDelegate on first launch. Subsequently, the user picks another date that is set to the UserDefaults. But every time I launch the app after having "killed" it (i.e swiped up from app switcher), the Picker displays the present date and NOT the one last saved in UserDefaults.

Also, I display some texts above and below the picker to try and make sense of the data flow, but it seems that there is a one step lag in the displaying of the dates, if anyone has the time to give it a try, here is the code :

1- In AppDelegate, I register my initial UserDefaults (like I always did in UIKit) :

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch
        UserDefaults.standard.register(defaults: [
        "MyBool 1": true,
        "MyString": "Soo",
        "MyDate": Date()
        ])

        return true
    }

2- in ContentView, I try to display them :

import SwiftUI

struct ContentView: View {

    let defaults = UserDefaults.standard

    var dateFormatter: DateFormatter {
     let formatter = DateFormatter()
    // formatter.dateStyle = .long
     formatter.dateFormat = "dd MMM yy"
     return formatter
     }

    @State var selectedDate = Date()

    init() {
        self.loadData() // This should set "selectedDate" to the UserDefaults value
    }

    var body: some View {

        VStack {
            Text("The BOOL 1 value is : Bool 1 = \(String(defaults.bool(forKey: "MyBool 1")))")
            Divider()
            Text("My string says : \(String(defaults.string(forKey: "MyString")!))")
            Divider()
            Text("The date from UserDefaults is : \(dateFormatter.string(from: defaults.object(forKey: "MyDate") as! Date))")
            Divider()
            DatePicker(selection: $selectedDate, label: { Text("") })
                .labelsHidden()
                .onReceive([self.selectedDate].publisher.first()) { (value) in
                    self.saveDate()
                }

            Divider()
            Text("The chosen date is : \(selectedDate)")
        }
    }

    func loadData() {
        selectedDate = defaults.object(forKey: "MyDate") as! Date
        print("----> selected date in \"init\" from UserDefaults: \(dateFormatter.string(from: selectedDate) )) ")
    }

    private func saveDate() { // This func is called whenever the Picker sends out a value
        UserDefaults.standard.set(selectedDate, forKey: "MyDate")
        print("Saving the date to User Defaults : \(dateFormatter.string(from: selectedDate) ) ")
    }

}

Any help would be appreciated !

Esowes
  • 287
  • 2
  • 13
  • I guess registering the default values in `applicationDidFinishLaunching` is too late. You have to register them as soon as possible at least before using them. Override `init` and move the code in there. – vadian Apr 18 '20 at 07:46

3 Answers3

1

try this:

struct ContentView: View {

    let defaults = UserDefaults.standard

    var dateFormatter: DateFormatter {
     let formatter = DateFormatter()
    // formatter.dateStyle = .long
     formatter.dateFormat = "dd MMM yy"
     return formatter
     }

    @State var selectedDate : Date

    init() {
        _selectedDate = State(initialValue: UserDefaults.standard.object(forKey: "MyDate") as! Date) // This should set "selectedDate" to the UserDefaults value
    }

    var body: some View {

        VStack {
            Text("The BOOL 1 value is : Bool 1 = \(String(defaults.bool(forKey: "MyBool 1")))")
            Divider()
            Text("My string says : \(String(defaults.string(forKey: "MyString")!))")
            Divider()
            Text("The date from UserDefaults is : \(dateFormatter.string(from: defaults.object(forKey: "MyDate") as! Date))")
            Divider()
            DatePicker(selection: $selectedDate, label: { Text("") })
                .labelsHidden()
                .onReceive([self.selectedDate].publisher.first()) { (value) in
                    self.saveDate()
                }

            Divider()
            Text("The chosen date is : \(selectedDate)")
        }
    }

    func loadData() {
        selectedDate = defaults.object(forKey: "MyDate") as! Date
        print("----> selected date in \"init\" from UserDefaults: \(dateFormatter.string(from: selectedDate) )) ")
    }

    private func saveDate() { // This func is called whenever the Picker sends out a value
        UserDefaults.standard.set(selectedDate, forKey: "MyDate")
        print("Saving the date to User Defaults : \(dateFormatter.string(from: selectedDate) ) ")
    }

}
Chris
  • 7,579
  • 3
  • 18
  • 38
  • Awesome. Learning a ton of stuff here. I guess the third text does not update its date value because the Stack is re-rendered as soon as selectedDate is changed, BEFORE saveDate is called ? – Esowes Apr 18 '20 at 10:48
  • exactly - the saveDate does not trigger an UI update...if you want this, you can try my 2nd answer... – Chris Apr 18 '20 at 12:54
1

here is my 2nd answer, if you want to update the text also...it is not "nice" and for sure not the best way, but it works (i have tested it)

struct ContentView: View {

    let defaults = UserDefaults.standard

    var dateFormatter: DateFormatter {
     let formatter = DateFormatter()
    // formatter.dateStyle = .long
     formatter.dateFormat = "dd MMM yy"
     return formatter
     }
    @State var uiUpdate : Int = 0
    @State var selectedDate : Date
    @State var oldDate : Date = Date()

    init() {
        _selectedDate = State(initialValue: UserDefaults.standard.object(forKey: "MyDate") as! Date) // This should set "selectedDate" to the UserDefaults value
    }

    var body: some View {

        VStack {
            Text("The BOOL 1 value is : Bool 1 = \(String(defaults.bool(forKey: "MyBool 1")))")
            Divider()
            Text("My string says : \(String(defaults.string(forKey: "MyString")!))")
            Divider()
            Text("The date from UserDefaults is : \(dateFormatter.string(from: defaults.object(forKey: "MyDate") as! Date))")
                .background(uiUpdate < 5 ? Color.yellow : Color.orange)
            Divider()
            DatePicker(selection: $selectedDate, label: { Text("") })
                .labelsHidden()
                .onReceive([self.selectedDate].publisher.first()) { (value) in
                    if self.oldDate != value {
                        self.oldDate = value
                        self.saveDate()
                    }
                }

            Divider()
            Text("The chosen date is : \(selectedDate)")
        }
    }

    func loadData() {
        selectedDate = defaults.object(forKey: "MyDate") as! Date
        print("----> selected date in \"init\" from UserDefaults: \(dateFormatter.string(from: selectedDate) )) ")
    }

    private func saveDate() { // This func is called whenever the Picker sends out a value
        UserDefaults.standard.set(selectedDate, forKey: "MyDate")
        print("Saving the date to User Defaults : \(dateFormatter.string(from: selectedDate) ) ")
        uiUpdate = uiUpdate + 1
    }

}
Chris
  • 7,579
  • 3
  • 18
  • 38
-3

Every time you start the App, it re-register the defaults. You could use this:

    if !UserDefaults.standard.bool(forKey: "first time only") {
        UserDefaults.standard.register(defaults: [
            "first time only": true,
            "MyBool 1": true,
            "MyString": "Soo",
            "MyDate": Date()
        ])
    }
  • 2
    Folks, please read the documentation what `register`ing default values means. – vadian Apr 18 '20 at 07:44
  • correct, I'm learning about register. According to the documentation "Registered defaults are never stored between runs of an application, and are visible only to the application that registers them." So does it means that no matter what you put in those during a run, it will not persist. – workingdog support Ukraine Apr 18 '20 at 08:24
  • Actually, no : defaults are NOT re-registered. My code actually reflects this. The third text “The date from UserDefaults is” displays the correct user-set default. – Esowes Apr 18 '20 at 09:47
  • sorry, my bad, I did not grasp the nature of the problem at first, focused on the wrong issue. @Chris answer works well. – workingdog support Ukraine Apr 18 '20 at 10:09