How to properly store user settings in Swift? - ios

I'm wondering what's the best way to store user settings in Swift. With user settings I mean simple (small) data, not some big files. Until now, I used a class with all properties I wanted to be saved.
All of those properties conform to Codable. However, I didn't save the whole class in UserDefaults, instead I saved each property individually. But I don't like that approach. There are several issues with it: The code gets longer because for every variable I have to write didSet{...}. For example:
var percentage: Double = UserDefaults.standard.double(forKey: "percentage") {
didSet {
UserDefaults.standard.set(percentage, forKey: "percentage")
}
}
As you can see, the variable name is written 4 times here. So there is a high chance of misspelling / copy and paste errors.
So why don't I save the whole class then? Well, I noticed that if I add a variable to the class, the decoding of the class doesn't work anymore and all data is lost even if I give the new variable a default value.
There seems to be a way to fix this: decoding manually. For example:
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(UUID.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
//etc...
}
However, decoding manually seems to require me to decode every variable separately. I don't like that as well because there is also a high chance to forget about one variable etc. (so it's the same problem as above).
What I would like to do as well is to give the user the option to export and import settings and to use iCloud for settings synchronization. For the former it would be better to store the whole Settings class (I could export and import the JSON file).
Is there a smart way to do this?
Thanks for helping me out!

You might also want some kind of class managing all of your user's stuff, something like this:
class SettingsManager {
private let defaults = UserDefaults.standard
var percentage: Double {
get { return defaults.value(forKey: "percentage") as? Double ?? 0.0 }
set { defaults.set(newValue, forKey: "percentage") }
}
}
This way you can reduce the required amount of code to this:
// Retrieve a value
let percentage = SettingsManager().percentage
// Set new value
SettingsManager().percentage = 0.55
Ideally you might use property wrappers like described here.
This eliminates the need of encoding/decoding the value until it's a custom type.

Related

Passing Dictionary to Analytics parameters: Urban Airship

I am trying to implement Urban Airship Analytics in my app. I want to track each and every event in my app, for that I have made a different class and passed tracking data as a dictionary.
Following https://docs.urbanairship.com/platform/ios/?swift#ios-screen-tracking link for the same.
I am passing parameters as:
UAirship.shared().analytics.trackScreen("MainScreen")
let event = UACustomEvent()
event.properties = createParamDictionary(paramDict1,paramDict2)
event.track()
As event properties is readonly, I can not assign/add data to it.
And the only option I can see is adding data one by one according to its defined type.
ie.
event.setStringProperty("abcd", forKey: "abcd")
event.setNumberProperty(123, forKey: "xyz")
Which is very tedious in my case.
So My questions are:
Am I doing it correctly?
If Yes, then is there any other variable or some way from which I can directly add parameters?
I also want to add User_id for tracking particular user. Any provision for this?
Thanks.
I think that you can create a custom method to UACustomEvent class which takes a dictionary and sets values using UA defined method, something like this,
extension UACustomEvent {
func setEventProperties<T: Any>(_ values: [String: T]) {
for keyValuePair in values {
if let value = keyValuePair.value as? String {
setStringProperty(value: value, forKey: keyValuePair.key)
} else if let value = keyValuePair.value as? Int {
setNumberProperty(value: value, forKey: keyValuePair.key)
}
}
}
}
That way, you dont have to use setNumberProperty or setStringProperty each time, you want to set events. You can simply do it like this,
event.setEventProperties(["abcd": "abcd", "xyz": 123])

Swift Decodable: how to transform one of values during decoding?

