Creating a expiry date for UserDefaults values - swift

So I had this idea of creating an expiry for UserDefaults. This is the approach I was starting to take but I'm stuck.
struct TimedObject<T: Codable>: Codable {
let object: T
let expireDate: Date
}
and then:
extension UserDefaults {
func set<T: Codable>(_ value: T, forKey key: String, expireDate: Date) {
let timedObject = TimedObject<T>(object: value, expireDate: expireDate)
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(timedObject) {
UserDefaults.standard.set(encoded, forKey: key)
}
override open class func value(forKey key: String) -> Any? {
guard let value = self.value(forKey: key) else {
return nil
}
if TimedObject<???>.self == type(of: value) { // This is where I'm stuck
...
}
}
So if I would name the type and not use generics I would easily solve this. But naturally I prefer to use generics. Can this be done?

I know OP is using a struct to wrap the stored value but I would still like to offer a different protocol based solution where any type that should be stored with an expiration date needs to conform to this protocol.
Here is the protocol for I am using
protocol TimedObject: Codable {
associatedtype Value: Codable
var value: Value { get }
var expirationDate: Date { get }
}
and the functions to store and retrieve from UserDefaults
extension UserDefaults {
func set<Timed: TimedObject>(_ value: Timed, forKey key: String) {
if let encoded = try? JSONEncoder().encode(value) {
self.set(encoded, forKey: key)
}
}
func value<Timed: TimedObject>(_ type: Timed.Type, forKey key: String) -> Timed.Value? {
guard let data = self.value(forKey: key) as? Data, let object = try? JSONDecoder().decode(Timed.self, from: data) else {
return nil
}
return object.expirationDate > .now ? object.value : nil
}
}
Finally an example
struct MyStruct: Codable {
let id: Int
let name: String
}
extension MyStruct: TimedObject {
typealias Value = Self
var value: MyStruct { self }
var expirationDate: Date {
.now.addingTimeInterval(24 * 60 * 60)
}
}
let my = MyStruct(id: 12, name: "abc")
UserDefaults.standard.set(my, forKey: "my")
let my2 = UserDefaults.standard.value(MyStruct.self, forKey: "my")

Since you're returning Any?, it is best to create another struct to point to TimedObject as you don't need the object property when returning Any?:
struct Expiry: Codable {
var expireDate: Date
}
struct TimedObject<T: Codable>: Timable {
let object: T
var expireDate: Date
}
override open class func value(forKey key: String) -> Any? {
guard let value = self.value(forKey: key) as? Data else {
return nil
}
if let timed = try? JSONDecoder().decode(Expiry.self, from: value) {
//do anything with timed
}
//make sure to return value
}
And here's a method to access TimedObject:
func timedObject<T: Codable>(forKey key: String) -> TimedObject<T>? {
guard let value = self.data(forKey: key) as? Data, let timed = try? JSONDecoder().decode(TimedObject<T>.self, from: value) else {
return nil
}
return value
}

If you make the value() method generic then you can reverse the process done in the set() method: retrieve the data and decode it as a TimedObject<T>.
However, I would choose a different name to avoid possible ambiguities with the exisiting value(forKey:) method. Also I see no reason why this should be a class method.
Note also that your generic set() method should call the non-generic version on the same instance.
extension UserDefaults {
func set<T: Codable>(_ value: T, forKey key: String, expireDate: Date) {
let timedObject = TimedObject(object: value, expireDate: expireDate)
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(timedObject) {
set(encoded, forKey: key)
}
}
func expiringValue<T: Codable>(forKey key: String) -> T? {
guard let data = self.data(forKey: key) else {
return nil
}
let decoder = JSONDecoder()
guard let decoded = try? decoder.decode(TimedObject<T>.self, from: data) else {
return nil
}
// check expire date ...
return decoded.object
}
}
Example usage:
let val1 = UserDefaults.standard.expiringValue(forKey: "foo") as String?
let val2: String? = UserDefaults.standard.expiringValue(forKey: "bar")
In both cases, expiringValue(forKey:) is called with the inferred type.
Or in combination with optional binding:
if let val: String = UserDefaults.standard.expiringValue(forKey: "test") {
print(val)
}
Another option is to pass the desired type as an additional argument:
func value<T: Codable>(forKey key: String, as: T.Type) -> T? {
guard let data = self.data(forKey: key) else {
return nil
}
let decoder = JSONDecoder()
guard let decoded = try? decoder.decode(TimedObject<T>.self, from: data) else {
return nil
}
// check expire date ...
return decoded.object
}
which is then used as
let val = UserDefaults.standard.value(forKey: "foo", as: String.self)

Related

Add multiple Structs to one UserDefaults entity

I have my UserDefaults like this
fileprivate enum userdefaultKeys: String {
userSearchHistory = "userSearchHistory",
}
extension UserDefaults {
static func getUserSearchHistory() -> SearchHistory? {
let data = self.standard.data(forKey: userdefaultKeys.userSearchHistory.rawValue)
return SearchHistory.decode(json: data)
}
static func setUserSearchHistory(userSearchHistory: SearchHistory?) {
guard let json: Any = userSearchHistory?.json else { return }
self.standard.set(json, forKey: userdefaultKeys.userSearchHistory.rawValue)
}
}
And I'm saving this data to the UserDefaults
struct SearchHistory: Codable {
let type: SearchHistoryEnum
let name: String
let corpNo: String
let storeNo: String
let long: Double
let lat: Double
}
enum SearchHistoryEnum: Codable {
case storeSearch
case jsonSearch
}
let historySearch = SearchHistory(type: SearchHistoryEnum.storeSearch, name: store?.storename ?? "", corpNo: store?.corpno ?? "", storeNo: store?.storeno ?? "", long: longtitude, lat: latitude)
UserDefaults.setUserSearchHistory(userSearchHistory: historySearch)
This is okay, but it saves only one instance of SearchHistory in the time. I would like to have max 5. When 6th instance comes, I would like to delete the most old one
First of all your enum doesn't compile, you probably mean
fileprivate enum UserdefaultKeys: String {
case userSearchHistory
}
It's not necessary to specify the raw value.
Second of all I highly recommend not to fight the framework and to conform to the UserDefaults pattern to overload the getter and setter. I don't know your special methods for decoding and encoding, this is a simple implementation with standard JSONDecoder and JSONEncoder
extension UserDefaults {
func searchHistory(forKey key: String = UserdefaultKeys.userSearchHistory.rawValue) -> SearchHistory? {
guard let data = data(forKey: key) else { return nil }
return try? JSONDecoder().decode(SearchHistory.self, from: data)
}
func searchHistory(forKey key: String = UserdefaultKeys.userSearchHistory.rawValue) -> [SearchHistory]? {
guard let data = data(forKey: key) else { return nil }
return try? JSONDecoder().decode([SearchHistory].self, from: data)
}
func set(_ value: SearchHistory, forKey key: String = UserdefaultKeys.userSearchHistory.rawValue) {
guard let data = try? JSONEncoder().encode(value) else { return }
set(data, forKey: key)
}
func set(_ value: [SearchHistory], forKey key: String = UserdefaultKeys.userSearchHistory.rawValue) {
guard let data = try? JSONEncoder().encode(value) else { return }
set(data, forKey: key)
}
}
The benefit is the syntax is always the same for a single item or for an array
let historySearch = SearchHistory(type: SearchHistoryEnum.storeSearch, name: store?.storename ?? "", corpNo: store?.corpno ?? "", storeNo: store?.storeno ?? "", long: longtitude, lat: latitude)
UserDefaults.standard.set(historySearch)
or
UserDefaults.standard.set([historySearch])
The code to delete the oldest put in the method to write the data.

Skipping empty string with JSONEncoders swift

I a codable serialization extension which I use to turn my Codable struct to dictionaries, the problem I am facing is strings. I get string value from my UITextField at at times this value could be empty and as a result an empty string is decoded. How can I return nil if the value is an empty string.
extension Encodable {
var requestDictionary: [String: Any]? {
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
guard let data = try? encoder.encode(self) else { return nil }
return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any] }
}
}
if I have a Struct
let example = Exa(age: 10, name: "")
let dict = example.requestDictionary
print(dict)
I want it to just print ["age": 10] and return nil for the empty string
You can implement your own String encoding method extending KeyedEncodingContainer:
extension KeyedEncodingContainer {
mutating func encode(_ value: String, forKey key: K) throws {
guard !value.isEmpty else { return }
try encodeIfPresent(value, forKey: key)
}
}
Btw your request dictionary can be simplified as:
extension Encodable {
var dictionary: [String: Any]? {
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
return try? JSONSerialization.jsonObject(with: encoder.encode(self)) as? [String: Any]
}
}
Playground testing:
struct Exa: Encodable {
let age: Int
let name: String
}
let example = Exa(age: 10, name: "")
let dict = example.dictionary!
print(dict) // "["age": 10]\n"
I'll just another approach using a property wrapper to mark which properties could be skipped.
#propertyWrapper
struct SkipEmpty {
var wrappedValue: String
}
extension SkipEmpty: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self.wrappedValue = try container.decode(String.self)
}
func encode(to encoder: Encoder) throws {
// nothing to do here, see below
}
}
But to actually skip, you'd also need to create a overload for the KeyedEncodingContainer.encode method for the SkipEmpty type:
extension KeyedEncodingContainer {
mutating func encode(_ value: SkipEmpty, forKey key: K) throws {
if !value.wrappedValue.isEmpty {
try encode(value.wrappedValue, forKey: key) // encode the value here
}
}
}
You could possibly try to make it more generic, e.g. SkipEmpty<T: Codable> and provide another argument for the value to skip or a predicate, etc...
The usage is:
struct Exa: Encodable {
var age: Int
#SkipEmpty var name: String
}

