How can I listen to changes in a #AppStorage property when not in a view? - swift

The following is the content of a playground that illustrates the problem. Basically I have a value stored in UserDefaults and accessed by a variable wrapped in the #AppStorage property wrapper. This lets me access the updated value in a View but I'm looking for a way to listen to changes in the property in ViewModels and other non-View types.
I have it working in the follow code but I'm not sure it's the best way to do it and I'd love to avoid having to declare a PassthroughSubject for each property I want to watch.
Note: I did originally sink the ObservableObject's objectWillChange property however that will reflect any change to the object and I'd like to do something more fine grained.
So does anyone have any ideas on how to improve this technique?
import Combine
import PlaygroundSupport
import SwiftUI
class AppSettings: ObservableObject {
var myValueChanged = PassthroughSubject<Int, Never>()
#AppStorage("MyValue") var myValue = 0 {
didSet { myValueChanged.send(myValue) }
}
}
struct ContentView: View {
#ObservedObject var settings: AppSettings
#ObservedObject var viewModel: ValueViewModel
init() {
let settings = AppSettings()
self.settings = settings
viewModel = ValueViewModel(settings: settings)
}
var body: some View {
ValueView(viewModel)
.environmentObject(settings)
}
}
class ValueViewModel: ObservableObject {
#ObservedObject private var settings: AppSettings
#Published var title: String = ""
private var cancellable: AnyCancellable?
init(settings: AppSettings) {
self.settings = settings
title = "Hello \(settings.myValue)"
// Is there a nicer way to do this?????
cancellable = settings.myValueChanged.sink {
print("object changed")
self.title = "Hello \($0)"
}
}
}
struct ValueView: View {
#EnvironmentObject private var settings: AppSettings
#ObservedObject private var viewModel: ValueViewModel
init(_ viewModel: ValueViewModel) {
self.viewModel = viewModel
}
var body: some View {
Text("This is my \(viewModel.title) value: \(settings.myValue)")
.frame(width: 300.0)
Button("+1") {
settings.myValue += 1
}
}
}
PlaygroundPage.current.setLiveView(ContentView())

