0

I am trying to write a property wrapper to access userDefaults but have two problems. The code snippet is as below

User Default wrapper

@propertyWrapper
struct UserDefault<T> {
    private let key: String
    private let defaultValue: T
    private let userDefaults: UserDefaults

    init(key: String, defaultValue: T, userDefaults: UserDefaults = .standard) {
        self.key = key
        self.defaultValue = defaultValue
        self.userDefaults = userDefaults
    }

    var wrappedValue: T {
        get {
            return userDefaults.object(forKey: key) as? T ?? defaultValue
        }

        set {
            userDefaults.set(newValue, forKey: key)
        }
    }
}

Observable Object

final class AppSettings: ObservableObject {

    let objectWillChange = PassthroughSubject<Void, Never>()

    @UserDefault(key: "focusSliderValue", defaultValue: 25.0)
    var focusSliderValue: TimeInterval {
        willSet {
            objectWillChange.send()
        }
    }

    @UserDefault(key: "shortBreakLength", defaultValue: 5.0)
    var shortBreakLength: TimeInterval {
        willSet {
            objectWillChange.send()
        }
    }

    @UserDefault(key: "longBreakLength", defaultValue: 25.0)
    var longBreakLength: TimeInterval {
        willSet {
            objectWillChange.send()
        }
    }

    @UserDefault(key: "sessionsPerRound", defaultValue: 4)
    var sessionsPerRound: Int {
        willSet {
            objectWillChange.send()
        }
    }
}

The above code works but have the following issues

Problem 1:

I am trying to replace most of the boiler plate code with @Published keyword but when i do that I get "Value of type 'Double' has no member 'wrappedValue'" error which I am unable to understand/fix.

Problem 2:

Keeping the code as is, I am using a stepper to update "focusSliderValue" on my settings screen which gets updated on my Root View. If I press the stepper once and immediately go back to the root view get updated properly but if I press the stepper say 10 times then go back to the root view I see the screen with "..." (updating) symbol then the field gets updated after few secs. Why is this transition / observable object not smooth ?

Updated GIF for problem 2

Content View For problem 2

struct ContentView: View {
    @State var to : CGFloat = 1
    @State private var isAnimating = false
    @ObservedObject var appSettings = AppSettings()

    let gradientColors = Gradient(colors: [Color.blue, Color.purple])
    var body: some View {
        NavigationView {
            ZStack{
                VStack(spacing: 50) {
                    ZStack {
                        Circle().trim(from: 0, to: 1)
                            .stroke(Color.black.opacity(0.09), style: StrokeStyle(lineWidth: 10, lineCap: .round))
                            .frame(width: 200, height: 200)
                        Circle()
                            .trim(from: 0, to: self.isAnimating ? self.to : 0)
                            .stroke(Color.black, style: StrokeStyle(lineWidth: 10, lineCap: .round))
                            .frame(width: 200, height: 200)
                            .rotationEffect(.init(degrees: -90))
                            .overlay(Text("\(Int(appSettings.focusSliderValue))").font(.title), alignment: .center)
                            .animation(Animation.linear(duration: 5).repeatCount(0, autoreverses: false))
                    }

                    Button(action: {self.isAnimating.toggle()}, label: {
                        Text("Start")
                            .font(.body)
                            .foregroundColor(.black)
                            .padding(40)
                    }).padding()
                        .clipShape(Circle())
                        .overlay(Circle().stroke(Color.black, style: StrokeStyle(lineWidth: 2, lineCap: .round)))
                }.padding()
            }.navigationBarTitle(Text("Pomodoro Timer"), displayMode: .automatic)
                .navigationBarItems(trailing: NavigationLink(destination: SettingsView(appSettings: appSettings), label: {
                    Image(systemName: "gear")
                        .font(.title)
                        .foregroundColor(.black)
                }))
        }
    }
}

struct SettingsView: View {
    @ObservedObject var appSettings: AppSettings

    var body: some View {
        Form {
            Section(header: Text("TIMER LENGTH")) {
                HStack {
                    Stepper(value: $appSettings.focusSliderValue , in: 0...60, step: 1.0) {
                        Text("Focus Length : \($appSettings.focusSliderValue.wrappedValue)" + " mins")
                            .fontWeight(.bold)
                            .foregroundColor(.black)
                    }

                }
                HStack {
                    Stepper(value: $appSettings.shortBreakLength) {
                        Text("Short break Length : \(($appSettings.shortBreakLength.wrappedValue))" + " mins")
                            .fontWeight(.bold)
                            .foregroundColor(.black)
                    }
                }
                HStack {
                    Stepper(value: $appSettings.longBreakLength) {
                        Text("Long break Length : \(($appSettings.longBreakLength.wrappedValue))" + " mins")
                            .fontWeight(.bold)
                            .foregroundColor(.black)
                    }
                }
            }.font(.caption)
        }
    }
}

struct ContentView_Previews: PreviewProvider {

    static var previews: some View {
        ContentView(appSettings: AppSettings())
    }
}

Edit: Updated gift for problem 2

1 Answers1

0

Try with this variant

final class AppSettings: ObservableObject {

    @UserDefault(key: "focusSliderValue", defaultValue: 25.0)
    var focusSliderValue: TimeInterval {
        didSet {
            // uncomment for multi-threaded use and make same for others
            // DispatchQueue.main.async { 
            self.objectWillChange.send() // << use default publisher !!
            // }
        }
    }

    @UserDefault(key: "shortBreakLength", defaultValue: 5.0)
    var shortBreakLength: TimeInterval {
        didSet {
            self.objectWillChange.send()
        }
    }

    @UserDefault(key: "longBreakLength", defaultValue: 25.0)
    var longBreakLength: TimeInterval {
        didSet {
            self.objectWillChange.send()
        }
    }

    @UserDefault(key: "sessionsPerRound", defaultValue: 4)
    var sessionsPerRound: Int {
        didSet {
            self.objectWillChange.send()
        }
    }
}
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • This variation works for me but the problem 2 still exists for me. When i play around with stepper faster and move between screen I see "..." loading symbol in the root view. If it's just one stepper click things are rendering fine. I tried using the dispatch que but that doesnt seem to help me as well. Updated the original thread with the link to a gif for problem 2 – Murali Murugan Jun 07 '20 at 06:48