How can I implement a generic struct that manages key-value pairs for UserDefaults in Swift? - swift

How would one implement a struct that manages UserDefaults mappings in Swift?
Right now I have some computed properties a, b, c, d of different types and corresponding keys that look like this:
enum UserDefaultsKeys {
a_key
b_key
...
}
var a: String {
get { UserDefaults.standard.string(forKey: UserDefaultsKeys.a_key.rawValue) }
set { UserDefaults.standard.set(newValue, forKey: UserDefaultsKeys.a_key.rawValue) }
}
var b: Int {
get { UserDefaults.standard.integer(forKey: UserDefaultsKeys.b_key.rawValue) }
set { UserDefaults.standard.set(newValue, forKey: UserDefaultsKeys.b_key.rawValue) }
}
...
What I would like to achieve instead is to implement a struct that has a key of type String and a generic type value.
The value get function should - depending on its type - chose wether to use UserDefaults.standard.string or UserDefaults.standard.integer so that I can just create a DefaultsVar with some key and everything else is managed automatically for me.
What I have so far is this:
struct DefaultsVar<T> {
let key: String
var value: T {
get {
switch self {
case is String: return UserDefaults.standard.string(forKey: key) as! T
case is Int: return UserDefaults.standard.integer(forKey: key) as! T
default: return UserDefaults.standard.float(forKey: key) as! T
}
}
set { UserDefaults.standard.set(newValue, forKey: key) }
}
}
I get the following error: "Cast from DefaultsVar to unrelated Type 'String' always fails.
I am completely new to swift (and relatively new to programming) and don't really understand how to implement this the right way - or if this is even an approach that would be considered a good practice. Could someone please shine some light on this?
Thank you in advance!

You can use a custom
Property wrapper:
#propertyWrapper
struct UserDefaultStorage<T: Codable> {
private let key: String
private let defaultValue: T
private let userDefaults: UserDefaults
init(key: String, default: T, store: UserDefaults = .standard) {
self.key = key
self.defaultValue = `default`
self.userDefaults = store
}
var wrappedValue: T {
get {
guard let data = userDefaults.data(forKey: key) else {
return defaultValue
}
let value = try? JSONDecoder().decode(T.self, from: data)
return value ?? defaultValue
}
set {
let data = try? JSONEncoder().encode(newValue)
userDefaults.set(data, forKey: key)
}
}
}
This wrapper can store/restore any kind of codable into/from the user defaults.
Usage
#UserDefaultStorage(key: "myCustomKey", default: 0)
var myValue: Int
iOS 14
SwiftUI has a similar wrapper (only for iOS 14) called #AppStorage and it can be used as a state. The advantage of using this is that it can be used directly as a State. But it requires SwiftUI and it only works from the iOS 14.

Related

How can I get all writable keypaths from a Swift struct programatically?

I'm trying to convert a `struct1 to Realm objects right now.
Realm object has same keypath with original struct.
so If I can get all writable keypaths from original struct, it is possible to convert with general method.
public protocol KeyPathListable {
var allKeyPaths:[WritableKeyPath<Self, Any>] { get }
}
extension KeyPathListable {
private subscript(checkedMirrorDescendant key: String) -> Any {
return Mirror(reflecting: self).descendant(key)!
}
var allKeyPaths:[WritableKeyPath<Self, Any>] {
var membersTokeyPaths = [WritableKeyPath<Self,Any>]()
let mirror = Mirror(reflecting: self)
for case (let key?, _) in mirror.children {
if let keyPath = \Self.[checkedMirrorDescendant: key] as? WritableKeyPath<Self, Any> {
membersTokeyPaths.append(keyPath)
}
}
return membersTokeyPaths
}
}
Just found the code snippet above but it returns KeyPath(not WritableKeyPath). I tried to typecast in this case, but it returns nil. Maybe mirror function has problem. Is there any solution for that?

Combine these into a single var

