How to Implement a Generic Struct That Manages Key-Value Pairs for Userdefaults in Swift

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

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.

Allowing User to Create a Key and Value in Swift userdefaults

Considering you have a UITextField on your UI for entering both Song Name and BMP Value, with a reference to it in your code as songTextField and bmpTextField respectively, when return Key is Tapped or whenever you want to perform the operation, you can use the below code

if let songName = songTextField.text, let bmpValue = bmpTextField.text {
UserDefaults.standard.set(bmpValue, forKey: songName)
}

In Swift, how can I implement a generic system that processes a type by registering processors for different key paths?

I've created a playground showing how this can be solved with function composition and then using what we've learned there to recreate your example.

Function composition allows you to create new functions by chaining together existing ones, as long as the types match up.

precedencegroup CompositionPrecedence {
associativity: left
}

infix operator >>>: CompositionPrecedence

func >>> <T, U, V>(lhs: @escaping (T) -> U, rhs: @escaping (U) -> V) -> (T) -> V {
return { rhs(lhs($0)) }
}

The Processor can be translated to a function that takes an object O, transforms it in some way, and returns a new object. Creating the function could be done like this:

func process<O, K>(keyPath: WritableKeyPath<O, K>, _ f: @escaping (K) -> K) -> (O) -> O {
return { object in
var writable = object
writable[keyPath: keyPath] = f(object[keyPath: keyPath])
return writable
}
}

let reverseName = process(keyPath: \Person.name, reverse)
let halfAge = process(keyPath: \Person.age, half)

