NEW: Learn to build amazing SwiftUI apps for macOS with my new book! >>

How would you model this situation where I want to use a custom property wrapper but also want @Published functionality?

Forums > SwiftUI

I'm building a settings form and am trying to find a good way to model things so that setting and getting values on the viewModel get passed through directly to the UserDefaults via a property wrapper. Here's a minimal example:

@propertyWrapper
struct UserDefault<T> {

    let key: String

    let defaultValue: T

    var wrappedValue: T {
        get { UserDefaults.standard.value(forKey: key) as? T ?? defaultValue }
        set { UserDefaults.standard.set(newValue, forKey: key) }
    }

    init(wrappedValue: T, _ key: String) {
        self.key = key
        self.defaultValue = wrappedValue
    }
}

// ⚠️ I can't use @Published in addition to my custom @UserDefault here ⚠️
class ViewModel: ObservableObject {
    @UserDefault("some-user-setting") var someUserSetting = false
    @UserDefault("another-user-setting") var anotherUserSetting = true
    // ...
}

struct MyView: View {
    @StateObject private var viewModel = ViewModel()

    var body: some View {
        Form {
            Toggle("Some user setting", isOn: $viewModel.someUserSetting)
            Toggle("Another user setting", isOn: $viewModel.anotherUserSetting)
        }
    }
}

Now my viewModel is an ObservableObject but doesn't use @Published anywhere because if I try to combine the two (@Published @UserDefault(...) ...) I get an error Key path value type 'Bool' cannot be converted to contextual type 'UserDefault<Bool>'.

I suppose I could append a { didSet { objectWillChange.send() } } after each property on my viewModel to trigger the updates but that's also very ugly.

Importantly, I need to support iOS 13, otherwise I could use @AppStorage which is available from iOS 14+.

How would you model this elegantly and efficiently? Any ideas?

1      

Reading through description I was wondering why would you want this if there is AppStorage? But this missing feature on iOS 13 is a real pain... I know, as I have also not yet let go my iOS 13 app.

If you like, you can take a look at my reimplementation of AppStorage for iOS 13 in my SwiftUI-Generations project:

https://github.com/pd95/SwiftUI-Generations/blob/main/SwiftUIShim/AppStorage.swift

It does work as much as I've tested it. There are even some unit tests to prove it. Looking at the code alone, it seems to be overly complicated. But I tried to mimick the full API Apple uses internally.

I think the most important things in my implementation are:

  • you need to model your property wrapper as a DynamicProperty (AppStorage)
  • and internally an @ObservedObject to track the current value and propagate its changes automatically (UserDefaultLocation)

Perhaps my implementation of AppStorage would be even helpful for you as a drop-in replacement of the iOS 14 version?

2      

BTW: There is also an implementation of @SceneStorage and (especially interesting for iOS 13) @StateObject in the project above.

These are just my custom implementations... not necessarily production ready. But perhaps still helpful to anybody.

2      

If you like, you can take a look at my reimplementation of AppStorage for iOS 13 in my SwiftUI-Generations project

Oh wow, thank you so much – that's indeed incredibly helpful! I've since started taking the path of including @Published functionality in my own @UserDefault property wrapper by accessing the property wrapper's enclosing instance but it feels a little dangerous to rely on underscore-prefixed language features like that one in production code so I may course-correct.

BTW: There is also an implementation of @SceneStorage and (especially interesting for iOS 13) @StateObject in the project above.

I'm not sure what's going on here but I'm using @StateObject in this app targeting iOS 13 and it works just fine without giving me an error, unlike @AppStorage. Didn't even know the docs said @StateObject is only iOS 14+. 🤔 Is this a mistake in the docs, or how can that be?

EDIT: Ha, it turns out I just didn't test @StateObject on iOS 13 yet. It does compile without error or warning, but then crashes with Thread 1: signal SIGABRT. Now that's not good.

1      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Spend less time managing in-app purchase infrastructure so you can focus on building your app. RevenueCat gives everything you need to easily implement, manage, and analyze in-app purchases and subscriptions without managing servers or writing backend code.

Get Started

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.