Apologies for the stupid question, I'm still really new to the Swift language.
Following up on this answer by #matt, I want to combine these two statements into a single var
UserDefaults.standard.set(try? PropertyListEncoder().encode(songs), forKey:"songs")
if let data = UserDefaults.standard.value(forKey:"songs") as? Data {
let songs2 = try? PropertyListDecoder().decode(Array<Song>.self, from: data)
}
I've thought maybe using a var with didSet {} like something along the lines of
var test: Array = UserDefaults.standard. { // ??
didSet {
UserDefaults.standard.set(try? PropertyListEncoder().encode(test), forKey: "songs")
}
}
But I can't think of where to go from here.
Thanks for the help in advance :))
The property should not be a stored property. It should be a computed property, with get and set accessors:
var songsFromUserDefaults: [Song]? {
get {
if let data = UserDefaults.standard.value(forKey:"songs") as? Data {
return try? PropertyListDecoder().decode(Array<Song>.self, from: data)
} else {
return nil
}
}
set {
if let val = newValue {
UserDefaults.standard.set(try? PropertyListEncoder().encode(val), forKey:"songs")
}
}
}
Notice that since the decoding can fail, the getter returns an optional. This forces the setter to accept an optional newValue, and I have decided to only update UserDefaults when the value is not nil. Another design is to use try! when decoding, or return an empty array when the decoding fails. This way the type of the property can be non-optional, and the nil-check in the setter can be removed.
While you can use computed properties like Sweeper suggested (+1), I might consider putting this logic in a property wrapper.
In SwiftUI you can use AppStorage. Or you can roll your own. Here is a simplified example:
#propertyWrapper public struct Saved<Value: Codable> {
private let key: String
public var wrappedValue: Value? {
get {
guard let data = UserDefaults.standard.data(forKey: key) else { return nil }
return (try? JSONDecoder().decode(Value.self, from: data))
}
set {
guard
let value = newValue,
let data = try? JSONEncoder().encode(value)
else {
UserDefaults.standard.removeObject(forKey: key)
return
}
UserDefaults.standard.set(data, forKey: key)
}
}
init(key: String) {
self.key = key
}
}
And then you can do things like:
#Saved(key: "username") var username: String?
Or
#Saved(key: "songs") var songs: [Song]?

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> {
...

Save state in Userdefaults

I have a class that saves the state of something, in my case some variable of the ViewController, but sometimes it loads wrong or old data, but I can't figure out why.
Maybe somebody can have a look of my code and see if it makes sense.
class TopFlopState: Codable, PersistenceState {
var group: Groups = .large {
didSet {
save()
}
}
var base: Bases = .usd {
didSet {
save()
}
}
var valueOne: StatIntervalBaseModel = StatIntervalBaseModel(stat: "ppc", interval: "24h", base: "usd") {
didSet {
save()
}
}
init(){
let savedValues = load()
if savedValues != nil {
self.group = savedValues!.group
self.base = savedValues!.base
self.valueOne = savedValues!.valueOne
}
}
}
This is the PersistenceState protocol:
/**
Saves and Loads the class, enum etc. with UserDefaults.
Has to conform to Codable.
Uses as Key, the name of the class, enum etc.
*/
protocol PersistenceState {
}
extension PersistenceState where Self: Codable {
private var keyUserDefaults: String {
return String(describing: self)
}
func save() {
saveUserDefaults(withKey: keyUserDefaults, myType: self)
}
func load() -> Self? {
return loadUserDefaults(withKey: keyUserDefaults)
}
private func saveUserDefaults<T: Codable>(withKey key: String, myType: T){
do {
let data = try PropertyListEncoder().encode(myType)
UserDefaults.standard.set(data, forKey: key)
print("Saved for Key:", key)
} catch {
print("Save Failed")
}
}
private func loadUserDefaults<T: Codable>(withKey key: String) -> T? {
guard let data = UserDefaults.standard.object(forKey: key) as? Data else { return nil }
do {
let decoded = try PropertyListDecoder().decode(T.self, from: data)
return decoded
} catch {
print("Decoding failed for key", key)
return nil
}
}
}
If a value gets set to the value it should automatically save, but like I set sometimes it saves the right values but loads the wrong ones...
In my opinion, It return the cache. Because in Apple official documentation, it state
UserDefaults caches the information to avoid having to open the user’s defaults database each time you need a default value
Maybe you can change the flow, when to save the data. In your code show that you call save() 3 times in init().

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