Now we can compose those two functions. The resulting functions still keeps the signature `(Person) -> Person). We can compose as many functions as we like, creating a processing pipeline.

let benjaminButton = reverseName >>> halfAge
let youngBradPitt = benjaminButton(bradPitt)

Moving on to recreating your example. As this answer mentions, the type is generic over the root object. This is just like in the function composition example, and it allows us to group all the processors in an array for example.

protocol Processor {
associatedtype T
func process(object: T) -> T
}

When erasing an object, it's important to keep a reference to the original, so that we can use it to implement the required functionality. In this case, we're keeping a reference to its process(:) method.

extension Processor {
func erased()-> AnyProcessor<T> {
AnyProcessor(base: self)
}
}

struct AnyProcessor<T>: Processor {
private var _process: (T) -> T

init<Base: Processor>(base: Base) where Base.T == T {
_process = base.process
}

func process(object: T) -> T {
_process(object)
}
}

Here's two types implementing the Processor protocol. Notice the first one has two placeholder types. The second placeholder will get erased.

struct AgeMultiplier<T, K: Numeric>: Processor {
let multiplier: K
let keyPath: WritableKeyPath<T, K>

private func f(_ value: K) -> K {
value * multiplier
}

func process(object: T) -> T {
var writable = object
writable[keyPath: keyPath] = f(object[keyPath: keyPath])
return writable
}
}

struct NameUppercaser<T>: Processor {
let keyPath: WritableKeyPath<T, String>

private func f(_ value: String) -> String {
value.uppercased()
}

func process(object: T) -> T {
var writable = object
writable[keyPath: keyPath] = f(object[keyPath: keyPath])
return writable
}
}

Finally, the ObjectProcessor that uses object composition. Notice the array holds object of the same type. An instance of this struct will only be able to process Persons for example. What each child processor does is hidden away in the implementation and the fact it may run on different kinds of data does not affect the ObjectProcessor.

struct ObjectProcessor<T>: Processor {
private var processers = [AnyProcessor<T>]()

mutating func add(processor: AnyProcessor<T>) {
processers.append(processor)
}

func process(object: T) -> T {
var object = object

for processor in processers {
object = processor.process(object: object)
}

return object
}
}

And here it is in action. Notice I add two processors for the same key.

var holyGrail = ObjectProcessor<Person>()
holyGrail.add(processor: NameUppercaser(keyPath: \Person.name).erased())
holyGrail.add(processor: AgeMultiplier(multiplier: 2, keyPath: \Person.age).erased())
holyGrail.add(processor: AgeMultiplier(multiplier: 3, keyPath: \Person.age).erased())

let bradPitt = Person(name: "Brad Pitt", age: 57)
let immortalBradPitt = holyGrail.process(object: bradPitt)

How to retrieve values from settings.bundle in Objective-c/Swift?

Although you define the defaults settings, they are not really stored as a value. They are stored as default. If you try to read it, the value is null. Default setting is another property as value is. But it doesnt mean that will write the default value as a default.

What I do is, first, check if some setting,(that I'm sure that should have a value) has anything stored on it. If it doesn't have anything then I write all the defaults.

Here is an example.

on AppDelegate.m I check if email_notificaciones_preference has a value, if not, I write ALL default settings to each setting.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
NSUserDefaults * standardUserDefaults = [NSUserDefaults standardUserDefaults];
NSString * email_notificaciones_preference = [standardUserDefaults objectForKey:@"email_notificaciones_preference"];
if (!email_notificaciones_preference) {
[self registerDefaultsFromSettingsBundle];
}

}

This function is what I use to write defaults to each element.

#pragma NSUserDefaults
- (void)registerDefaultsFromSettingsBundle {
// this function writes default settings as settings
NSString *settingsBundle = [[NSBundle mainBundle] pathForResource:@"Settings" ofType:@"bundle"];
if(!settingsBundle) {
NSLog(@"Could not find Settings.bundle");
return;
}

NSDictionary *settings = [NSDictionary dictionaryWithContentsOfFile:[settingsBundle stringByAppendingPathComponent:@"Root.plist"]];
NSArray *preferences = [settings objectForKey:@"PreferenceSpecifiers"];

NSMutableDictionary *defaultsToRegister = [[NSMutableDictionary alloc] initWithCapacity:[preferences count]];
for(NSDictionary *prefSpecification in preferences) {
NSString *key = [prefSpecification objectForKey:@"Key"];
if(key) {
[defaultsToRegister setObject:[prefSpecification objectForKey:@"DefaultValue"] forKey:key];
NSLog(@"writing as default %@ to the key %@",[prefSpecification objectForKey:@"DefaultValue"],key);
}
}

[[NSUserDefaults standardUserDefaults] registerDefaults:defaultsToRegister];

}

Hope that helps.

Trying to save custom object in UserDefaults using NSKeyedArchiver

Use JSONEncoder to save data to UserDefaults. Dummy code:

class YourClassName: Codable {
//Your properties here

func saveInPreference() {
do {
let jsonEncoder = JSONEncoder()
UserDefaults.standard.set(try jsonEncoder.encode(Your object here), forKey: "Write UserDefaultKey here")
UserDefaults.standard.synchronize()
}
catch {
print(error.localizedDescription)
}
}
}

Hope that helps!

@Faraz A. Khan comment if you have any questions in the above piece of code.

How to Unit-Test with global structs?

First, LevelView looks like it has too much logic in it. The point of a view is to display model data. It's not to include business logic like GameStatus.xp > 95. That should be done elsewhere and set into the view.

Next, why is GameStatus static? This is just complicating this. Pass the GameStatus to the view when it changes. That's the job of the view controller. Views just draw stuff. If anything is really unit-testable in your view, it probably shouldn't be in a view.

Finally, the piece that you're struggling with is the user defaults. So extract that piece into a generic GameStorage.

protocol GameStorage {
var xp: Int { get set }
var level: Int { get set }
}

Now make UserDefaults a GameStorage:

extension UserDefaults: GameStorage {
var xp: Int {
get { /* Read from UserDefaults */ return ... }
set { /* Write to UserDefaults */ }
}
var level: Int {
get { /* Read from UserDefaults */ return ... }
set { /* Write to UserDefaults */ }
}
}

And for testing, create a static one:

struct StaticGameStorage: GameStorage {
var xp: Int
var level: Int
}

Now when you create a GameStatus, pass it storage. But you can give that a default value, so you don't have to pass it all the time

class GameStatus {
private var storage: GameStorage

// A default parameter means you don't have to pass it normally, but you can
init(storage: GameStorage = UserDefaults.standard) {
self.storage = storage
}

With that, xp and level can just pass through to storage. No need for a special "load the storage now" step.

private(set) var xp: Int {
get { return storage.xp }
set { storage.xp = newValue }
}
private(set) var level: Int {
get { return storage.level }
set { storage.level = newValue }
}

EDIT: I made a change here from GameStatus being a struct to a class. That's because GameStatus lacks value semantics. If there are two copies of GameStatus, and you modify one of them, the other may change, too (because they both write to UserDefaults). A struct without value semantics is dangerous.

It's possible to regain value semantics, and it's worth considering. For example, instead of passing through xp and level to the storage, you could go back to your original design that has an explicit "restore" step that loads from storage (and I assume a "save" step that writes to storage). Then GameStatus would be an appropriate struct.


I'd also extract LevelState so that you can more easily test it and it captures the business logic outside of the view.

enum LevelState {
case showStart
case showCountdown
case showFinalCountDown
init(xp: Int) {
if xp > 95 {
self = .showFinalCountDown
} else if xp > 90 {
self = .showCountdown
}
self = .showStart
}
}

If this is only ever used by this one view, it's fine to nest it. Just don't make it private. You can test LevelView.LevelState without having to do anything with LevelView itself.

And then you can update the view's GameStatus as you need to:

class LevelView: UIView {

var gameStatus: GameStatus? {
didSet {
// Refresh the view with the new status
}
}

var state: LevelState {
guard let xp = gameStatus?.xp else { return .showStart }
return LevelState(xp: xp)
}

//...configurations depending on the level
}

Now the view itself doesn't need logic testing. You might do image-based testing to make sure it draws correctly given different inputs, but that's completely end-to-end. All the logic is simple and testable. You can test GameStatus and LevelState without UIKit at all by passing a StaticGameStorage to GameStatus.

How can I use UserDefaults in Swift?

ref: NSUserdefault objectTypes

Swift 3 and above

Store

UserDefaults.standard.set(true, forKey: "Key") //Bool
UserDefaults.standard.set(1, forKey: "Key") //Integer
UserDefaults.standard.set("TEST", forKey: "Key") //setObject

Retrieve

 UserDefaults.standard.bool(forKey: "Key")
UserDefaults.standard.integer(forKey: "Key")
UserDefaults.standard.string(forKey: "Key")

Remove

 UserDefaults.standard.removeObject(forKey: "Key")

Remove all Keys

 if let appDomain = Bundle.main.bundleIdentifier {
UserDefaults.standard.removePersistentDomain(forName: appDomain)
}

Swift 2 and below

Store

NSUserDefaults.standardUserDefaults().setObject(newValue, forKey: "yourkey")
NSUserDefaults.standardUserDefaults().synchronize()

Retrieve

  var returnValue: [NSString]? = NSUserDefaults.standardUserDefaults().objectForKey("yourkey") as? [NSString]

Remove

 NSUserDefaults.standardUserDefaults().removeObjectForKey("yourkey")


Register

registerDefaults: adds the registrationDictionary to the last item in every search list. This means that after NSUserDefaults has looked for a value in every other valid location, it will look in registered defaults, making them useful as a "fallback" value. Registered defaults are never stored between runs of an application, and are visible only to the application that registers them.

Default values from Defaults Configuration Files will automatically be registered.

for example detect the app from launch , create the struct for save launch

struct DetectLaunch {
static let keyforLaunch = "validateFirstlunch"
static var isFirst: Bool {
get {
return UserDefaults.standard.bool(forKey: keyforLaunch)
}
set {
UserDefaults.standard.set(newValue, forKey: keyforLaunch)
}
}
}

Register default values on app launch:

UserDefaults.standard.register(defaults: [
DetectLaunch.isFirst: true
])

remove the value on app termination:

func applicationWillTerminate(_ application: UIApplication) {
DetectLaunch.isFirst = false

}

and check the condition as

if DetectLaunch.isFirst {
// app launched from first
}

UserDefaults suite name

another one property suite name, mostly its used for App Groups concept, the example scenario I taken from here :

The use case is that I want to separate my UserDefaults (different business logic may require Userdefaults to be grouped separately) by an identifier just like Android's SharedPreferences. For example, when a user in my app clicks on logout button, I would want to clear his account related defaults but not location of the the device.

let user = UserDefaults(suiteName:"User")

use of userDefaults synchronize, the detail info has added in the duplicate answer.

JSON encode/decode Swift CNContact object generically

No, CNContact hasn't changed according to the official documentation, so it doesn't conform to the Codable protocol, which is just a typealias for Encodeable & Decodable. You can see the list of classes currently conforming to Encodable and Decodable and see here as well that CNContact is not amongst them.

However, you can write an extension for CNContact to make it conform to above protocols.

Here is an example of how to encode a CNContact object using JSONSerialization framework in Swift3. Please note that this is just an example, so I haven't parsed all possible fields and with this implementation if a certain value doesn't exist in the CNContact object, the key doesn't exist in the JSON either. Also, the decoder function is not implemented fully, but you can easily implement it if you check how the encoder works.

The names of the JSON keys were also chosen arbitrarily along with the structure, so you can change any of those.

Below piece of code is a full, working playground file, so you can test it yourself if you want to.

import Contacts

let contact = CNMutableContact()
contact.birthday = DateComponents(calendar: Calendar.current,year: 1887, month: 1, day: 1)
contact.contactType = CNContactType.person
contact.givenName = "John"
contact.familyName = "Appleseed"

contact.imageData = Data() // The profile picture as a NSData object

let homeEmail = CNLabeledValue(label:CNLabelHome, value: NSString(string: "john@example.com"))
let workEmail = CNLabeledValue(label:CNLabelWork, value: NSString(string: "j.appleseed@icloud.com"))
contact.emailAddresses = [homeEmail, workEmail]

contact.phoneNumbers = [CNLabeledValue(label:CNLabelPhoneNumberiPhone, value:CNPhoneNumber(stringValue:"(408) 555-0126"))]

let homeAddress = CNMutablePostalAddress()
homeAddress.street = "1 Infinite Loop"
homeAddress.city = "Cupertino"
homeAddress.state = "CA"
homeAddress.postalCode = "95014"
contact.postalAddresses = [CNLabeledValue(label:CNLabelHome, value:homeAddress)]

func encodeContactToJson(contact: CNContact)->Data?{
var contactDict = [String:Any]()
if let birthday = contact.birthday?.date {
let df = DateFormatter()
df.dateFormat = "yyyy-MM-dd"
contactDict["birthday"] = df.string(from: birthday)
}
contactDict["givenName"] = contact.givenName
contactDict["familyName"] = contact.familyName
if let imageData = contact.imageData {
contactDict["image"] = imageData.base64EncodedString()
}
if contact.emailAddresses.count > 0 {
var emailAddresses = [String:String]()
for (index, emailAddress) in contact.emailAddresses.enumerated() {
emailAddresses[emailAddress.label ?? "email\(index)"] = (emailAddress.value as String)
}
contactDict["emailAddresses"] = emailAddresses
}
if contact.phoneNumbers.count > 0 {
var phoneNumbers = [String:String]()
for (index, phoneNumber) in contact.phoneNumbers.enumerated() {
phoneNumbers[phoneNumber.label ?? "phone\(index)"] = phoneNumber.value.stringValue
}
contactDict["phoneNumbers"] = phoneNumbers
}
if contact.postalAddresses.count > 0 {
var postalAddresses = [String:String]()
for (index, postalAddress) in contact.postalAddresses.enumerated() {
postalAddresses[postalAddress.label ?? "postal\(index)"] = (CNPostalAddressFormatter.string(from: postalAddress.value, style: .mailingAddress))
}
contactDict["postalAddresses"] = postalAddresses
}
return try? JSONSerialization.data(withJSONObject: contactDict)
}

func decodeContactsJson(jsonData: Data)->CNContact?{
if let jsonDict = (try? JSONSerialization.jsonObject(with: jsonData)) as? [String:Any] {
let contact = CNMutableContact()
print(jsonDict)
return contact as CNContact
} else {
return nil
}
}

if let jsonContact = encodeContactToJson(contact: contact) {
print(decodeContactsJson(jsonData: jsonContact) ?? "Decoding failed")
} else {
print("Encoding failed")
}


Related Topics



Leave a reply



Submit