Type 'Cache' does not conform to protocol 'Encodable'

I have been following the guide found here for Caching in Swift.
I am currently getting the error Type 'Cache' does not conform to protocol 'Encodable'
This makes no sense to me as I have followed the guide to the letter, I have found a couple of people using the same cache on GitHub and I believe my output matches theirs.
Why does 'Cache' not conform?
I have added the complete class as created in the tutorial below -
final class Cache<Key: Hashable, Value> {
private let wrapped = NSCache<WrappedKey, Entry>()
private let entryLifetime: TimeInterval
private let dateProvider: () -> Date
private let keyTracker = KeyTracker()
init(dateProvider: #escaping () -> Date = Date.init,entryLifetime: TimeInterval = 12 * 60 * 60, maximumEntryCount: Int = 50) {
self.dateProvider = dateProvider
self.entryLifetime = entryLifetime
wrapped.countLimit = maximumEntryCount
wrapped.delegate = keyTracker
}
func insert(_ value: Value, forKey key: Key) {
let date = dateProvider().addingTimeInterval(entryLifetime)
let entry = Entry(key: key, value: value, expirationDate: date)
let wrappedKey = WrappedKey(key: key)
wrapped.setObject(entry, forKey: wrappedKey)
keyTracker.keys.insert(key)
}
func value(forKey key: Key) -> Value? {
guard let entry = wrapped.object(forKey: WrappedKey(key: key)) else { return nil }
guard dateProvider() < entry.expirationDate else {
//Discard expired values
removeValue(forKey: key)
return nil
}
return entry.value
}
func removeValue(forKey key: Key) {
let key = WrappedKey(key: key)
wrapped.removeObject(forKey: key)
}
}
private extension Cache {
final class WrappedKey: NSObject {
let key: Key
init(key: Key) {
self.key = key
}
override var hash: Int { return key.hashValue }
override func isEqual(_ object: Any?) -> Bool {
guard let value = object as? WrappedKey else { return false }
return value.key == key
}
}
}
private extension Cache {
final class Entry {
let key: Key
let value: Value
let expirationDate: Date
init(key: Key, value: Value, expirationDate: Date) {
self.key = key
self.value = value
self.expirationDate = expirationDate
}
}
}
extension Cache {
subscript(key: Key) -> Value? {
get { return value(forKey: key) }
set {
guard let value = newValue else {
//If nil is assigned using subscript then remove any value for that key
removeValue(forKey: key)
return
}
insert(value, forKey: key)
}
}
}
private extension Cache {
final class KeyTracker: NSObject, NSCacheDelegate {
var keys = Set<Key>()
func cache(_ cache: NSCache<AnyObject, AnyObject>, willEvictObject object: Any) {
guard let entry = object as? Entry else { return }
keys.remove(entry.key)
}
}
}
extension Cache.Entry: Codable where Key: Codable, Value: Codable {}
private extension Cache {
func entry(forKey key: Key) -> Entry? {
guard let entry = wrapped.object(forKey: WrappedKey(key: key)) else { return nil }
guard dateProvider() < entry.expirationDate else {
removeValue(forKey: key)
return nil
}
return entry
}
func insert(_ entry: Entry) {
wrapped.setObject(entry, forKey: WrappedKey(key: entry.key))
keyTracker.keys.insert(entry.key)
}
}
extension Cache: Codable where Key: Codable, Value: Codable {
convenience init(from decoder: Decoder) throws {
self.init()
let container = try decoder.singleValueContainer()
let entries = try container.decode([Entry].self)
entries.forEach(insert)
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(keyTracker.keys.compactMap(entry))
}
}
extension Cache where Key: Codable, Value: Codable {
func saveToDisk(withName name: String, using fileManager: FileManager = .default) throws {
let folderURLs = fileManager.urls(
for: .cachesDirectory,
in: .userDomainMask
)
let fileURL = folderURLs[0].appendingPathComponent(name + ".cache")
let data = try JSONEncoder().encode(self)
try data.write(to: fileURL)
}
}