The accepted solution PublishingAppStorage class is not fully working when binding views and
#propertyWrapper
public struct PublishedAppStorage<Value> {
// Based on: https://github.com/OpenCombine/OpenCombine/blob/master/Sources/OpenCombine/Published.swift
#AppStorage
private var storedValue: Value
private var publisher: Publisher?
internal var objectWillChange: ObservableObjectPublisher?
/// A publisher for properties marked with the `#Published` attribute.
public struct Publisher: Combine.Publisher {
public typealias Output = Value
public typealias Failure = Never
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == Value, Downstream.Failure == Never
{
subject.subscribe(subscriber)
}
fileprivate let subject: Combine.CurrentValueSubject<Value, Never>
fileprivate init(_ output: Output) {
subject = .init(output)
}
}
public var projectedValue: Publisher {
mutating get {
if let publisher = publisher {
return publisher
}
let publisher = Publisher(storedValue)
self.publisher = publisher
return publisher
}
}
#available(*, unavailable, message: """
#Published is only available on properties of classes
""")
public var wrappedValue: Value {
get { fatalError() }
set { fatalError() }
}
public static subscript<EnclosingSelf: ObservableObject>(
_enclosingInstance object: EnclosingSelf,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, PublishedAppStorage<Value>>
) -> Value {
get {
return object[keyPath: storageKeyPath].storedValue
}
set {
// https://stackoverflow.com/a/59067605/14314783
(object.objectWillChange as? ObservableObjectPublisher)?.send()
object[keyPath: storageKeyPath].publisher?.subject.send(newValue)
object[keyPath: storageKeyPath].storedValue = newValue
}
}
// MARK: - Initializers
// RawRepresentable
init(wrappedValue: Value, _ key: String, store: UserDefaults? = nil) where Value : RawRepresentable, Value.RawValue == String {
self._storedValue = AppStorage(wrappedValue: wrappedValue, key, store: store)
}
// String
init(wrappedValue: String, _ key: String, store: UserDefaults? = nil) where Value == String {
self._storedValue = AppStorage(wrappedValue: wrappedValue, key, store: store)
}
// Data
init(wrappedValue: Data, _ key: String, store: UserDefaults? = nil) where Value == Data {
self._storedValue = AppStorage(wrappedValue: wrappedValue, key, store: store)
}
// Int
init(wrappedValue: Value, _ key: String, store: UserDefaults? = nil) where Value: RawRepresentable, Value.RawValue == Int {
self._storedValue = AppStorage(wrappedValue: wrappedValue, key, store: store)
}
// URL
init(wrappedValue: URL, _ key: String, store: UserDefaults? = nil) where Value == URL {
self._storedValue = AppStorage(wrappedValue: wrappedValue, key, store: store)
}
// Double
init(wrappedValue: Double, _ key: String, store: UserDefaults? = nil) where Value == Double {
self._storedValue = AppStorage(wrappedValue: wrappedValue, key, store: store)
}
// Bool
init(wrappedValue: Bool, _ key: String, store: UserDefaults? = nil) where Value == Bool {
self._storedValue = AppStorage(wrappedValue: wrappedValue, key, store: store)
}
}
This one works as if you were using #AppStorage
viewModel.$fade.sink { [weak self] newFadeSetting in
}
where fade is
#PublishedAppStorage("fade") var fade: ScreenFadeSettingItem = .on
And for binding you can simply use
SettingSegmentPicker<ScreenFadeSettingItem>(
titles: ScreenFadeSettingItem.allCases,
selection: $viewModel.fade
)

The AppStorage changes in ObservableObject result in firing objectWillChange, so we can use it and code becomes much simpler
class AppSettings: ObservableObject {
#AppStorage("MyValue") var myValue = 0
}
class ValueViewModel: ObservableObject {
private var settings = AppSettings()
#Published var title: String = ""
private var cancellable: AnyCancellable?
init() {
cancellable = settings.objectWillChange.sink { [weak self] _ in
guard let self = self else { return }
self.title = "Hello \(self.settings.myValue)"
}
}
}
Yes, it is not known which property did change exactly, but assigning same (not modified) will not generate following updates (eg. .onChange, etc.).
So should be considered case-by-case, but can be applicable.
BTW, #ObservedObject works only in View, so in VM is just redundant.

I wrote this property wrapper:
/// Property wrapper that acts the same as #AppStorage, but also provides a ``Publisher`` so that non-View types
/// can receive value updates.
#propertyWrapper
struct PublishingAppStorage<Value> {
var wrappedValue: Value {
get { storage.wrappedValue }
set {
storage.wrappedValue = newValue
subject.send(storage.wrappedValue)
}
}
var projectedValue: Self {
self
}
/// Provides access to ``AppStorage.projectedValue`` for binding purposes.
var binding: Binding<Value> {
storage.projectedValue
}
/// Provides a ``Publisher`` for non view code to respond to value updates.
private let subject = PassthroughSubject<Value, Never>()
var publisher: AnyPublisher<Value, Never> {
subject.eraseToAnyPublisher()
}
private var storage: AppStorage<Value>
init(wrappedValue: Value, _ key: String) where Value == String {
storage = AppStorage(wrappedValue: wrappedValue, key)
}
init(wrappedValue: Value, _ key: String) where Value: RawRepresentable, Value.RawValue == Int {
storage = AppStorage(wrappedValue: wrappedValue, key)
}
init(wrappedValue: Value, _ key: String) where Value == Data {
storage = AppStorage(wrappedValue: wrappedValue, key)
}
init(wrappedValue: Value, _ key: String) where Value == Int {
storage = AppStorage(wrappedValue: wrappedValue, key)
}
init(wrappedValue: Value, _ key: String) where Value: RawRepresentable, Value.RawValue == String {
storage = AppStorage(wrappedValue: wrappedValue, key)
}
init(wrappedValue: Value, _ key: String) where Value == URL {
storage = AppStorage(wrappedValue: wrappedValue, key)
}
init(wrappedValue: Value, _ key: String) where Value == Double {
storage = AppStorage(wrappedValue: wrappedValue, key)
}
init(wrappedValue: Value, _ key: String) where Value == Bool {
storage = AppStorage(wrappedValue: wrappedValue, key)
}
mutating func update() {
storage.update()
}
}
Basically it wraps #AppStorage and adds a Publisher. Using it is exactly the same from a declaration point of view:
#PublishedAppStorage("myValue") var myValue = 0
and accessing the value is exactly the same, however accessing the binding is slightly different as the projected value projects Self so it done through $myValue.binding instead of just $myValue.
And of course now my non-view can access a publisher like this:
cancellable = settings.$myValue.publisher.sink {
print("object changed")
self.title = "Hello \($0)"
}

Related

Make ObservableObject subscribe to custom PropertyWrapper

I have written a custom PropertyWrapper, that tries to wrap UserDefaults while also giving them the same behaviour as a #Published variable. It almost works, except that the ObservableObject does not propagate the changes without observing the UserDefaults themselves.
I cannot pass a objectWillChange ref to the #Setting init, as self is not available during Settings.init...
I wonder how #Published does that..
import Combine
import Foundation
class Settings: ObservableObject {
// Trying to avoid this:
/////////////////////////////////////////////
let objectWillChange = PassthroughSubject<Void, Never>()
private var didChangeCancellable: AnyCancellable?
private init(){
didChangeCancellable = NotificationCenter.default
.publisher(for: UserDefaults.didChangeNotification)
.map { _ in () }
.receive(on: DispatchQueue.main)
.subscribe(objectWillChange)
}
/////////////////////////////////////
static var shared = Settings()
#Setting(key: "isBla") var isBla = true
}
#propertyWrapper
public struct Setting<T> {
let key: String
let defaultValue: T
init(wrappedValue value: T, key: String) {
self.key = key
self.defaultValue = value
}
public var wrappedValue: T {
get {
let val = UserDefaults.standard.object(forKey: key) as? T
return val ?? defaultValue
}
set {
objectWillChange?.send()
publisher?.subject.value = newValue
UserDefaults.standard.set(newValue, forKey: key)
}
}
public struct Publisher: Combine.Publisher {
public typealias Output = T
public typealias Failure = Never
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == T, Downstream.Failure == Never {
subject.subscribe(subscriber)
}
fileprivate let subject: Combine.CurrentValueSubject<T, Never>
fileprivate init(_ output: Output) {
subject = .init(output)
}
}
private var publisher: Publisher?
internal var objectWillChange: ObservableObjectPublisher?
public var projectedValue: Publisher {
mutating get {
if let publisher = publisher {
return publisher
}
let publisher = Publisher(wrappedValue)
self.publisher = publisher
return publisher
}
}
}

Custom Property Wrapper that Updates View Swift

Xcode 11.3, Swift 5.1.3
I am trying currently to create a custom property wrapper that allows me to link variables to a Firebase database. When doing this, to make it update the view, I at first tried to use the #ObservedObject #Bar var foo = []. But I get an error that multiple property wrappers are not supported. Next thing I tried to do, which would honestly be ideal, was try to make my custom property wrapper update the view itself upon being changed, just like #State and #ObservedObject. This both avoids needing to go down two layers to access the underlying values and avoid the use of nesting property wrappers. To do this, I checked the SwiftUI documentation and found out that they both implement the DynamicProperty protocol. I tried to use this too but failed because I need to be able to update the view (call update()) from within my Firebase database observers, which I cannot do since .update() is mutating.
Here is my current attempt at this:
import SwiftUI
import Firebase
import CodableFirebase
import Combine
#propertyWrapper
final class DatabaseBackedArray<Element>: ObservableObject where Element: Codable & Identifiable {
typealias ObserverHandle = UInt
typealias Action = RealtimeDatabase.Action
typealias Event = RealtimeDatabase.Event
private(set) var reference: DatabaseReference
private var currentValue: [Element]
private var childAddedObserverHandle: ObserverHandle?
private var childChangedObserverHandle: ObserverHandle?
private var childRemovedObserverHandle: ObserverHandle?
private var childAddedActions: [Action<[Element]>] = []
private var childChangedActions: [Action<[Element]>] = []
private var childRemovedActions: [Action<[Element]>] = []
init(wrappedValue: [Element], _ path: KeyPath<RealtimeDatabase, RealtimeDatabase>, events: Event = .all,
actions: [Action<[Element]>] = []) {
currentValue = wrappedValue
reference = RealtimeDatabase()[keyPath: path].reference
for action in actions {
if action.event.contains(.childAdded) {
childAddedActions.append(action)
}
if action.event.contains(.childChanged) {
childChangedActions.append(action)
}
if action.event.contains(.childRemoved) {
childRemovedActions.append(action)
}
}
if events.contains(.childAdded) {
childAddedObserverHandle = reference.observe(.childAdded) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
self.objectWillChange.send()
self.currentValue.append(decodedValue)
self.childAddedActions.forEach { $0.action(&self.currentValue) }
}
}
if events.contains(.childChanged) {
childChangedObserverHandle = reference.observe(.childChanged) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
guard let changeIndex = self.currentValue.firstIndex(where: { $0.id == decodedValue.id }) else {
return
}
self.objectWillChange.send()
self.currentValue[changeIndex] = decodedValue
self.childChangedActions.forEach { $0.action(&self.currentValue) }
}
}
if events.contains(.childRemoved) {
childRemovedObserverHandle = reference.observe(.childRemoved) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
self.objectWillChange.send()
self.currentValue.removeAll { $0.id == decodedValue.id }
self.childRemovedActions.forEach { $0.action(&self.currentValue) }
}
}
}
private func setValue(to value: [Element]) {
guard let encodedValue = try? FirebaseEncoder().encode(currentValue) else {
fatalError("Could not encode value to Firebase.")
}
reference.setValue(encodedValue)
}
var wrappedValue: [Element] {
get {
return currentValue
}
set {
self.objectWillChange.send()
setValue(to: newValue)
}
}
var projectedValue: Binding<[Element]> {
return Binding(get: {
return self.wrappedValue
}) { newValue in
self.wrappedValue = newValue
}
}
var hasActiveObserver: Bool {
return childAddedObserverHandle != nil || childChangedObserverHandle != nil || childRemovedObserverHandle != nil
}
var hasChildAddedObserver: Bool {
return childAddedObserverHandle != nil
}
var hasChildChangedObserver: Bool {
return childChangedObserverHandle != nil
}
var hasChildRemovedObserver: Bool {
return childRemovedObserverHandle != nil
}
func connectObservers(for event: Event) {
if event.contains(.childAdded) && childAddedObserverHandle == nil {
childAddedObserverHandle = reference.observe(.childAdded) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
self.objectWillChange.send()
self.currentValue.append(decodedValue)
self.childAddedActions.forEach { $0.action(&self.currentValue) }
}
}
if event.contains(.childChanged) && childChangedObserverHandle == nil {
childChangedObserverHandle = reference.observe(.childChanged) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
guard let changeIndex = self.currentValue.firstIndex(where: { $0.id == decodedValue.id }) else {
return
}
self.objectWillChange.send()
self.currentValue[changeIndex] = decodedValue
self.childChangedActions.forEach { $0.action(&self.currentValue) }
}
}
if event.contains(.childRemoved) && childRemovedObserverHandle == nil {
childRemovedObserverHandle = reference.observe(.childRemoved) { snapshot in
guard let value = snapshot.value, let decodedValue = try? FirebaseDecoder().decode(Element.self, from: value) else {
fatalError("Could not decode value from Firebase.")
}
self.objectWillChange.send()
self.currentValue.removeAll { $0.id == decodedValue.id }
self.childRemovedActions.forEach { $0.action(&self.currentValue) }
}
}
}
func removeObserver(for event: Event) {
if event.contains(.childAdded), let handle = childAddedObserverHandle {
reference.removeObserver(withHandle: handle)
self.childAddedObserverHandle = nil
}
if event.contains(.childChanged), let handle = childChangedObserverHandle {
reference.removeObserver(withHandle: handle)
self.childChangedObserverHandle = nil
}
if event.contains(.childRemoved), let handle = childRemovedObserverHandle {
reference.removeObserver(withHandle: handle)
self.childRemovedObserverHandle = nil
}
}
func removeAction(_ action: Action<[Element]>) {
if action.event.contains(.childAdded) {
childAddedActions.removeAll { $0.id == action.id }
}
if action.event.contains(.childChanged) {
childChangedActions.removeAll { $0.id == action.id }
}
if action.event.contains(.childRemoved) {
childRemovedActions.removeAll { $0.id == action.id }
}
}
func removeAllActions(for event: Event) {
if event.contains(.childAdded) {
childAddedActions = []
}
if event.contains(.childChanged) {
childChangedActions = []
}
if event.contains(.childRemoved) {
childRemovedActions = []
}
}
}
struct School: Codable, Identifiable {
/// The unique id of the school.
var id: String
/// The name of the school.
var name: String
/// The city of the school.
var city: String
/// The province of the school.
var province: String
/// Email domains for student emails from the school.
var domains: [String]
}
#dynamicMemberLookup
struct RealtimeDatabase {
private var path: [String]
var reference: DatabaseReference {
var ref = Database.database().reference()
for component in path {
ref = ref.child(component)
}
return ref
}
init(previous: Self? = nil, child: String? = nil) {
if let previous = previous {
path = previous.path
} else {
path = []
}
if let child = child {
path.append(child)
}
}
static subscript(dynamicMember member: String) -> Self {
return Self(child: member)
}
subscript(dynamicMember member: String) -> Self {
return Self(child: member)
}
static subscript(dynamicMember keyPath: KeyPath<Self, Self>) -> Self {
return Self()[keyPath: keyPath]
}
static let reference = Database.database().reference()
struct Event: OptionSet, Hashable {
let rawValue: UInt
static let childAdded = Event(rawValue: 1 << 0)
static let childChanged = Event(rawValue: 1 << 1)
static let childRemoved = Event(rawValue: 1 << 2)
static let all: Event = [.childAdded, .childChanged, .childRemoved]
static let constructive: Event = [.childAdded, .childChanged]
static let destructive: Event = .childRemoved
}
struct Action<Value>: Identifiable {
let id = UUID()
let event: Event
let action: (inout Value) -> Void
private init(on event: Event, perform action: #escaping (inout Value) -> Void) {
self.event = event
self.action = action
}
static func on<Value>(_ event: RealtimeDatabase.Event, perform action: #escaping (inout Value) -> Void) -> Action<Value> {
return Action<Value>(on: event, perform: action)
}
}
}
Usage example:
struct ContentView: View {
#DatabaseBackedArray(\.schools, events: .all, actions: [.on(.constructive) { $0.sort { $0.name < $1.name } }])
var schools: [School] = []
var body: some View {
Text("School: ").bold() +
Text(schools.isEmpty ? "Loading..." : schools.first!.name)
}
}
When I try to use this though, the view never updates with the value from Firebase even though I am positive that the .childAdded observer is being called.
One of my attempts at fixing this was to store all of these variables in a singleton that itself conforms to ObservableObject. This solution is also ideal as it allows the variables being observed to be shared throughout my application, preventing multiples instances of the same date and allowing for a single source of truth. Unfortunately, this too did not update the view with the fetched value of currentValue.
class Session: ObservableObject {
#DatabaseBackedArray(\.schools, events: .all, actions: [.on(.constructive) { $0.sort { $0.name < $1.name } }])
var schools: [School] = []
private init() {
//Send `objectWillChange` when `schools` property changes
_schools.objectWillChange.sink {
self.objectWillChange.send()
}
}
static let current = Session()
}
struct ContentView: View {
#ObservedObject
var session = Session.current
var body: some View {
Text("School: ").bold() +
Text(session.schools.isEmpty ? "Loading..." : session.schools.first!.name)
}
}
Is there any way to make a custom property wrapper that also updates a view in SwiftUI?
Making use of the DynamicProperty protocol we can easily trigger view updates by making use of SwiftUI's existing property wrappers. (DynamicProperty tells SwiftUI to look for these within our type)
#propertyWrapper
struct OurPropertyWrapper: DynamicProperty {
// A state object that we notify of updates
#StateObject private var updater = Updater()
var wrappedValue: T {
get {
// Your getter code here
}
nonmutating set {
// Tell SwiftUI we're going to change something
updater.notifyUpdate()
// Your setter code here
}
}
class Updater: ObservableObject {
func notifyUpdate() {
objectWillChange.send()
}
}
}
The solution to this is to make a minor tweak to the solution of the singleton. Credits to #user1046037 for pointing this out to me. The problem with the singleton fix mentioned in the original post, is that it does not retain the canceller for the sink in the initializer. Here is the correct code:
class Session: ObservableObject {
#DatabaseBackedArray(\.schools, events: .all, actions: [.on(.constructive) { $0.sort { $0.name < $1.name } }])
var schools: [School] = []
private var cancellers = [AnyCancellable]()
private init() {
_schools.objectWillChange.sink {
self.objectWillChange.send()
}.assign(to: &cancellers)
}
static let current = Session()
}

Error Using property Wrapper in class swift 5.1

I am using UserDefaults and Combine in SwiftUI.
My UserDefault.swift file:
import SwiftUI
struct UserDefault<T> {
let key: String
let defaultValue:T
var wrappedValue:T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
} set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
When using this struct in the following class as follows:
DataStore.swift file:
import SwiftUI
import Combine
final class DataStore : ObservableObject { //(1)
let didChange = PassthroughSubject<DataStore, Never>()
#UserDefault(key: "firstLaunch", defaultValue: true) //(2)
var firstLaunch:Bool{
didSet{
didChange.send(self)
}
}
}
In the above code, I am getting 2 errors:
(1):Class 'DataStore' has no initializers
(2):Generic struct 'UserDefault' cannot be used as an attribute
I think there is a change or depreciation in swift 5.1, but I am unable to find it.
Use something like this:
#propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
init(key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md
You need to add the #propertyWrapper annotation to your UserDefault struct.
#propertyWrapper
struct UserDefault<T> {
...

swift - UserDefaults wrapper

I want to make userDefaults easier to use. I write this code.
But I don't want to create a allKeys enum(cases voipToken, userId, userName, displayName,...) as the key path for my property.
If I want add a var accessToken to struct LoginInfo, I must add a case accessToken to enum AllKeys. I use CoreStore.key(.accessToken) get a keyPath for CoreStore.LoginInfo.accessToken.
import Foundation
struct CoreStore {
// static let sharedInstance = CoreStore()
// private init(){}
private static let keyPrefix = "_shared_store_"
enum allKeys: String {
case accessToken
}
static func key(_ key: allKeys) -> String {
return keyPrefix + key.rawValue
}
struct LoginInfo: CoreStoreSettable,CoreStoreGettable { }
}
extension CoreStore.LoginInfo {
static var accessToken: String? {
set {
set(value: newValue, forKey: CoreStore.key(.accessToken))
}
get {
return getString(forKey: CoreStore.key(.accessToken))
}
}
// ...
}
protocol CoreStoreSettable {}
extension CoreStoreSettable {
static func set(value: String?, forKey key: String) {
UserDefaults.standard.set(value, forKey: key)
}
}
protocol CoreStoreGettable {}
extension CoreStoreGettable {
static func getString(forKey key: String) -> String? {
return UserDefaults.standard.string(forKey: key)
}
}
Questions:
Is there a way to remove "enum allKeys"?
Should I use sharedInstance? Why?
I tried :
static var accessToken: String? {
set {
let keyPath = \CoreStore.Loginfo.accessToken
let keyPathString = keyPath.string
set(value: newValue, forKey: keyPathString)
}
get {
return getString(forKey: CoreStore.key(.accessToken))
}
}
I get error "Type of expression is ambiguous without more context"

How can I store a Swift enum value in NSUserDefaults

I have an enum like this:
enum Environment {
case Production
case Staging
case Dev
}
And I'd like to save an instance in NSUserDefaults like this:
func saveEnvironment(environment : Environment){
NSUserDefaults.standardUserDefaults().setObject(environment, forKey: kSavedEnvironmentDefaultsKey)
}
I understand that a Swift enum isn't an NSObject, and that makes it difficult to save, but I'm unsure what the best way is to convert it to something storable.
Using rawValue for the enum is one way of using types that can be stored in NSUserDefaults, define your enum to use a rawValue. Raw values can be strings, characters, or any of the integer or floating-point number types :
enum Environment: String {
case Production = "Prod"
case Staging = "Stg"
case Dev = "Dev"
}
You can also create an enum instance directly using the rawValue (which could come from NSUserDefaults) like:
let env = Environment(rawValue: "Dev")
You can extract the rawValue (String) from the enum object like this and then store it in NSUserDefaults if needed:
if let myEnv = env {
println(myEnv.rawValue)
}
func saveEnvironment(environment : Environment){
NSUserDefaults.standardUserDefaults().setObject(environment.rawValue, forKey: kSavedEnvironmentDefaultsKey)
}
If you would like to save/read data from UserDefaults and separate some logic, you can do it in following way (Swift 3):
enum Environment: String {
case Production
case Staging
case Dev
}
class UserDefaultsManager {
static let shared = UserDefaultsManager()
var environment: Environment? {
get {
guard let environment = UserDefaults.standard.value(forKey: kSavedEnvironmentDefaultsKey) as? String else {
return nil
}
return Environment(rawValue: environment)
}
set(environment) {
UserDefaults.standard.set(environment?.rawValue, forKey: kSavedEnvironmentDefaultsKey)
}
}
}
So saving data in UserDefaults will look this way:
UserDefaultsManager.shared.environment = Environment.Production
And reading data, saved in UserDefaults in this way:
if let environment = UserDefaultsManager.shared.environment {
//you can do some magic with this variable
} else {
print("environment data not saved in UserDefaults")
}
Using Codable protocol
Extent Environment enum that conforms to Codable protocol to encode and decode values as Data.
enum Environment: String, Codable {
case Production
case Staging
case Dev
}
A wrapper for UserDefaults:
struct UserDefaultsManager {
static var userDefaults: UserDefaults = .standard
static func set<T>(_ value: T, forKey: String) where T: Encodable {
if let encoded = try? JSONEncoder().encode(value) {
userDefaults.set(encoded, forKey: forKey)
}
}
static func get<T>(forKey: String) -> T? where T: Decodable {
guard let data = userDefaults.value(forKey: forKey) as? Data,
let decodedData = try? JSONDecoder().decode(T.self, from: data)
else { return nil }
return decodedData
}
}
Usage
// Set
let environment: Environment = .Production
UserDefaultsManager.set(environment, forKey: "environment")
// Get
let environment: Environment? = UserDefaultsManager.get(forKey: "environment")
Here is another alternative that can be be easily used with enums based on types (like String, Int etc) that can be stored by NSUserDefaults.
#propertyWrapper
struct StoredProperty<T: RawRepresentable> {
let key: String
let defaultValue: T
init(_ key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
guard let rawValue = UserDefaults.standard.object(forKey: key) as? T.RawValue, let value = T(rawValue: rawValue) else {
return defaultValue
}
return value
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: key)
}
}
}
Example usage:
enum Environment: String {
case Production
case Staging
case Dev
}
#StoredProperty("Environment", defaultValue: .Dev)
var storedProperty: Environment
Swift 5.1 You can create a generic property wrapper, using Codable to transform values in and out the UserDefaults
extension UserDefaults {
// let value: Value already set somewhere
// UserDefaults.standard.set(newValue, forKey: "foo")
//
func set<T>(_ value: T, forKey: String) where T: Encodable {
if let encoded = try? JSONEncoder().encode(value) {
setValue(encoded, forKey: forKey)
}
}
// let value: Value? = UserDefaults.standard.get(forKey: "foo")
//
func get<T>(forKey: String) -> T? where T: Decodable {
guard let data = value(forKey: forKey) as? Data,
let decodedData = try? JSONDecoder().decode(T.self, from: data)
else { return nil }
return decodedData
}
}
#propertyWrapper
public struct UserDefaultsBacked<Value>: Equatable where Value: Equatable, Value: Codable {
let key: String
let defaultValue: Value
var storage: UserDefaults = .standard
public init(key: String, defaultValue: Value) {
self.key = key
self.defaultValue = defaultValue
}
// if the value is nil return defaultValue
// if the value empty return defaultValue
// otherwise return the value
//
public var wrappedValue: Value {
get {
let value: Value? = storage.get(forKey: key)
if let stringValue = value as? String, stringValue.isEmpty {
// for string values we want to equate nil with empty string as well
return defaultValue
}
return value ?? defaultValue
}
set {
storage.set(newValue, forKey: key)
storage.synchronize()
}
}
}
// use it
struct AppState: Equatable {
enum TabItem: String, Codable {
case home
case book
case trips
case account
}
var isAppReady = false
#UserDefaultsBacked(key: "selectedTab", defaultValue: TabItem.home)
var selectedTab
// default value will be TabItem.home
#UserDefaultsBacked(key: "selectedIndex", defaultValue: 33)
var selectedIndex
// default value will be 33
}
I am using like this type staging. Can you please try this it will help you.
enum Environment: String {
case Production = "Production URL"
case Testing = "Testing URl"
case Development = "Development URL"
}
//your button actions
// MARK: set Development api
#IBAction func didTapDevelopmentAction(_ sender: Any) {
let env = Environment.Development.rawValue
print(env)
UserDefaults.standard.set(env, forKey:Key.UserDefaults.stagingURL)
}
// MARK: set Production api
#IBAction func didTapProductionAction(_ sender: Any) {
let env = Environment.Production.rawValue
print(env)
UserDefaults.standard.set(env, forKey:Key.UserDefaults.stagingURL)
}
// MARK: set Testing api
#IBAction func didTapTestingAction(_ sender: Any) {
let env = Environment.Testing.rawValue
print(env)
UserDefaults.standard.set(env, forKey:Key.UserDefaults.stagingURL)
}
//Based on selection act
print("\(UserDefaults.standard.object(forKey: "stagingURL") ?? "")")
Swift 5.1
You can create property wrapper for this
#propertyWrapper final class UserDefaultsLanguageValue {
var defaultValue: LanguageType
var key: UserDefaultsKey
init(key: UserDefaultsKey, defaultValue: LanguageType) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: LanguageType {
get { LanguageType(rawValue: UserDefaults.standard.object(forKey: key.rawValue) as? String ?? defaultValue.rawValue) ?? .en }
set { UserDefaults.standard.set(newValue.rawValue, forKey: key.rawValue) }
}
}
enum UserDefaultsKey: String {
case language
}
enum LanguageType: String {
case en
case ar
}
And use it just like that
#UserDefaultsLanguageValue(key: .language, defaultValue: LanguageType.en) var language