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

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

Related

How to find value difference of two struct instances in Swift

I have two instances from the same struct in Swift. I need to find out key-values that have the same keys but different values.
For example:
struct StructDemo {
let shopId: Int
let template: String?
}
let a = StructDemo(shopId: 3, template: "a")
let a = StructDemo(shopId: 3, template: "different a")
// My expectation is to return the change pairs
let result = [template: "different a"]
My approach is as show below but comes errors.
static func difference(left: StructDemo, right: StructDemo) -> [String: Any]{
var result:[String: Any] = [:]
for leftItem in Mirror(reflecting: left).children {
guard let key = leftItem.label else { continue }
let value = leftItem.value
if value != right[key] { // This is the problem. Errror message: Protocol 'Any' as a type cannot conform to 'RawRepresentable'
result[key] = right[key]
}
}
}
Appreciate for any suggestion and solutions.
Thank you
The problem that you are seeing is that you referred to
right[key]
but right is a StructDemo and is not subscriptable. You can't look up fields given a runtime name. Well, you can with Mirror which you correctly used for left, but you did not mirror right.
Using Mirror will lead to other issues, as you will have expressions with static type Any where Equatable will be required in order to compare values.
IMHO, your best bet is to avoid a generic, reflective approach, and just embrace static typing and write a custom difference functions that iterates all the known fields of your type. Hard coding is not so bad here, if there is only one struct type that you are trying to diff.
If you have a handful of struct types each needing diffs then that might be a different story, but I don't know a good way to get around the need for Equatable. But if you have a ton of diffable types, maybe you want dictionaries to begin with?

Saving object to be accessible anywhere

I have a condition hereby let's say a user has logged in by calling an API. And the response contain's user's details. Is there anyway we can save the details of the user as an object and be accessible globally? Below is how I used ObjectMapper to call the api
For the model class:
import ObjectMapper
class User: Mappable {
var id: Int?
var userId: Int?
var orgId: Int?
var type: Int?
var email: String?
var lang: String?
var nickname: String?
var status: Int?
var token: String?
required init(map: Map) {
mapping(map: map)
}
func mapping(map: Map) {
id <- map["id"]
userId <- map["userId"]
orgId <- map["orgId"]
type <- map["type"]
failedAttempt <- map["failedAttempt"]
email <- map["email"]
lang <- map["lang"]
nickname <- map["nickname"]
status <- map["status"]
token <- map["token"]
}
}
And from my Networking file,
func postLogin(params: [String: Any], controller: UIViewController, completion: #escaping (User) -> Void) {
alamofire("/login", method: .post, token: false, params: params, controller: controller) { result in
if let userDetails = Mapper<User>().map(JSON: result as! [String: Any]) {
DispatchQueue.main.async {
completion(userDetails)
}
}
}
}
Some solutions may be using UserDefaults but it's just not practical to be using 9 UserDefaults to save the 9 keys that we got from this response. What are the suggested ways of approach we can go about this where user logged in, we save these details as an object globally and even when after closing the app, the details are not reseted? Thank you all
I agree that saving 9 individual keys in UserDefaults is not practical. But why not encode the whole thing as JSON and save the resulting Data?
extension User: Codable { }
// Assuming user is an instance of User
guard let userJSON = try? JSONEncoder().encode(user) else {
// handle encoding failure
}
let userDefaultsKeyForUser = "com.yourdomain.snazzyapp.userInfo"
UserDefaults.standard.set(userJSON, forKey: userDefaultsKeyForUser)
I think Swift will automatically synthesize Codable conformance for your User class, but you might need to make it final. Or implement Codable explicitly.
To retrieve it
guard let userJSON = UserDefaults.standard.data(forKey: userDefaultsKeyForUser) else {
// Handle missing data (first run maybe?)
}
guard let user = try? JSONDecoder().decode(User.self, from: userJSON) else {
// Handle decoding failure
}
Alternative encoding/decoding methods
Although conforming to Codable would be the preferred way to do this, that can sometimes be a problem for classes, particularly when inheritance is involved. In particular Codable is a combination of Encodable and Decodable. Decodable is the one that is sometimes a problem for classes. The issue has to do with the required init(from decoder: Decoder) throws having to be declared directly in the class, not in an extension, and then you might have trouble with encoding the super. If your class is the base class that shouldn't be a problem. This is mainly a problem when you inherit from AppKit/UIKit classes.
If for some reason User can't conform to Codable, you can use NSCoding instead. It works a little differently.
In the worst case you could implement methods where you manually encode/decode each property explicitly to data. You could even store them as binary, but you'll probably need to do something like store byte counts for the strings so you know where each one starts and ends, and you'll need to come up with a scheme to indicate when they are nil in the encoding. It's not as flexible as JSON, which is why it's a last resort... although I will note that it is much faster. That wouldn't matter much for your User class, but I wrote a neural network library where I wanted to save the model at checkpoints during training, which meant encoding many layers of very large matrices. Millions of parameters. Reading and writing the model was about 20x faster with my own binary encoding than letting Codable handle it, even when I had Codable saving it as a binary plist.
A third option would be to re-work your User class. Either make it a struct, or use a struct internally to store its properties and use computed properties to get and set them in your class. All of your class's properties already conform to Codable, so Swift can definitely synthesize Codable conformance for a struct with those properties. If you still wrap the struct inside of a class, then you just have an initializer that takes a Data, and do the same decoding I showed above, to set the wrapped struct, and an encode method (or even a computed var) that encodes the struct as above and returns the Data.
I don't think you'll need these alternate solutions, but I mention them just in case I'm wrong, and because they're useful to know about for future situations.
You have to ask yourself how you would like to access this object with compilator autocompletion from different places in your app. Secondly, what is the life span of the object? The object should be accessible during login / logout, app session or app life time?
Let's start with the object life span.
login / logout, the object can be stored in memory
app session, the object can be stored in memory
app install / delete, the object should be stored in UserDefaults, Database or a file.
Autocompletion Driven Development aka ADD:
Deciding where to store userDetails object and how to access it
Let's say that you have a logic in a view controller which hide or unhide a view in a initial configuration defined in viewDidLoad. Write some prototype code to decide how you would like to access the 'userDetails' object from different places of the app.
func viewDidLoad() {
super.viewDidLoad()
if userDetails.status {
//userDetails is a global instance of a user object
}
if session.userDetails.status {
//session is a global instance of a Session object which has a property which stores a User instance in userDetails
}
if Session.current.userDetails.status {
// Session is a type and 'current' is a singleton of that type which has a userDetails property which stores an instance of User type
}
if User.userDetails.status {
// User is your type which can have a static property 'userDetails' which stores a User instance
}
}

