The introduction Part. 1
In this article, we introduce Codable, a new feature introduced in Swift4.0, which serializes in-app data structures into exchangeable data and into common data formats for internal use, making it easier to convert objects and their representations to each other. Based on the iOS team’s hands-on experience in iOS, this guide will show you what problems and solutions you may encounter in Codable.
A brief introduction Part. 2
The Codable protocol was introduced in Swift4.0 with the goal of replacing the existing NSCoding protocol. It supports structures, enumerations, and classes. It can convert weakly-typed JSON data into strongly-typed data for use in code and, thanks to the compiler, help developers write less repetitive code.
Codable is a hybrid type made up of Decodable and Encodable protocols. The Decodable protocol defines an initialization function:
init(from decoder: Decoder) throws
Decodable protocol compliant types can be initialized with any Decoder object to complete a decoding process. The Encodable protocol defines a method that:
func encode(to encoder: Encoder) throws
Any Encoder object can complete the coding process by creating a representation that complies with the Encodable protocol type. The Swift standard library supports Codable protocols for types like String, Int, Double, and Data, Date, and URL by default. Let’s take common student information as an example, with id representing number, name representing name, and grade representing grade as the simplest object.
struct Student{ var id:String var name:String var grade:Int}
If you need the Student type to support Codable, just add compliance to Codable.
struct Student:Codable{ var id:String var name:String var grade:Int}
Student types now support Codable init(from:) and encode(to:) methods by default, even if they don’t declare them. Next, we’ll take the most common JSON format as an example to describe the encoding and decoding process.
All codes in this paper have been tested in Xcode10, Swift4.2 environment.
2.1 the decoding
{“id”: “127182781278”, “id”: “127182781278”, “grade”: 1}
This JSON data corresponds to the fields of the Student structure, and we now use the JSONEncoder provided by the system to decode the data
Let student = try JSONDecoder(). Decode (student. Self,from: json)print(student) “Xiaoming “, Grade: 1)
The whole process is simple: create a decoder, and the decode method of the decoder takes two parameters. The first parameter specifies the type of the data structure to be converted from JSON, which is key to converting weak types into strong ones, and the second parameter is passed in as raw data.
2.2 coding
The encoding process corresponds to the decoding process basically, and the system provides a JSONEncoder object for encoding. Create an object and pass in the same parameters:
Let student2 = Student(id: “127182781278”, grade: 1)
Create an encoder and pass in values to encode it. The encoder returns a collection of bytes as a Data instance, which we have converted to a string for display purposes.
do { let jsonData = try JSONEncoder().encode(student2) let jsonString = String(decoding: jsonData, as: UTF8. Self) print (jsonString)} catch {print (error. LocalizedDescription)} / / output: {” id “:” 127182781278 “, “name” : “xiao Ming”, “grade” : 1}
The decoding process has many more problems than the encoding process, such as missing data, type errors, single interface data format changes due to complex business scenarios, etc., so the following content will cover the decoding process in more depth.
Part.3 Application Scenarios
JSON is one of the most commonly used data transfer formats on the Web. Codable is able to convert JSON data into an application format for use in cidcidr instead of manually coding and decoding code. This is an efficient way to reduce the amount of repetitive and pointless code that needs to be written.
On the other hand, since Codable protocols are designed to replace NSCoding, NSKeyedArchiver and NSKeyedUnarchiver objects can be archived and unarchived seamlessly for Codable objects. To persist and de-persist structured data in simple ways. The decoding and persistence processes that were previously handled separately are now handled together through the new Codable protocol for greater efficiency.
Part.4 Using Skills
4.1 Nested objects, arrays, and dictionaries
Because Swift4 supports conditional consistency, every element in an array is Codable, and dictionary keys and values are Codable, and entire objects are Codable.
//swift/stdlib/public/core/Codable.swift.gybextension Array : Decodable where Element : Decodable { // … }//swift/stdlib/public/core/Codable.swift.gybextension Dictionary : Decodable where Key : Decodable, Value : Decodable { // … }
Below is the data for a class object, consisting of the class number and student membership.
{” classNumber “:” 101 “, “students” : [{” id “:” 127182781278 “, “name” : “xiao Ming”, “grade” : 1}, {” id “:” 216776999999 “, “name” : “, “grade”: 1}]}
So we can define an object such as Class, classNumber represents the Class number, students represents the student array, according to the consistency rules mentioned above, can complete decoding.
struct Class:Codable{ var classNumber:String var students:[Student]}
4.2 Empty objects or values
-
An empty object
In complex business scenarios, it is likely that we need to deal with a complex data structure. Different field keys correspond to the same data structure, but may have a value, or may just return a null value. For example, two fields firstFeed and sourceFeed have the same JSON structure, but due to business needs, What happens when firstFeed either has a value (structurally identical to sourceFeed) or has no value and returns an empty object {} in different scenarios?
{ “firstFeed”: {}, “sourceFeed”: { “feedId”: “408255848708594304”, “title”: “The stars of the whole universe are bending over you” 🌟🌟🌟\nphoto by Overwater “}}
Based on past experience, we try to use the following way to resolve:
class SourceFeed: Codable{ public var feedId: String public var title: String} class Feed: Codable { public var firstFeed: SourceFeed? public var sourceFeed: SourceFeed}var decoder = JSONDecoder()let feed = try decoder.decode(Feed.self, from: json)print(feed.firstFeed)print(feed.sourceFeed.feedId)
If you run firstFeed, you will find an error telling firstFeed that the specified key does not exist.
▿ keyNotFound: 2 elements -.0: CodingKeys(stringValue: “feedId”, intValue: nil) ▿.1: Context ▿ codingPath: 1 element – 0 : CodingKeys(stringValue: “firstFeed”, intValue: nil) – debugDescription : “No value associated with key CodingKeys(stringValue: \”feedId\”, intValue: nil) (\”feedId\”).” – underlyingError : nil
We need to adjust the SourceFeed notation:
class SourceFeed: Codable{ public var feedId: String? public var title: String? }
Set each attribute of the SourceFeed to an optional value, so that since firstFeed is also an optional value in the Feed object, it is automatically resolved to nil when firstFeed returns an empty object {}.
-
Null null
Null values are often returned by servers. Null values are converted to NSNull by default in Objective-C, and may Crash in cidcidr without checking for type. Swift is ready to convert possible null values into optional types for Codable. This makes it easy for Codable to automatically map null to nil.
Take SourceFeed as an example:
{ “firstFeed”: null, “sourceFeed”: { “feedId”: “408255848708594304”, “title”: “The stars of the whole universe are bending over you” 🌟🌟🌟\nphoto by Overwater “}}
class SourceFeed: Codable{ public var feedId: String public var title: String} class Feed: Codable { public var firstFeed: SourceFeed? public var sourceFeed: SourceFeed}var decoder = JSONDecoder()let feed = try decoder.decode(Feed.self, from: Json) print (feed firstFeed) print (feed) sourceFeed) feedId) / / / nil / / 408255848708594304 / output results
4.3 Field Matching
-
Handle mismatch between key and attribute names
Unlike the snake_case method used for JSON in Web services, which converts names to lowercase strings and concatenates them with underscores (_) instead of Spaces, the Swift API design guide predefines conversion to type as UpperCamelCase. Everything else is defined as lowerCamelCase.
Because this requirement is so common, JSONDecoder added the keyDecodingStrategy attribute with Swift4.1 to make it easy to switch between different writing conventions.
var decoder = JSONDecoder()decoder.keyDecodingStrategy = .convertFromSnakeCase
If there is such a key chat_message, it will be converted to chatMessage.
But there are special cases where the developer of the Web service has been careless and has not followed the snake nomenclature, but has acted haphazardly, so what if we want to correct the key value?
The solution is to specify an explicit mapping using CodingKeys. Swift looks for a subtype called CodingKeys that conforms to the CodingKey protocol. This is an enumeration type marked private that specifies an explicit String raw value for keys whose names do not match.
private enum CodingKeys: String, CodingKey { case template case chatMessage = “chat_message” case chatID case groupUserName = “group_user_name” case groupId }
-
The key values on both ends do not match
Many times the key value information delivered by a Web service is the most basic information that the client needs to process and process to make it usable.
{“2018-08-23 11:11:56.659”, “repostsCount”: 0, “tag”: “205”}
The above JSON data represents the record time required to forward messages, the number of forwarded messages and the mark; The corresponding data structure is as follows:
struct Feed: Decodable { var createTime : String? var repostsCount :Int var tag: String? }
One to one correspondence between the two, meet the requirements; However, if you want to add a formatTime formatTime to display the display information, you need to add a formatTime formatTime to display the display information. But this information is not provided by the Web service, so if you add
struct Feed: Decodable { var createTime : String? var repostsCount :Int var tag: String? var formatTime:String? }
Decoder will fail in Codable, prompting keyNotFound. This is where CodingKeys comes in handy, so let’s rewrite CodingKeys
private enum CodingKeys: String, CodingKey { case createTime case repostsCount case tag }
Enumeration values only list the fields that the Web service delivers that we need to decode, ignoring the formatTime so that the decoding works.
Alternatively, the Web service delivers a large number of key-value fields, and we only need to parse some of them and not use the rest, for example:
{“2018-08-23 11:11:56.659”, “repostsCount”: 0, “tag”: “205” “id “:” 23623263636633″}
The id key is not needed, but is delivered by the Web service, which is easy to handle:
struct Feed: Decodable { var createTime : String? var repostsCount :Int var tag: String? }
Just list the key values that need to be resolved in the Feed object, and without rewriting CodingKeys, Codable decoding will automatically ignore the extra key values.
4.4 Customizing date format processing
We often need to deal with dates, the date you last posted, the date you viewed the article, the number of days counting down, and so on. These data can be presented in different forms. The most common standard is ISO8601 date (https://zh.wikipedia.org/wiki/ISO_8601) and RFC3339 (https://tools.ietf.org/html/rfc3339), for example:
1985-04-12T23:50.52Z //11996-12-19T16:39:57-08:00 //21996-12-20T00:39:57Z //3 1990-12-31T23:59:60z / / 41990-12-31 t15:59:6 0-08:00 / / 5 1937-01-01 T12:00:27. 87 + 00:20 / / 6
These are all date formats, but only the second and third examples are available for Codable in Swift. Let’s look at how to decode them first:
{ “updated”:”2018-04-20T14:15:00-0700″}
struct Feed:Codable{ var updated:Date} let decoder = JSONDecoder()decoder.dateDecodingStrategy = .iso8601let feed = try! Decoder. decode(feed. self, from: json)print(feed.updated) 2018-04-20 21:15:00 +0000
JSONDecoder provides a convenient mechanism to parse date types. If you need to set the dateDecodingStrategy property to ISO8601, you can decode the date format conforming to the standard (ISO8601DateFormatter).
Another commonly used date format is timestamp, which refers to the total number of seconds since 00 hours 00 minutes 00 seconds GMT, January 01, 1970.
{ “updated”:1540650536}
struct Feed:Codable{ var updated:Date} let decoder = JSONDecoder()decoder.dateDecodingStrategy = .secondsSince1970 / / if it is ms, use. MillisecondsSince1970let feed = a try! Decoder. decode(feed. self, from: json)print(feed.updated) 2018-10-27 14:28:56 +0000
JSONDecoder’s dateDecodingStrategy is secondsSince1970 or millisecondsSince1970.
So what if it’s not one of the decoder formats mentioned earlier that can be supported by default? JSONDecoder objects also provide customizations: Let’s take the first format mentioned earlier, 1985-04-12 T23:20:50.52z, and define a new iso8601Full by extending the DateFormatter and passing this as a parameter to the dateDecodingStrategy.
extension DateFormatter { static let iso8601Full: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = “yyyy-MM-dd’T’HH:mm:ss.SSSZZZZZ” formatter.calendar = Calendar(identifier: .iso8601) formatter.timeZone = TimeZone(secondsFromGMT: 0) formatter.locale = Locale(identifier: “en_US_POSIX”) return formatter }() }let decoder = JSONDecoder()decoder.dateDecodingStrategy = .formatted(DateFormatter.iso8601Full) let feed = try! decoder.decode(Feed.self, from: json)print(feed.updated)
Here’s what the official document says:
JSONDecoder.DateDecodingStrategy.formatted(_:)The strategy that defers formatting settings to a supplied date formatter.
Provides a custom date formatting tool, you can customize the decoding format according to your requirements.
4.5 enumeration values
In the mobile product of information flow type, there must be a template type field to distinguish the card style presented, whether it is image, video, link or advertisement, etc. When we use it in code, we generally prefer to convert the template type to enumeration type for easy use. Here are two simple examples of how to convert string or integer data to an enumerated type.
-
Parse enumeration types from strings
{ “feedId”:”100000″, “template”: “video”}
Template represents the template type, which is a string type.
struct Feed:Codable { var feedId:String var template:FeedTemplate } enum FeedTemplate:String,Codable{ case FEED_VIDEO = “video” case FEED_PIC = “pic” case FEED_LINK = “link” } let feed = try! JSONDecoder().decode(feed.self, from: json) print(feed.feedid) print(feed.template) // output //100000 //FEED_VIDEO
First create the FeedTemplate enumeration type, which is primitive String and Codable, list all possible types and corresponding String values. Then define the template field in the Feed type as FeedTemplate. You can complete the conversion from data to enumerated types.
-
Resolves enumeration types from integer types
Decoding enumerations from integer data is very similar to strings, except that the primitive type of the enumerated type is specified.
{ “feedId”:”100000″, “template”: 1}
struct Feed:Codable { var feedId:String var template:FeedTemplate } enum FeedTemplate:Int,Codable{ case FEED_VIDEO = 1 case FEED_PIC = 2 case FEED_LINK = 3 } let feed = try! JSONDecoder().decode(Feed.self, from: json) print(feed.feedId) print(feed.template)
4.6 Dynamic key-value Structure
In many cases, Web services deliver dynamically structured data due to product functionality, such as this simplified JSON structure:
{ “template”:”video”, “videoFeed”:{ “vid”:”1234″, “url”:”http://www.baidu.com”, “CoverPic” : “http://www.baidu.com/pic.png”}, “picFeed” : {” content “:” the weather is good today “, “pics” : {” width “: 100,” height “: 200}}, “LinkFeed” : {” title “:” four seasons song mu “, “url” : “http://www.google.com”}}
Where, template represents the template type, there are three possibilities: video, PIC, link; Web services deliver only one kind of data at a time.
For example, in video mode:
{ “template”:”video”, “videoFeed”:{ “vid”:”1234″, “url”:”http://www.baidu.com”, “coverPic”:”http://www.baidu.com/pic.png” }}
In graphic mode:
{” template “:” PIC “, “picFeed” : {” content “:” the weather is good today “, “pics” : {” width “: 100,” height “: 200}}}
If you want to handle dynamic data structures, you have to override the init and encode methods.
To simplify things, just implement the init method:
struct Feed:Codable { var template:FeedTemplate var videoFeed:VideoFeed? var picFeed:PicFeed? var linkFeed:LinkFeed? private enum CodingKeys:String,CodingKey{ case template case videoFeed case picFeed case linkFeed } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) template = try container.decode(FeedTemplate.self, forKey: .template) do { videoFeed = try container.decodeIfPresent(VideoFeed.self, forKey: .videoFeed) } catch { videoFeed = nil } do { picFeed = try container.decodeIfPresent(PicFeed.self, forKey: .picFeed) } catch { picFeed = nil } do { linkFeed = try container.decodeIfPresent(LinkFeed.self, forKey: .linkFeed) } catch { linkFeed = nil } }} struct VideoFeed:Codable { var vid:String var url:String var coverPic:String} struct PicFeed:Codable { var content:String var pics:PicFeedImage } struct PicFeedImage:Codable{ var width:Int var height:Int} struct LinkFeed:Codable{ var title:String var url:String } enum FeedTemplate:String,Codable{ case FEED_VIDEO = “video” case FEED_PIC = “pic” case FEED_LINK = “link”}
Among them is the decodeIfPresent method, which we didn’t mention before. When you’re not sure if the key will exist, set the property to optional, and then use decodeIfPresent to find if the key exists, decode if it does, and return nil if it doesn’t, which makes it easy to deal with problems caused by dynamic data structures.
4.7 Special Types
Many times we want a structure or object to conform to Codable, but one of the attributes does not support Codable, so we need special handling. For example, CLLocationCoordinate2D is a common structure that defines longitude and latitude, but it does not follow the Codable protocol. When we want to define a description of a destination with two parameters: destination name and latitude and longitude coordinates, you might want to make CLLocationCoordinate2D follow the Codable protocol by implementing encode and init methods, for example:
struct Destination:Codable { var location : CLLocationCoordinate2D var name : String private enum CodingKeys:String,CodingKey{ case latitude case longitude case name } public func encode(to encoder: Encoder) throws { var container = try! Encoder. Container (keyedBy: codingkeys. self) try container. Encode (name,forKey:.name) // Encode latitude try Container. Encode (location, latitude, forKey: latitude) / / coding longitude try container. The encode (location, longitude, forKey: longitude)} public init(from decoder: Decoder) throws { var latitude: CLLocationDegrees var longitude: CLLocationDegrees let container = try decoder.container(keyedBy: CodingKeys.self) latitude = try container.decode(CLLocationDegrees.self,forKey:.latitude) longitude = try container.decode(CLLocationDegrees.self,forKey:.longitude) self.location = CLLocationCoordinate2D(latitude:latitude,longitude:longitude) self.name = try container.decode(String.self,forKey:.name) }}
This may seem like a fine thing to do, but if apple updates the system in the future to make CLLocationCoordinate2D compliant to Codable, it could conflict with our implementation.
So we can do it ina different way, because the CLLocationCoordinate2D actually only has two properties, so we can customize a Coordinate object that has the same two properties. We can use this new Coordinate to replace the original CLLocationCoordinate2D, and realize the conversion between the two, which solves the problem well. When CLLocationCoordinate2D also follows Codable protocol in the future, there will be no impact.
struct Destination:Codable { var location : Coordinate var name : String}struct Coordinate: Codable { let latitude, longitude: Double}extension CLLocationCoordinate2D { init(_ coordinate: Coordinate) { self = .init(latitude: coordinate.latitude, longitude: coordinate.longitude) } }extension Coordinate { init(_ coordinate: CLLocationCoordinate2D) { self = .init(latitude: coordinate.latitude, longitude: coordinate.longitude) }}
Part five summarizes
By introducing Codable into Swift, you can bridge the gap between your application’s internal data structures and common data in a seamless way, using very little code. It can simplify your development costs by linking JSON’s loose structure to strong typing. In this article, we introduce the possible problems and corresponding solutions in various scenarios from the user’s point of view; Due to space limitations, there are many problems that cannot be analyzed in detail. We hope that people who have not used Codable can help them gain some knowledge through our sharing experience.
reference
1
https://developer.apple.com/documentation/swift/codabl
2
https://developer.apple.com/documentation/foundation/archives_and_serialization/using_json_with_custom_types
3
https://github.com/xitu/gold-miner/blob/master/TODO/ultimate-guide-to-json-parsing-with-swift-4.md
Recommended reading
⬇
Mobile terminal component architecture
Mobile terminal component architecture
An in-depth look at how Runtime works