Be default, Decodable protocol makes translation of JSON values to object values with no change. But sometimes you need to transform value during json decoding, for example, in JSON you get {id = "id10"} but in your class instance you need to put number 10 into property id (or into even property with different name).
You can implement method init(from:) where you can do what you want with any of the values, for example:
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
latitude = try container.decode(Double.self, forKey:.latitude)
longitude = try container.decode(Double.self, forKey: .longitude)
// and for example there i can transform string "id10" to number 10
// and put it into desired field
}
Thats sounds great for me, but what if i want to change value just for one of the JSON fields and left all my other 20 fields with no change? In case of init(from:) i should manually get and put values for every of 20 fields of my class! After years of objC coding it's intuitive for me to first call super's implementation of init(from:) and then make changes just to some fields, but how i can achieve such effect with Swift and Decodable protocol?
You can use a lazy var. The downside being that you still have to provide a list of keys and you can't declare your model a constant:
struct MyModel: Decodable {
lazy var id: Int = {
return Int(_id.replacingOccurrences(of: "id", with: ""))!
}()
private var _id: String
var latitude: CGFloat
var longitude: CGFloat
enum CodingKeys: String, CodingKey {
case latitude, longitude
case _id = "id"
}
}
Example:
let json = """
{
"id": "id10",
"latitude": 1,
"longitude": 1
}
""".data(using: .utf8)!
// Can't use a `let` here
var m = try JSONDecoder().decode(MyModel.self, from: json)
print(m.id)
Currently you are forced to fully implement the encode and decode methods if you want to change the parsing of even a single property.
Some future version of Swift Codable will likely allow case-by-case handling of each property's encoding and decoding. But that Swift feature work is non-trivial and hasn't been prioritized yet:
Regardless, the goal is to likely offer a strongly-typed solution that allows you to do this on a case-by-case basis with out falling off the "cliff" into having to implement all of encode(to: and init(from: for the benefit of one property; the solution is likely nontrivial and would require a lot of API discussion to figure out how to do well, hence why we haven't been able to do this yet.
- Itai Ferber, lead developer on Swift 4 Codable
https://bugs.swift.org/browse/SR-5249?focusedCommentId=32638

Set value to model class properties swift

I know I can get the list of class properties using Mirror(reflecting:) but I can only print them. But what if I want to set properties to them and return the mirrored object.
Somwhat like this -
let mirroredObj = Mirror(reflecting: User())
for (index, property) in mirroredObj.children.enumerate() {
property.value = <SOME_VALUE>
}
return mirroredObj
Or maybe some totally different approach to do this?
You're trying to modify a class during runtime, which is impossible in Swift.
You are able to add a dictionary [String: Any] as a property though. It can be modified during runtime.

Swift: Mirror(reflecting: self) too slow?

I am trying to make a dictionary with the properties of a class of mine.
class SomeClass() {
var someString = "Hello, stackoverflow"
var someInt = 42 // The answer to life, the universe and everything
var someBool = true
func objectToDict() -> [String: String] {
var dict = [String: String]()
let reflection = Mirror(reflecting: self)
for child in reflection.children {
if let key = child.label {
dict[key] = child.value as? AnyObject
}
return dict
}
}
but objectToDict() is very slow. Is there a way to speed this up, or may be another approach to add the property values to a Dictionary?
I do not agree with most other users. Using reflection results less code, which means less time to develop, maintain, and test your product. With a well written library like EVReflection you don't need to worry about things behind the scene too much.
However, if performance is going to be a concern, do NOT use reflection based approaches at all. I'd say it's never really a problem in front-end development for me, especially in iOS, but it cannot be ignored in back-end development.
To see how slow it can be, I ran some test in Xcode. I'll write a blog about it, but generally speaking, getting Mirror is not the worst part (plus it may be possible to catch property list in memory), so replacing it with objc runtime wouldn't change the situation too much. In the other hand, setValue(_, forKey) is surprisingly slow. Considering that in real life you also need to perform tasks like checking dynamicType and so on, using the dynamic approach surely will make it 100+ times slower, which won't be acceptable for server development.
- Looping 1,000,000 times for a class with 1 `Int` property
- Getting mirror: 1.52
- Looping throw mirror and set `child.value`: 3.3
- Looping throw mirror and set `42`: 3.27
- Setting value `42`: 0.05
Again, for iOS I'll keep using it to save my time. Hopefully end customers won't care about whether it's 0.005 seconds or 0.0005 seconds.
Not only is that slow, it's also not a good idea: mirroring is for debug introspection only. You should instead construct the dictionary yourself. This ensures that you have the flexibility to store all the data in exactly the right way, and also decouples your Swift property names from the keys of the dictionary you're generating.
class SomeClass {
var someString = "Hello, stackoverflow"
var someInt = 42 // The answer to life, the universe and everything
var someBool = true
func objectToDict() -> [String: AnyObject] {
return ["someString": someString, "someInt": someInt, "someBool": someBool]
}
}

How to record date, time, and score in swift

I am creating a simple quiz app. I am planning to show some kind of "history" where the user can see the following:
Date and time of playing
Score for that particular session
How do I do that?
As of the Date and Time of playing, I saw this thread on SO: How to get the current time as datetime
However, how do I "RECORD" the date(s) and time(s) the user played the game?
Regarding the Score data, I am using:
NSUserDefaults.standardUserDefaults().setInteger(currentScore, forKey: "score")
However, I am only able to get the CURRENT SCORE. How do I record the score(s) the user got for EACH session on different date(s) and time(s)?
Please note that I have no problem in getting the user's CURRENT SCORE. I need help in storing or recording the user's score(s) in multiple sessions.
For instance, I wanted to display something like this:
Date: 2/7/16
Time: 7:00 AM
Score: 70/100
NSUserDefaults probably isn't right for what you are trying to do. I recommend using NSCoding for simple data storing. Core Data may be too complicated for something this simple. However, if you plan on saving a large data model with relationships, Core Data is the way to go.
NSCoding
NSCoding has two parts:
Encoding and decoding
Archiving and unarchiving
NSHipster explains this perfectly:
NSCoding is a simple protocol, with two methods: -initWithCoder: and encodeWithCoder:. Classes that conform to NSCoding can be serialized and deserialized into data that can be either be archived to disk or distributed across a network.
That archiving is performed by NSKeyedArchiver and NSKeyedUnarchiver.
Session
Even without NSCoding, it is suggested to represent data with objects. In this case, we can use the very creative name Session to represent a session in the history.
class Session: NSObject, NSCoding {
let date: NSDate // stores both date and time
let score: Int
init(date: NSDate, score: Int) { // initialize a NEW session
self.date = date
self.score = score
super.init()
}
required init?(coder aDecoder: NSCoder) { // decodes an EXISTING session
if let decodedDate = aDecoder.decodeObjectForKey("date") as? NSDate {
self.date = decodedDate
} else {
self.date = NSDate() // placeholder // this case shouldn't happen, but clearing compiler errors
}
self.score = aDecoder.decodeIntegerForKey("score")
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(date, forKey: "date")
aCoder.encodeInteger(score, forKey: "score")
}
}
The above code in English, in order from top to bottom:
Defining the class, conforming to NSCoding
The properties of a session: the date (+ time) and the score
The initializer for a new session - simply takes a date and score and creates an session for it
The required initializer for an existing session - decodes the date and score that is saved
decodeObjectForKey: simply does what it says (decodes an object using a key), and it returns AnyObject?
decodeIntegerForKey:, however, returns Int. If none exists on file, it returns 0, which is why it isn't optional. This is the case for most of the decoding methods except for decodeObjectForKey:
The required method for encoding an existing session - encodes the date and score
The encoding methods are just as straightforward as the decoding methods.
That takes care of the Session class, with the properties ready for NSCoding. Of course, you could always add more properties and methods.
SessionHistory
While the sessions itself are nice, an object to manage the array of sessions is needed, and it also needs to conform to NSCoding. You could also add this code to an existing class.
class SessionHistory: NSObject, NSCoding {
var sessions = [Session]()
required init?(coder aDecoder: NSCoder) {
if let decodedSessions = aDecoder.decodeObjectForKey("sessions") as? [Session] {
self.sessions = decodedSessions
} else {
self.sessions = [] // another compiler error clearer
}
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(sessions, forKey: "sessions")
}
override init() { // Used for convenience
super.init()
}
}
English translation:
Defining the manager, conforming to NSCoding
Add property for the array of sessions
Next two NSCoding methods do nearly the same thing as Session. Except this time, it is with an array.
Initializer for a new manager, which will be used below.
NSCoding looks at this manager class and sees that it needs to encode an array of sessions, so then NSCoding looks at the Session class to see what to encode for those sessions.
NSKeyedArchiver/NSKeyedUnarchiver and Singletons
While all the NSCoding is set up now, the final step is to incorporate NSKeyedArchiver and NSKeyedUnarchiver to actually save and load the data.
The two important methods are NSKeyedArchiver.archiveRootObject(_, toFile:) and NSKeyedUnarchiver.unarchiveRootObjectWithFile:
Note that both methods need a file. It automagically creates the file for you, but you need to set a location. Add this to SessionHistory:
static var dataPath: String {
let URLs = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
let URL = URLs[0]
return URL.URLByAppendingPathComponent("savehistory").path! // Put anything you want for that string
}
That simply finds a location for the file. You could, of course, find somewhere else to put the file.
With the data path ready, you can use the two methods I mentioned earlier. I like to use a modified version of a singleton for the manager class to make sure I'm using the same array of objects. In the SessionHistory class:
private static var history: SessionHistory!
static func appHistory() -> SessionHistory {
if history == nil {
if let data = NSKeyedUnarchiver.unarchiveObjectWithFile(dataPath) as? SessionHistory {
history = data
} else {
history = SessionHistory()
}
}
return history
}
This creates a private static property to store the one session history of the app. The static method checks if the session history is nil. If so, it returns the current history on file and loads the file into the history property. Otherwise, it creates a new empty session history. After that, or if the history property already stores something, it returns the history property.
Usage
All the setup for NSCoding and NSKeyedArchiver is done. But how do you use this code?
Each time you want to access the session history, call
SessionHistory.appHistory()
Wherever you want to save the session history, call
NSKeyedArchiver.archiveRootObject(SessionHistory.appHistory(), toFile: SessionHistory.dataPath)
Sample usage would work like this:
let session = Session(date: someRandomDate, score: someRandomScore)
SessionHistory.appHistory().sessions.append(session)
NSKeyedArchiver.archiveRootObject(SessionHistory.appHistory(), toFile: SessionHistory.dataPath)
The session history will automatically be loaded from the file when accessed via SessionHistory.appHistory().
You don't really need to "link" the classes per se, you just need to append the sessions to the sessions array of the session history.
Further Reading
NSHipster is a good, simple introduction to NSCoding.
Apple's NSCoding guide, although older and in Objective-C, goes deeper into NSCoding.
To store scores for each session, you'll need some sort of data structure associated with each user. You could use a dictionary of key value pairs that associates a date with a score. That way you would have a score for each date stored for the user.
You need to use a database to store such data with an undefined count of records.
Check out the Core Data Programming Guide by Apple here
You could then create an entity named history where you store records of the user's game play history, by inserting a new object into the entity every time the user finishes a game.
When you need to show the results, you'd create an NSFetchRequest over an NSManagedObjectContext to get all the results and display/filter them as you'd like.
Hope that helps!

Resources