How should I access @AppStorage and @SceneStorage values outside of views?

The two property wrappers @AppStorage and @SceneStorage are great because you can simply redeclare a property in multiple views to share the value and automatically update other views.

But how should I access those same values outside of views?

I have helper functions which are used in multiple views and which change behaviour based on those stored properties. I believe I could use the old NSUserDefaults.standard to reference the @AppStorage values, but is that the "right" way to do it, and how do I access @SceneStorage values?
Post not yet marked as solved Up vote post of zkarj735 Down vote post of zkarj735
7.9k views

Replies

I have the same question. Right now, I'm importing SwiftUI in my other classes and using @AppStorage the same as I would inside a View. But importing SwiftUI outside of a View seems like not the right thing to do.

That said, I don't want to have use UserDefaults directly in some parts of my code, and @AppStorage in others. And, as you point out, @SceneStorage is another use case.

I'd be interested in what others think.
I didn't think it worked outside Views at all? Maybe I'm doing something different (wrong?). In my ContentView.swift (from the standard Xcode template), I have my ContentView struct defined as a TabView containing three other views. all also defined in the same file, along with a bunch of enums and some standalone functions outside of any struct/class/enum. I cannot declare an @AppStorage property in one of those functions. It gives me the error Property wrappers are not yet supported on local properties.

From what you're saying @rob-secondstage, by declaring these functions inside a helper class or struct, I could declare the wrapper at the class/struct level and thus use it in all of the member functions?
I don't really understand your question, but I wrote an article about everything you should about @AppStorage and @SceneStorage on Medium, which might answer your question better.

Perhaps you don't realize that with defining @AppStorage you defined a source-of-truth. If you need to access it "outside" the views perhaps its better to place the @AppStorage somewhere higher in the view hierarchy and pass it with bindings.

Links:
  • Introducing @AppStorage in SwiftUI (medium.com/swlh/introducing-appstorage-in-swiftui-470a56f5ba9e)

  • Introducing @SceneStorage in SwiftUI: (medium.com/@crystalminds/introducing-scenestorage-in-swiftui-5a4ec1a90ca3)



Thanks @crystalminds. They are nicely written articles and from a quick read appear to cover material I am not 100% familiar with so I will definitely revisit.

However, the answer to my problem does not lie in them. You suggested putting @AppStorage higher in the view hierarchy, but that is the root of my problem. I have helper methods that *do not belong to any view* because they are shared by multiple, unrelated views. Attaching such a method to a view would solve the storage problem but make them a real pain to call, if that's even possible.

At this time I have them declared as plain functions (outside any class), though @rob-secondstage suggested they be added to a class and that @AppStorage may work there. In theory the raw use of UserDefaults would work around this for @AppStorage (though I could not get it to work) but that still leaves the question around @SceneStorage values.

The use case is quite simple. User sets a preference in a settings screen which is then used in calculations for multiple views. Rather than repeat the calculations across multiple views, the code is factored out into helper methods, which need access to the preference values. The only "logical" way I have found so far is to pass in the preference values every time I call the helper functions but that is a kind of unnecessary handling as the views themselves don't actually need the raw preference value, only the result of the calculation.
  • I recognize this is an old thread, but I've been struggling with this same issue for days now. I went through a newb's natural progression of renewed hope and disappointment through @State, then @Observable, then @EnvironmentObject, and now @AppStorage. I'm confident that there's a good solution for this, as OP's question must be a common scenario. But I can't find anything yet in StackOverflow, Youtube, or the Apple forums.

Add a Comment

I just stumbled across this question. I know this is an old thread, but I know the answer for AppStorage. AppStorage is just a more convenient way of writing to UserDefaults. When you're not in a View just use the standard functions for dealing with UserDefaults (UserDefaults.standard.value, UserDefaults.standard.string, etc). I know this works because when I rewrote my app's UI in SwiftUI I had my Views use AppStorage but my underlying, non-view code I left untouched that was using UserDefaults.standard and it continued to work. Just make sure to use the same name in both places.

I've got no idea what the proper/best solution is for this, though I'm keenly watching this thread hoping to find it, but here's what I'm doing in the meantime. Again, it feels like it's not the right/best way of doing it, hence I'm watching this thread, but it is at least working in the meantime.

I declare all my @AppStorage properties in a UserPreferences class which conforms to @ObservableObject. So I have to import Combine and SwiftUI for the class, and it does seem a little odd to import SwiftUI for a class rather than a View, but it works (or seems to).

I then just access any of my @AppStorage properties from the UserPreferences class, whether in another View or a global function or some other struct.

I know you can just re-declare the @AppStorage properties anywhere, but this way I at least only have to write the code once and don't have to worry about typing the keys/names differently or whatever.

Would love to know if there's a better way of doing this.

I remember reading something in one of Apple's recent release notes about adding support for AppStorage in ObservableObjects. I don't recall the exact build, but if someone has time to dig through release notes, it would be nice to have the reference. In other words, it should work in the latest SwiftUI release.

Add a Comment

I'm facing the same issue with a custom property wrapper I created to manage in-app settings (like user's preferred color scheme regardless the system's scheme), the struct im having is this:

import SwiftUI

class SettingKeys: ObservableObject {
    @AppStorage("colorScheme") var colorScheme: AppColorScheme?
}

@propertyWrapper
struct AppSetting<T>: DynamicProperty {
    @StateObject private var keys = SettingKeys()
    private let key: ReferenceWritableKeyPath<SettingKeys, T>

    var wrappedValue: T {
        get {
            keys[keyPath: key]
        }

        nonmutating set {
            keys[keyPath: key] = newValue
        }
    }
    
    var projectedValue: Binding<T> {
        .init (
            get: { wrappedValue },
            set: { wrappedValue = $0 }
        )
    }

    init(_ key: ReferenceWritableKeyPath<SettingKeys, T>) {
        self.key = key
    }
}


enum AppColorScheme: String {
    
    case light
    case dark
    
    var value: ColorScheme {
        
        switch self {
        case .light:
            return .light
            
        case .dark:
            return .dark
        }
    }
}

When I try to access any settings value from outside a View (like in an extension or helper function) like this:

func someFunc() {
@AppSetting(\.colorScheme)  var colorScheme
//do something with colorScheme var
}

i get this runtime error at the getter line: keys[keyPath: key]:

Accessing StateObject's object without being installed on a View. This will create a new instance each time. I understand what does this mean, but my use-case requires to access this setting outside of a view, and at the same time I need to use this setting as a wrapper to manager reading and writing its value in a SwiftUI view, something like this: @AppSetting(\.colorScheme) private var colorScheme

if anybody comes with a best practice it would be appreciated.

  • I managed to use the values that should be saved on user defaults in my code snippet above from a call site other than a SwiftUI View by creating a static shared (singleton) instance on SettingKeys class and access the required key directly: SettingKeys.shared.colorScheme

Add a Comment