How to properly store user settings in Swift?

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.

Swift 5 Default Decododable implementation with only one exception

Is there a way to keep Swift's default implementation for a Decodable class with only Decodable objects but one exception?
So for example if I have a struct/class like that:
struct MyDecodable: Decodable {
var int: Int
var string: String
var location: CLLocation
}
I would like to use default decoding for int and string but decode location myself.
So in init(from decoder:) i would like to have something like this:
required init(from decoder: Decoder) throws {
<# insert something that decodes all standard decodable properties #>
// only handle location separately
let container = try decoder.container(keyedBy: CodingKeys.self)
location = <# insert custom location decoding #>
}
Is there a way to keep Swift's default implementation for a Decodable class with only Decodable objects but one exception
Unfortunately no. To be Decodable all properties must be Decodable. And if you are going to write a custom init you must initialize (and therefore decode) all properties yourself.
Apple knows this is painful and has given some thought to the matter, but right now a custom init for a Decodable is all or nothing.
As has been suggested in a comment you might work around this by splitting your struct into two separate types. That way you could have a type with just one property, you initialize it manually, and you’re all done.

Advantages/Disadvantages of parsing data using initializer vs computed properties

Let's say I have a json data object I would like to parse into an object of my own. I've came across two ways of doing this. The first is to use an initializer like so:
class DataModelOne {
let firstProperty: String?
let secondProperty: String?
init(json: [Sting: AnyObject]) {
self.firstProperty = json["firstProperty"] as? String
self.secondProperty = json["secondProperty"] as? String
}
}
This would be called like so:
let object = DataModelOne(json: data)
where data is your JSON that you are trying to parse.
The second method that I have come across is by using computed properties:
class DataModelTwo {
let json: [String: AnyObject]
init(json: [String: AnyObject]) {
self.json = json
}
var firstProperty: String? {
return json["firstProperty"] as? String
}
var secondProperty: String? {
return json["secongProperty"] as? String
}
}
This would be initialized in the same way as above:
let object = DataModelTwo(json: data)
Aside from the fact that you couldn't set the properties once the data has been cast using DataObjectTwo, as these are computed properties and so get-only, what are the advantages/disadvantages of parsing using these two methods?
I don't see any real advantage to the computed properties. I prefer the first option, because:
This decouples your data model from the JSON. With the first option you can instantiate a DataModelOne without any JSON, and assign properties manually.
It's clearer because the parsing happens in one place.
You don't have to keep the dictionary around like you do with the computed properties. So if the dictionary contains a lot of other data that can be discarded, it can free up some memory.
The only advantage of the computed properties I can think of is that it delays accessing the Dictionary to the last moment. If you never access a property, it will never have to reach into the Dictionary in the first place. But the increase in performance will be negligible.
Lastly, I would rename the initialiser to something like init(values:). You're not initialising it with JSON, but with a plain Swift Dictionary.

Resources