Get 'NSInvalidArgumentException' 'Attempt to insert non-property list object when saving class type array to UserDefaults [duplicate]

I have a simple object which conforms to the NSCoding protocol.
import Foundation
class JobCategory: NSObject, NSCoding {
var id: Int
var name: String
var URLString: String
init(id: Int, name: String, URLString: String) {
self.id = id
self.name = name
self.URLString = URLString
}
// MARK: - NSCoding
required init(coder aDecoder: NSCoder) {
id = aDecoder.decodeObject(forKey: "id") as? Int ?? aDecoder.decodeInteger(forKey: "id")
name = aDecoder.decodeObject(forKey: "name") as! String
URLString = aDecoder.decodeObject(forKey: "URLString") as! String
}
func encode(with aCoder: NSCoder) {
aCoder.encode(id, forKey: "id")
aCoder.encode(name, forKey: "name")
aCoder.encode(URLString, forKey: "URLString")
}
}
I'm trying to save an instance of it in UserDefaults but it keeps failing with the following error.
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to insert non-property list object for key jobCategory'
This is the code where I'm saving in UserDefaults.
enum UserDefaultsKeys: String {
case jobCategory
}
class ViewController: UIViewController {
#IBAction func didTapSaveButton(_ sender: UIButton) {
let category = JobCategory(id: 1, name: "Test Category", URLString: "http://www.example-job.com")
let userDefaults = UserDefaults.standard
userDefaults.set(category, forKey: UserDefaultsKeys.jobCategory.rawValue)
userDefaults.synchronize()
}
}
I replaced the enum value to key with a normal string but the same error still occurs. Any idea what's causing this?
You need to create Data instance from your JobCategory model using JSONEncoder and store that Data instance in UserDefaults and later decode using JSONDecoder.
struct JobCategory: Codable {
let id: Int
let name: String
}
// To store in UserDefaults
if let encoded = try? JSONEncoder().encode(category) {
UserDefaults.standard.set(encoded, forKey: UserDefaultsKeys.jobCategory.rawValue)
}
// Retrieve from UserDefaults
if let data = UserDefaults.standard.object(forKey: UserDefaultsKeys.jobCategory.rawValue) as? Data,
let category = try? JSONDecoder().decode(JobCategory.self, from: data) {
print(category.name)
}
Old Answer
You need to create Data instance from your JobCategory instance using archivedData(withRootObject:) and store that Data instance in UserDefaults and later unarchive using unarchiveTopLevelObjectWithData(_:), So try like this.
For Storing data in UserDefaults
let category = JobCategory(id: 1, name: "Test Category", URLString: "http://www.example-job.com")
let encodedData = NSKeyedArchiver.archivedData(withRootObject: category, requiringSecureCoding: false)
let userDefaults = UserDefaults.standard
userDefaults.set(encodedData, forKey: UserDefaultsKeys.jobCategory.rawValue)
For retrieving data from UserDefaults
let decoded = UserDefaults.standard.object(forKey: UserDefaultsKeys.jobCategory.rawValue) as! Data
let decodedTeams = NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(decoded) as! JobCategory
print(decodedTeams.name)
Update Swift 4, Xcode 10
I have written a struct around it for easy access.
//set, get & remove User own profile in cache
struct UserProfileCache {
static let key = "userProfileCache"
static func save(_ value: Profile!) {
UserDefaults.standard.set(try? PropertyListEncoder().encode(value), forKey: key)
}
static func get() -> Profile! {
var userData: Profile!
if let data = UserDefaults.standard.value(forKey: key) as? Data {
userData = try? PropertyListDecoder().decode(Profile.self, from: data)
return userData!
} else {
return userData
}
}
static func remove() {
UserDefaults.standard.removeObject(forKey: key)
}
}
Profile is a Json encoded object.
struct Profile: Codable {
let id: Int!
let firstName: String
let dob: String!
}
Usage:
//save details in user defaults...
UserProfileCache.save(profileDetails)
Hope that helps!!!
Thanks
Swift save Codable object to UserDefault with #propertyWrapper
#propertyWrapper
struct UserDefault<T: Codable> {
let key: String
let defaultValue: T
init(_ key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
if let data = UserDefaults.standard.object(forKey: key) as? Data,
let user = try? JSONDecoder().decode(T.self, from: data) {
return user
}
return defaultValue
}
set {
if let encoded = try? JSONEncoder().encode(newValue) {
UserDefaults.standard.set(encoded, forKey: key)
}
}
}
}
enum GlobalSettings {
#UserDefault("user", defaultValue: User(name:"",pass:"")) static var user: User
}
Example User model confirm Codable
struct User:Codable {
let name:String
let pass:String
}
How to use it
//Set value
GlobalSettings.user = User(name: "Ahmed", pass: "Ahmed")
//GetValue
print(GlobalSettings.user)
Save dictionary Into userdefault
let data = NSKeyedArchiver.archivedData(withRootObject: DictionaryData)
UserDefaults.standard.set(data, forKey: kUserData)
Retrieving the dictionary
let outData = UserDefaults.standard.data(forKey: kUserData)
let dict = NSKeyedUnarchiver.unarchiveObject(with: outData!) as! NSDictionary
Based on Harjot Singh answer. I've used like this:
struct AppData {
static var myObject: MyObject? {
get {
if UserDefaults.standard.object(forKey: "UserLocationKey") != nil {
if let data = UserDefaults.standard.value(forKey: "UserLocationKey") as? Data {
let myObject = try? PropertyListDecoder().decode(MyObject.self, from: data)
return myObject!
}
}
return nil
}
set {
UserDefaults.standard.set(try? PropertyListEncoder().encode(newValue), forKey: "UserLocationKey")
}
}
}
Here's a UserDefaults extension to set and get a Codable object, and keep it human-readable in the plist (User Defaults) if you open it as a plain text file:
extension Encodable {
var asDictionary: [String: Any]? {
guard let data = try? JSONEncoder().encode(self) else { return nil }
return try? JSONSerialization.jsonObject(with: data) as? [String : Any]
}
}
extension Decodable {
init?(dictionary: [String: Any]) {
guard let data = try? JSONSerialization.data(withJSONObject: dictionary) else { return nil }
guard let object = try? JSONDecoder().decode(Self.self, from: data) else { return nil }
self = object
}
}
extension UserDefaults {
func setEncodableAsDictionary<T: Encodable>(_ encodable: T, for key: String) {
self.set(encodable.asDictionary, forKey: key)
}
func getDecodableFromDictionary<T: Decodable>(for key: String) -> T? {
guard let dictionary = self.dictionary(forKey: key) else {
return nil
}
return T(dictionary: dictionary)
}
}
If you want to also support array (of codables) to and from plist array, add the following to the extension:
extension UserDefaults {
func setEncodablesAsArrayOfDictionaries<T: Encodable>(_ encodables: Array<T>, for key: String) {
let arrayOfDictionaries = encodables.map({ $0.asDictionary })
self.set(arrayOfDictionaries, forKey: key)
}
func getDecodablesFromArrayOfDictionaries<T: Decodable>(for key: String) -> [T]? {
guard let arrayOfDictionaries = self.array(forKey: key) as? [[String: Any]] else {
return nil
}
return arrayOfDictionaries.compactMap({ T(dictionary: $0) })
}
}
If you don't care about plist being human-readable, it can be simply saved as Data (will look like random string if opened as plain text):
extension UserDefaults {
func setEncodable<T: Encodable>(_ encodable: T, for key: String) throws {
let data = try PropertyListEncoder().encode(encodable)
self.set(data, forKey: key)
}
func getDecodable<T: Decodable>(for key: String) -> T? {
guard
self.object(forKey: key) != nil,
let data = self.value(forKey: key) as? Data
else {
return nil
}
let obj = try? PropertyListDecoder().decode(T.self, from: data)
return obj
}
}
(With this second approach, you don't need the Encodable and Decodable extensions from the top)

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