0

I am trying to save Checkin time preferences of a user (to send them notification reminders). So far I've been able to save a Bool and string to UserDefaults but I'm unable to figure out how to save the time preference. Here's my UserSettings class to store the preferences.

public class UserSettings: ObservableObject {
    
    
    @Published var eveningCheckin: Bool {
        didSet{
            UserDefaults.standard.set(eveningCheckin, forKey: "eveningCheckin")
            print("Evening checkin toggle value in didSET to \(self.eveningCheckin)")
            
        }
    }
    
    @Published var eveningCheckinTime: Date {
        didSet{
            UserDefaults.standard.set(eveningCheckinTime, forKey: "eveningCheckinTime")
            print("Evening checkin didSet to \(self.eveningCheckinTime)")
        }
    }
    
    
    init() {

        self.eveningCheckin = UserDefaults.standard.object(forKey: "eveningCheckin") as? Bool ?? false
        self.eveningCheckinTime = UserDefaults.standard.object(forKey: "eveningCheckinTime") as? Date ?? Date(timeIntervalSince1970: 64800)
        print("Evening checkin time init to \(self.eveningCheckinTime)")// --> To debug
        print("Evening checkin toggle value in init to \(self.eveningCheckin)")
    }
}

I want to set this Evening checkin time up with a Time picker that SwiftUI provides like this. Here are my Settings and Timepicker views.

import SwiftUI

struct SettingsMain: View {
    @ObservedObject var userSettings = UserSettings()
    @State private var showTimepickerEvening = false

    var body: some View {
        NavigationView{
            ScrollView {
                VStack {
                    //Evening checkins
                    Button(action: {
                        //Show  Evening time picker sheet
                        self.showTimepickerEvening.toggle()
                    }) {
                        HStack {
                            Text("\(userSettings.eveningCheckinTime.hour12):\(userSettings.eveningCheckinTime.minute0x) \(userSettings.eveningCheckinTime.amPM.lowercased()) ")
                            Text("Change >")
                        }
                    }
                    .sheet(isPresented: $showTimepickerEvening) {
                        //Sheet view with the Timepicker
                        TimePickerView(pickedTime: self.$userSettings.eveningCheckinTime)
                    }
                }
                .buttonStyle(PlainButtonStyle())
            }
            .navigationBarTitle("Preferences", displayMode: .inline)
        }
    }
}

struct TimePickerView: View {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    @Binding var pickedTime: Date
    
    var body: some View {
        VStack {
            DatePicker("Checkin time", selection: $pickedTime, displayedComponents: .hourAndMinute)
            .labelsHidden()
            
            Button(action: {
                    //Dimiss. Should I actually update my UserDefaults here as well?
                self.presentationMode.wrappedValue.dismiss()
                }) {
                    ZStack {
                        ColorManager.buttonGrey
                        Text("Save time")
                            .font(.system(size: 20))
                            .fontWeight(.semibold)
                    }
                    .frame(height: 64)
                }
            .padding(.all)
            .buttonStyle(PlainButtonStyle())
        }
        .background(Color.white)
    }
}

My problem is that the Time picker does setup the time into the @Published var BUT it get's reset with the init statement. It does not happen with the Bool value. Why??

Here's the output from the print() statements above: Basically means that every time I'm trying to use the Datepicker to set the time -> didSet does choose the new time but init() resets it back to default.

Evening checkin didSet to 1970-01-01 12:12:00 +0000

Evening checkin time init to 1970-01-01 18:00:00 +0000

Evening checkin toggle value in init to true

Evening checkin didSet to 1970-01-01 12:13:00 +0000

Evening checkin time init to 1970-01-01 18:00:00 +0000

Evening checkin toggle value in init to true

Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
Adit Gupta
  • 863
  • 3
  • 8
  • 23
  • How are you storing `UserSettings` on your view which contains the `DatePicker`? Please include the code for your view as well. – Dávid Pásztor Sep 08 '20 at 15:31
  • If you change your question my answer will not make any sense – Leo Dabus Sep 08 '20 at 15:48
  • Agreed Leo, My code had your answer in place. The bug in storing the time is still prevalent. Sorry about my mess up. – Adit Gupta Sep 08 '20 at 15:50
  • @AditGupta your cast it is not successful so you are getting the default value you have chosen with the nil coalescing operator. Are you sure didSet method is getting called? – Leo Dabus Sep 08 '20 at 15:54
  • Yes the didSet is called, I've added the print statement in the didSet which correctly fires with the time in the Picker's UI. ^^If you see I've added the print logs there from the didSet. – Adit Gupta Sep 08 '20 at 15:56

1 Answers1

1

The issue with how you store and annotate your view model. You should never be creating an @ObservedObject in the View itself, but rather injecting it. Whenever an @ObservedObject's @Published property changes, the View which stores that object will be reloaded - this means that if you are initialising that @ObservedObject inside the View, a new instance of that object will be created.

You need to inject the object into your view to avoid recreating it every time your view is refreshed.

struct SettingsMain: View {
    @ObservedObject var userSettings: UserSettings
    @State private var showTimepickerEvening = false
...

And from wherever you create SettingsMain (let's call it MainView as an illustration), create UserSettings there and annotate it as @State (or if you are creating it from something other than a View - say a ViewModel, then make it @Published):

struct MainView: View {
    @State var userSettings = UserSettings()

    var body: some View {
        SettingsMain(userSettings: userSettings)
    }
}
Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
  • David, is it possible to instantiate a class like UserSetting() with @Published? I'm getting "'wrappedValue' is unavailable: @Published is only available on properties of classes" error from XCode. – Adit Gupta Sep 08 '20 at 16:05
  • @AditGupta my bad, that's because `MainView` is a `struct` and `struct`s cannot have `@Published` properties. If you're going to instantiate `UserSettings` in a `View`, make it a `@State`, if you're going to create it from another class (say a ViewModel), make it `@Published`. Check my updated answer. – Dávid Pásztor Sep 08 '20 at 16:09
  • @DávidPásztor This seems to be working for me - thank you. But should it be a `@StateObject` instead of a `@State` variable? [This article](https://levelup.gitconnected.com/state-vs-stateobject-vs-observedobject-vs-environmentobject-in-swiftui-81e2913d63f9) makes it seem like using a `@State` object wouldn't update based on all the individual `@Published` properties in UserSettings. – Charlie Page Jun 19 '21 at 15:10
  • @Charlie if you're targeting iOS 14, then yes, `@StateObject` is more appropriate. However, `@StateObject` was not available at the time of writing my answer, since it was only introduced in iOS 14. – Dávid Pásztor Jun 28 '21 at 08:21