From 807be8c94635b55fe254710f01309795d7590355 Mon Sep 17 00:00:00 2001 From: Valentino Urbano Date: Sat, 25 Jan 2020 19:45:03 +0100 Subject: [PATCH 01/12] codable --- _posts/2020-01-26-advanced-codable.md | 284 ++++++++++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 _posts/2020-01-26-advanced-codable.md diff --git a/_posts/2020-01-26-advanced-codable.md b/_posts/2020-01-26-advanced-codable.md new file mode 100644 index 00000000..db7e49ea --- /dev/null +++ b/_posts/2020-01-26-advanced-codable.md @@ -0,0 +1,284 @@ +--- +layout: post +title: Advanced Codable in Swift +date: 2020-01-26 16:28:05.000000000 +01:00 +type: post +published: true +status: publish +categories: [Programming] +image: +image2: +author: Valentino Urbano +--- + +Swift 4 introduced the Codable protocol a few years ago. Some apps migrated to it straight away and others stood with NSJsonSerialization and either manual parsing of the json values or by using a third party framework (like [Gloss][1]). + +If you are developing a new application it is easier to make the switch since you can decide before having written a single line or code which way you want to take. I find Codable way Swifty and easy to implement and use, but it also has its drawbacks. Mostly that you need to create dummy classes to be able to decode nested values. + +While the basics are straightforward there are a few advanced features like dynamic keys that are confusing if you're not used to them with Codable while they're usually easier with 3rd party solutions. + +## Basic Decodable + +The easiest way to implement Codable is to name and type the variables the same way as the json and implement Codable. Swift will automatically create the object from you by calling JSONEncoder().encode(data) or JSONDecoder().decode(data). + +Given the JSON: + +``` +{ + "name":"John", + "surname":"Smith" +} +``` + +Create the swift struct: + +``` +struct User { + let name: String + let surname: String +} +extension User: Codable {} +``` + +That's all you need. + +``` +//Get the 'Data' from the body of the response +let user = JSONDecoder().decode(data) //decode +``` + +If you need to create a request instead: + +``` +let user = User(name: "John", surname: "Smith") +let json = JSONEncoder().encode(user) +``` + + +## Handling Dynamic Keys + +To do that we are going to need a DynamicKey that can hold anything: + + +import Foundation + +//https://gist.github.com/samwize/a82f29a1fb34091cd61fc06934568f82 + +struct DynamicKey : CodingKey { + var stringValue: String + init?(stringValue: String) { + self.stringValue = stringValue + } + var intValue: Int? + init?(intValue: Int) { + saelf.stringValue = "\(intValue)" + self.intValue = intValue + } +} + + + +We can now implement manual decoding for our user object: + +import Foundation + +struct User { + let linkedAccounts: [UserInfo]? + let currentAccount: UserInfo +} + +extension User: Decodable { + enum CodingKeys: String, CodingKey { + case linkedAccounts + case currentAccount + } + + init(from decoder: Decoder) throws { + //user account + let currentAccount = try container.nestedContainer(keyedBy: DynamicKey.self, forKey: .currentAccount) + + var modelValue: UserInfo? + for key in currentAccount.allKeys { + let value = try currentAccount.decode(String.self, forKey: key) + modelValue = UserInfo(id: key.stringValue, name: value) + } + guard let model = modelValue else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Cannot initialize UserModel")) + } + self.currentAccount = model + + //linked accounts + if let linkedAccounts = try? container.nestedContainer(keyedBy: DynamicKey.self, forKey: .linkedAccounts) { + var accountValues: [UserInfo] = [] + for key in linkedAccounts.allKeys { + let value = try linkedAccounts.decode(String.self, forKey: key) + accountValues.append(UserInfo(id: key.stringValue, name: value)) + } + self.linkedAccounts = accountValues + } else { + self.linkedAccounts = nil//no linked account + } + } +} + + + +## Automatic Conversion + +**Keys** + +``` + let decoder: JSONDecoder = { + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + return decoder + }() + let encoder: JSONEncoder = { + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + return encoder + }() +``` + + + +Be wary that the keyEncodingStrategy will be opaque to you. All the keys you receive in init(from decoder: Decoder) throws { will be already converted this will lead to you having to define the Coding keys based on the keyDecodingStrategy applied. + +If we use the above example for which the server is using snake_case and the client is using camelCase all your CodingKeys will need to be in camelCase. + +I run through one caveat using this method. We had occurences of snake case keys that started with a number. The json was: weather_30days we called the key weather30days, but the decoding failed since the 3 would need to be uppercase, but it was a number. After some trial and error we found out that the uppercase letter is just the first letter that comes after it so weather30Days. + +**Dates** + +Using this approach it is also possible to automatically convert dates using `.dateEncodingStrategy` and `.dateDecodingStrategy`. + +``` + let decoder: JSONDecoder = { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .formatter(dateFormatter) + return decoder + }() + let encoder: JSONEncoder = { + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .formatter(dateFormatter) + return encoder + }() +``` + +The options are [listed in this doc from Apple][2], but most of the times you will be using a custom data formatter as shown. + + +## Custom Encode Using Type Erasure + +We had one case where we needed the result to be .urlInBody, but as json encoded string since we were using a legacy api that only accepted such input. There was no straight way to do it so we had to go the long way around. In the end the api was changed so we could remove this not so great code, but it shows the power of type erasure in Swift. + +extension Data { + func asDictionary() throws -> [String: Any] { + guard let dictionary = try JSONSerialization.jsonObject(with: self, options: .allowFragments) as? [String: Any] else { + throw NSError() + } + return dictionary + } + func asString() -> String? { + return String(data: self, encoding: .utf8) + } +} + + +struct StringEncoding: Encodable { + let value: AnyEncodable + let encoder: JSONEncoder + var valueString: String { + guard let jsonData = try? encoder.encode(value), + let string = jsonData.asString() else { + log.error("StringEncoding failed for \(self.value)") + assertionFailure("StringEncoding failed") + return "null"//server doesn't accept an empty value + } + return string + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self) + } +} + +extension SingleValueEncodingContainer { + mutating func encode(_ value: StringEncoding) throws { + try encode(value.valueString) + } +} + +struct AnyEncodable: Encodable { + + private let encodable: Encodable + + public init(_ encodable: Encodable) { + self.encodable = encodable + } + + func encode(to encoder: Encoder) throws { + try encodable.encode(to: encoder) + } +} + + +## Custom Single Value Decoder + +If you need to wrap a value inside a struct or an object, but the json for that property is a single value, you might have thought that you had to create one struct to decode the object closely matching the server response and another to do what you wanted. + +While this approach may have its merits, it is not needed in this case. The struct can use SingleValueDecodingContainer and SingleValueEncodingContainer to tell Swift that this struct can be decoded and encoded to a single value. + +struct DecimalString: Codable { + + let string: String + let decimal: Decimal? + var decimalValue: Decimal { + return decimal ?? 0 + } + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + self = try container.decode(DecimalString.self) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self) + } +} + +extension SingleValueDecodingContainer { + func decode(_ type: DecimalString.Type) throws -> DecimalString { + if let decimalString = try? decode(String.self) { + return DecimalString(string: decimalString) + } else if let decimalStringDouble = try? decode(Double.self) { + return DecimalString(double: decimalStringDouble) + } else { + log.error("Error decoding DecimalString, setting as 0") + return DecimalString(double: 0)//We need to have a default otherwise decoding failure would propagate + } + } +} + +extension SingleValueEncodingContainer { + mutating func encode(_ value: DecimalString) throws { + try encode(value.string) + } +} + +In decode and encode you need to specify your custom encoding and decoding fuctions telling how to turn the single value into this struct and viceversa. + +## Speed + +If you really care about speed you should stick with NSJSONSerialization in [all cases it is faster][3] (on average Codable takes 2x the time). + +But also while using Codable the differences between different approaches [is significant][4]. Implementing encode() and decode() manually instead of having them automatically generated slows the process down by another 2x. + +All of this is not really noticeable if the number of objects that you are encoding and decoding is reasonable. + + +[2]: https://developer.apple.com/documentation/foundation/jsonencoder/dateencodingstrategy +[3]: https://flight.school/articles/benchmarking-codable/ +[4]: https://medium.com/@zippicoder/performance-of-decoding-automatically-in-swift4-f089831f05a5 \ No newline at end of file From 3612bda4e5cd5ecfc7f52361ee55ebd6d0d9a40b Mon Sep 17 00:00:00 2001 From: Valentino Urbano Date: Sun, 26 Jan 2020 18:46:06 +0100 Subject: [PATCH 02/12] Update 2020-01-26-advanced-codable.md --- _posts/2020-01-26-advanced-codable.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_posts/2020-01-26-advanced-codable.md b/_posts/2020-01-26-advanced-codable.md index db7e49ea..f2a7302a 100644 --- a/_posts/2020-01-26-advanced-codable.md +++ b/_posts/2020-01-26-advanced-codable.md @@ -40,7 +40,7 @@ struct User { extension User: Codable {} ``` -That's all you need. +That's all you need. Swift will automatically encode and decode the model for you from and to a Data object. ``` //Get the 'Data' from the body of the response @@ -281,4 +281,4 @@ All of this is not really noticeable if the number of objects that you are encod [2]: https://developer.apple.com/documentation/foundation/jsonencoder/dateencodingstrategy [3]: https://flight.school/articles/benchmarking-codable/ -[4]: https://medium.com/@zippicoder/performance-of-decoding-automatically-in-swift4-f089831f05a5 \ No newline at end of file +[4]: https://medium.com/@zippicoder/performance-of-decoding-automatically-in-swift4-f089831f05a5 From f1c3f4d4bc6c111b2de819dc1b564b2938f1df32 Mon Sep 17 00:00:00 2001 From: Valentino Urbano Date: Mon, 27 Jan 2020 20:52:25 +0100 Subject: [PATCH 03/12] Update 2020-01-26-advanced-codable.md --- _posts/2020-01-26-advanced-codable.md | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/_posts/2020-01-26-advanced-codable.md b/_posts/2020-01-26-advanced-codable.md index f2a7302a..7c106659 100644 --- a/_posts/2020-01-26-advanced-codable.md +++ b/_posts/2020-01-26-advanced-codable.md @@ -42,12 +42,16 @@ extension User: Codable {} That's all you need. Swift will automatically encode and decode the model for you from and to a Data object. +The only thing you need to manually do is to instantiate the decoder or encoder and pass the object to decode/encode. + +If you need to decode an object: + ``` //Get the 'Data' from the body of the response let user = JSONDecoder().decode(data) //decode ``` -If you need to create a request instead: +If you need to encode it instead: ``` let user = User(name: "John", surname: "Smith") @@ -57,9 +61,9 @@ let json = JSONEncoder().encode(user) ## Handling Dynamic Keys -To do that we are going to need a DynamicKey that can hold anything: - +Handling dynamic keys is not supported by default using Codable. To be able to decode or encode an object that contains a key that is dynamic we need to create a custom DynamicKey object that can hold anything: +``` import Foundation //https://gist.github.com/samwize/a82f29a1fb34091cd61fc06934568f82 @@ -75,11 +79,13 @@ struct DynamicKey : CodingKey { self.intValue = intValue } } +``` +Make sure that you implement the CodingKey protocol, this way the key can be used as a key by Codable. You'd want to add initializers for any type that the key supports (in this case just Int and String). +We can now go back to the object we want to decode that implements that custom key and implement manual decoding for it. While using a custom key this way automatic decoding is not supported so you need to manually write the implementation of `init(from decoder: Decoder) throws`: -We can now implement manual decoding for our user object: - +``` import Foundation struct User { @@ -108,7 +114,7 @@ extension User: Decodable { self.currentAccount = model //linked accounts - if let linkedAccounts = try? container.nestedContainer(keyedBy: DynamicKey.self, forKey: .linkedAccounts) { + if let linkedAccounts = try? container.nestedContainer(keyedBy: DynamicKey.self, forKey: .linkedAccounts) {//1 var accountValues: [UserInfo] = [] for key in linkedAccounts.allKeys { let value = try linkedAccounts.decode(String.self, forKey: key) @@ -120,8 +126,11 @@ extension User: Decodable { } } } +``` +1. We try to get the object that has for the the custom key and loop through the keys in the dictionary to map it to our model object. +This is just an example of how you can use it, but it is very powerful. This way you do noyt necessarely need to have a model that looks very similar to the json object returned by the api with the disadvantage of having to write the decoding function manually. ## Automatic Conversion From 684f50f78bc69abf425e97967c31d070e4eb4305 Mon Sep 17 00:00:00 2001 From: Valentino Urbano Date: Wed, 29 Jan 2020 19:57:04 +0100 Subject: [PATCH 04/12] Update 2020-01-26-advanced-codable.md --- _posts/2020-01-26-advanced-codable.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/_posts/2020-01-26-advanced-codable.md b/_posts/2020-01-26-advanced-codable.md index 7c106659..43487047 100644 --- a/_posts/2020-01-26-advanced-codable.md +++ b/_posts/2020-01-26-advanced-codable.md @@ -134,6 +134,8 @@ This is just an example of how you can use it, but it is very powerful. This way ## Automatic Conversion +What follows are a few example on how to setup your Encoder and Decoder object to automatically handle certain common case conversions without having to handle them each time in the model object. This way you don't need to include custom logic for the conversion in the model, but they will live in the encoder/decoder. + **Keys** ``` From dda54ce736ad7d095c71d25dafbd72fde49abc73 Mon Sep 17 00:00:00 2001 From: Valentino Urbano Date: Thu, 30 Jan 2020 17:17:56 +0100 Subject: [PATCH 05/12] Update 2020-01-26-advanced-codable.md --- _posts/2020-01-26-advanced-codable.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/_posts/2020-01-26-advanced-codable.md b/_posts/2020-01-26-advanced-codable.md index 43487047..11cc3ac6 100644 --- a/_posts/2020-01-26-advanced-codable.md +++ b/_posts/2020-01-26-advanced-codable.md @@ -161,7 +161,7 @@ I run through one caveat using this method. We had occurences of snake case keys **Dates** -Using this approach it is also possible to automatically convert dates using `.dateEncodingStrategy` and `.dateDecodingStrategy`. +Using this approach it is also possible to automatically convert dates from and to strings automatically applying the correct formatting required by the server. You can use the `.dateEncodingStrategy` and `.dateDecodingStrategy` properties to set what kind of encoding/decoding to apply for any date. ``` let decoder: JSONDecoder = { @@ -176,8 +176,12 @@ Using this approach it is also possible to automatically convert dates using `.d }() ``` -The options are [listed in this doc from Apple][2], but most of the times you will be using a custom data formatter as shown. +The full list of options are [listed in this doc from Apple][2], but most of the times you will be using a custom data formatter as shown in the example. A few on the most common ones are: +- `.iso8601` +- `.seconds/millisecondsSince1970` - If the server returns an Int instead of a date representing the seconds/ms since epoch. + +If you want to provide your own custom implementation of the decoding/encoding process without using a date formatter object that's possible as well by using `.custom` and implementing the following callback `((Date, Encoder) throws -> Void)` or `((Date, Decoder) throws -> Void)`. ## Custom Encode Using Type Erasure From 8c6eab7c606d643c9ba9b1a7574d7dbaf192793c Mon Sep 17 00:00:00 2001 From: Valentino Urbano Date: Fri, 31 Jan 2020 21:04:09 +0100 Subject: [PATCH 06/12] Update 2020-01-26-advanced-codable.md --- _posts/2020-01-26-advanced-codable.md | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/_posts/2020-01-26-advanced-codable.md b/_posts/2020-01-26-advanced-codable.md index 11cc3ac6..3b8bf880 100644 --- a/_posts/2020-01-26-advanced-codable.md +++ b/_posts/2020-01-26-advanced-codable.md @@ -185,8 +185,15 @@ If you want to provide your own custom implementation of the decoding/encoding p ## Custom Encode Using Type Erasure -We had one case where we needed the result to be .urlInBody, but as json encoded string since we were using a legacy api that only accepted such input. There was no straight way to do it so we had to go the long way around. In the end the api was changed so we could remove this not so great code, but it shows the power of type erasure in Swift. +We had one case where we needed the result to be .urlInBody, but as json encoded string since we were using a legacy api that only accepted such input. There was no straight way to do it using the framework and networking stack we had and changing them to support such use case automatically was too much worse since we knew that this was only something needed as a workaround for a short time until we could adopt a new more straightforward api and was limited to only one endpoint. Because of all those reasons we decided to go the long way around. +After a few months the api was changed and we could remove this workaround. + +The one thing that this code does well is showing the power of type erasure in Swift. By using type erasure you can avoid the error "X cannot conform to protocol Y since only concrete types can conform to protocol", common if you use a lot of protocol oriented programming. + +First some helper methods we are going to need for this example. + +``` extension Data { func asDictionary() throws -> [String: Any] { guard let dictionary = try JSONSerialization.jsonObject(with: self, options: .allowFragments) as? [String: Any] else { @@ -198,8 +205,10 @@ extension Data { return String(data: self, encoding: .utf8) } } +``` - +This is where we generate the json string. Notice how we need a value that implements Encodable, but we cannot pass `Encodable` directly. To solve this problem we are going to use type erasure with `AnyEncodable`. +``` struct StringEncoding: Encodable { let value: AnyEncodable let encoder: JSONEncoder @@ -218,13 +227,21 @@ struct StringEncoding: Encodable { try container.encode(self) } } +``` + +Finally we implement `SingleValueEncodingContainer` to be able to encode `StringEncoding` to a single value since it's what we wanted in the first place. +``` extension SingleValueEncodingContainer { mutating func encode(_ value: StringEncoding) throws { try encode(value.valueString) } } +``` +`AnyEncodable` wraps the Encodable protocol to type erase it and be able to use it inside the `StringEncoding` struct. + +``` struct AnyEncodable: Encodable { private let encodable: Encodable @@ -237,7 +254,9 @@ struct AnyEncodable: Encodable { try encodable.encode(to: encoder) } } +``` +You can use type erasure to wrap any kind of protocol. If you want to see some examples from Apple SwiftUI uses it extensively. ## Custom Single Value Decoder From b6a2b7bd4002e9b576dded489aab9f0eb9903778 Mon Sep 17 00:00:00 2001 From: Valentino Urbano Date: Sat, 1 Feb 2020 17:47:55 +0100 Subject: [PATCH 07/12] Update 2020-01-26-advanced-codable.md --- _posts/2020-01-26-advanced-codable.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/_posts/2020-01-26-advanced-codable.md b/_posts/2020-01-26-advanced-codable.md index 3b8bf880..d4a4bcf8 100644 --- a/_posts/2020-01-26-advanced-codable.md +++ b/_posts/2020-01-26-advanced-codable.md @@ -208,6 +208,7 @@ extension Data { ``` This is where we generate the json string. Notice how we need a value that implements Encodable, but we cannot pass `Encodable` directly. To solve this problem we are going to use type erasure with `AnyEncodable`. + ``` struct StringEncoding: Encodable { let value: AnyEncodable @@ -229,7 +230,7 @@ struct StringEncoding: Encodable { } ``` -Finally we implement `SingleValueEncodingContainer` to be able to encode `StringEncoding` to a single value since it's what we wanted in the first place. +Finally we implement `SingleValueEncodingContainer` to be able to encode `StringEncoding` to a single value, since it's what we wanted in the first place. ``` extension SingleValueEncodingContainer { @@ -239,7 +240,7 @@ extension SingleValueEncodingContainer { } ``` -`AnyEncodable` wraps the Encodable protocol to type erase it and be able to use it inside the `StringEncoding` struct. +`AnyEncodable` wraps the Encodable protocol to type erase it and be able to use it inside the `StringEncoding` struct. This is not only used for encodable, but you may use this tecnique for any protocol. ``` struct AnyEncodable: Encodable { @@ -256,7 +257,7 @@ struct AnyEncodable: Encodable { } ``` -You can use type erasure to wrap any kind of protocol. If you want to see some examples from Apple SwiftUI uses it extensively. +You can use type erasure to wrap any kind of protocol. If you want to see some examples from Apple, SwiftUI uses it extensively. ## Custom Single Value Decoder From cd6e14e050bbd6d0f0b6165dfd74310a84a7dab4 Mon Sep 17 00:00:00 2001 From: Valentino Urbano Date: Sun, 2 Feb 2020 19:15:21 +0100 Subject: [PATCH 08/12] Update 2020-01-26-advanced-codable.md --- _posts/2020-01-26-advanced-codable.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/_posts/2020-01-26-advanced-codable.md b/_posts/2020-01-26-advanced-codable.md index d4a4bcf8..c6158f07 100644 --- a/_posts/2020-01-26-advanced-codable.md +++ b/_posts/2020-01-26-advanced-codable.md @@ -261,10 +261,15 @@ You can use type erasure to wrap any kind of protocol. If you want to see some e ## Custom Single Value Decoder -If you need to wrap a value inside a struct or an object, but the json for that property is a single value, you might have thought that you had to create one struct to decode the object closely matching the server response and another to do what you wanted. +If you need to wrap a value inside a struct or an object, but the json for that property is a single value, you might have thought that you had to create one additional structure to decode it. This structure closely matching the server response in able to take advantage of the Codable protocol. A different structure would be needed to later do what you wanted with the data. -While this approach may have its merits, it is not needed in this case. The struct can use SingleValueDecodingContainer and SingleValueEncodingContainer to tell Swift that this struct can be decoded and encoded to a single value. +While this approach may have its merits, and it is usually advisable (for an architecture that uses this extensively [look no further than MVVM-C][5]), it is not needed if you don't want to use it. +The solution is taking advantage of `SingleValueDecodingContainer` and `SingleValueEncodingContainer` to tell Swift that this struct can be decoded and encoded to a single value. The disadvantage of this approach is that you can't take advantage of automatic `Codable` comformance, you have to implement `init(from decoder` and `func encode(to encoder` manually. + +Here's how you would go to implement it: + +``` struct DecimalString: Codable { let string: String @@ -283,7 +288,11 @@ struct DecimalString: Codable { try container.encode(self) } } +``` +After taking care of the struct and its `Codable` conformance we now need to implement `SingleValueDecodingContainer` and `SingleValueEncodingContainer`. That's straightforard as well: + +``` extension SingleValueDecodingContainer { func decode(_ type: DecimalString.Type) throws -> DecimalString { if let decimalString = try? decode(String.self) { @@ -302,8 +311,9 @@ extension SingleValueEncodingContainer { try encode(value.string) } } +``` -In decode and encode you need to specify your custom encoding and decoding fuctions telling how to turn the single value into this struct and viceversa. +In decode and encode you need to specify your custom encoding and decoding fuctions telling how to turn the single value into this struct and viceversa, just as you do while implementing Codable. ## Speed @@ -317,3 +327,4 @@ All of this is not really noticeable if the number of objects that you are encod [2]: https://developer.apple.com/documentation/foundation/jsonencoder/dateencodingstrategy [3]: https://flight.school/articles/benchmarking-codable/ [4]: https://medium.com/@zippicoder/performance-of-decoding-automatically-in-swift4-f089831f05a5 +[5]: {% post_url 2020-01-09-ios-architectures %} From 0525af725a0f0ab1fe6ebe33db9c751fa0cc3504 Mon Sep 17 00:00:00 2001 From: Valentino Urbano Date: Mon, 3 Feb 2020 17:59:23 +0100 Subject: [PATCH 09/12] Update 2020-01-26-advanced-codable.md --- _posts/2020-01-26-advanced-codable.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_posts/2020-01-26-advanced-codable.md b/_posts/2020-01-26-advanced-codable.md index c6158f07..0754f183 100644 --- a/_posts/2020-01-26-advanced-codable.md +++ b/_posts/2020-01-26-advanced-codable.md @@ -257,11 +257,11 @@ struct AnyEncodable: Encodable { } ``` -You can use type erasure to wrap any kind of protocol. If you want to see some examples from Apple, SwiftUI uses it extensively. +You can use type erasure to wrap any kind of protocol. It is used to wrap a protocol and turn it into a structure making it more generic. If you want to see some examples from Apple, SwiftUI uses it extensively. ## Custom Single Value Decoder -If you need to wrap a value inside a struct or an object, but the json for that property is a single value, you might have thought that you had to create one additional structure to decode it. This structure closely matching the server response in able to take advantage of the Codable protocol. A different structure would be needed to later do what you wanted with the data. +While using Codable there might have been cases when you need to wrap a value inside a struct or an object, but the json for that property is a single value. You might have thought that you had to create one additional structure to decode it. This structure would closely match the server response in order to take advantage of the Codable protocol. A different structure would be needed later to turn the data into the format you need it. While this approach may have its merits, and it is usually advisable (for an architecture that uses this extensively [look no further than MVVM-C][5]), it is not needed if you don't want to use it. From 90ecbce9ce8f51fda6f50a1f301ee0067dfa436a Mon Sep 17 00:00:00 2001 From: Valentino Urbano Date: Tue, 4 Feb 2020 21:18:40 +0100 Subject: [PATCH 10/12] Update 2020-01-26-advanced-codable.md --- _posts/2020-01-26-advanced-codable.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_posts/2020-01-26-advanced-codable.md b/_posts/2020-01-26-advanced-codable.md index 0754f183..86f3c486 100644 --- a/_posts/2020-01-26-advanced-codable.md +++ b/_posts/2020-01-26-advanced-codable.md @@ -290,7 +290,7 @@ struct DecimalString: Codable { } ``` -After taking care of the struct and its `Codable` conformance we now need to implement `SingleValueDecodingContainer` and `SingleValueEncodingContainer`. That's straightforard as well: +After taking care of the struct and its `Codable` conformance we now need to implement `SingleValueDecodingContainer` and `SingleValueEncodingContainer` to be able to decode and encode down to a single value "hiding" the structure as an implementation detail. That's straightforard as well: ``` extension SingleValueDecodingContainer { From 09bb7983f638b7baad9686cc5e6e41dd3f35190f Mon Sep 17 00:00:00 2001 From: Valentino Urbano Date: Wed, 5 Feb 2020 21:01:30 +0100 Subject: [PATCH 11/12] Update 2020-01-26-advanced-codable.md --- _posts/2020-01-26-advanced-codable.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/_posts/2020-01-26-advanced-codable.md b/_posts/2020-01-26-advanced-codable.md index 86f3c486..77fd584f 100644 --- a/_posts/2020-01-26-advanced-codable.md +++ b/_posts/2020-01-26-advanced-codable.md @@ -317,14 +317,25 @@ In decode and encode you need to specify your custom encoding and decoding fucti ## Speed -If you really care about speed you should stick with NSJSONSerialization in [all cases it is faster][3] (on average Codable takes 2x the time). +Before ending this article I want to talk about speed. In the beginning I naively assumed that the speed would be comparable to using the old NSJSSONSerialization, [since it's mostly wrapping it][6], but that's not the case at all. -But also while using Codable the differences between different approaches [is significant][4]. Implementing encode() and decode() manually instead of having them automatically generated slows the process down by another 2x. +If you really care about performance you should stick with NSJSONSerialization in [all cases, as it is way faster][3]. On average Codable takes 2x the time as NSJSONSerialization. -All of this is not really noticeable if the number of objects that you are encoding and decoding is reasonable. +But also while using Codable, the differences between using one approach against the other [is significant][4]. Implementing the `encode()` and `decode()` methods manually, instead of having them automatically generated buy the compiler, slows the process down by another 2x on average. You can read the two linked article for more details and analysis. There is one caveat though, in case of very complex objects this is reversed and [the manual way is faster than the automatically generated one][4]. + +All of this is not really noticeable if the number of objects that you are encoding and decoding is reasonably limited. If you are working on a lot of objects (more than 1000) you really should start thinking about it[^1]. + + +## Epilogue + +You're now an expert at using Codable in Swift and have learned its strength and shortcomings. If you are working on an application that uses something different for serialization and looking to moving over to the Apple suggested way remember that you do not need to convert everything right away. You convert one model object every time you have time to refactor and you'll be done in no time. + + +[^1]: You should also think about why on Earth are you loading 1000 items all at once, but in some cases you need to for legacy reasons =) [2]: https://developer.apple.com/documentation/foundation/jsonencoder/dateencodingstrategy [3]: https://flight.school/articles/benchmarking-codable/ [4]: https://medium.com/@zippicoder/performance-of-decoding-automatically-in-swift4-f089831f05a5 [5]: {% post_url 2020-01-09-ios-architectures %} +[6]: https://github.com/apple/swift/blob/d93e0dfa01ddd897ba733b6a2d43b05e2f0073f9/stdlib/public/SDK/Foundation/JSONEncoder.swift#L1105 From aef2cfa35ceb52c28984e02c5f08a8d22250af60 Mon Sep 17 00:00:00 2001 From: Valentino Urbano Date: Thu, 6 Feb 2020 19:34:09 +0100 Subject: [PATCH 12/12] Update 2020-01-26-advanced-codable.md --- _posts/2020-01-26-advanced-codable.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_posts/2020-01-26-advanced-codable.md b/_posts/2020-01-26-advanced-codable.md index 77fd584f..57dc9d8d 100644 --- a/_posts/2020-01-26-advanced-codable.md +++ b/_posts/2020-01-26-advanced-codable.md @@ -326,7 +326,7 @@ But also while using Codable, the differences between using one approach against All of this is not really noticeable if the number of objects that you are encoding and decoding is reasonably limited. If you are working on a lot of objects (more than 1000) you really should start thinking about it[^1]. -## Epilogue +## Conclusion You're now an expert at using Codable in Swift and have learned its strength and shortcomings. If you are working on an application that uses something different for serialization and looking to moving over to the Apple suggested way remember that you do not need to convert everything right away. You convert one model object every time you have time to refactor and you'll be done in no time.