Skip to main content

Swift - Codable (P1)

Codable được giới thiệu cùng với phiên bản 4.0 của Swift, đem lại sự thuận tiện cho người dùng mỗi khi cần encode/ decode giữa JSON và Swift object.

Codable là alias của 2 protocols: Decodable & Encodable
. Decodable: Chuyển data dạng string, bytes... sang instance (decoding/ deserialization)
. Encodable: Chuyển instance sang string, bytes... (encoding/ serialization)

Table of contents
. Swift Codable basic
. Swift Codable manual encode decode
. Swift Codable coding key
. Swift Codable key decoding strategy
. Swift Codable date decoding strategy
. Swift Codable nested unkeyed container

Swift Codable basic
Chúng ta sẽ đi vào ví dụ đầu tiên của Swift Codable, mục tiêu sẽ là convert đoạn JSON sau sang Swift object (struct)

{
    "name": "NhatHM",
    "age": 29,
    "page": "https://magz.techover.io/"
}

Cách làm:
Đối với JSON có dạng đơn giản thế này, công việc của chúng ta chỉ là define Swift struct to Codable protocol cho chính xác, sau đó dùng JSONDecoder() để decode về instance là được. Note: Nếu không cần phải chuyển ngược lại thành string/bytes (không cần encode) thì chỉ cần conform protocol Decodable là đủ.

Implementation: 

Define Swift struct:

struct Person: Codable {
    let name: String
    let age: Int
    let page: URL
    let bio: String?
}

Convert string to instance

// Convert json string to data
var data = Data(json.utf8)
let decoder = JSONDecoder()
// Decode json with dictionary
let personEntity = try? decoder.decode(Person.self, from: data)
if let personEntity = personEntity {
    print(personEntity)
}

Chú ý: Đối với dạng json trả về là array như dưới:

[{
    "name": "NhatHM",
    "age": 29,
    "page": "https://magz.techover.io/"
},
{
    "name": "RioV",
    "age": 19,
    "page": "https://nhathm.com/"
}]

thì chỉ cần define loại data sẽ decode cho chính xác là được:

let personEntity = try? decoder.decode([Person].self, from: data)

Ở đây ta đã định nghĩa được data decode ra sẽ là array của struct Person.

Swift Codable manual encode decode

Trong một vài trường hợp , data trả về mà chúng ta cần có thể nằm trong một key khác như dưới:

{
    "person": {
        "name": "NhatHM",
        "age": 29,
        "page": "https://magz.techover.io/"
    }
}

Trong trường hợp này, nếu define Swift struct đơn giản như phần 1 chắc chắn sẽ không thể decode được. Do đó cách làm sẽ là define struct sao cho nó tương đồng có thể với format của JSON. Ví dụ như đối với JSON ở trên, chúng ta có thể define struct như dưới:

Implementation

struct PersonData: Codable {
    struct Person: Codable {
        let name: String
        let age: Int
        let page: URL
        let bio: String? }
 
    let person: Person
}

Đối với trường hợp này, chúng ta vẫn sử dụng JSONDecoder() để decode string về instance như thường, tuy nhiên lúc sử dụng value của struct thì sẽ hơi bất tiện :

let data = Data(json.utf8)
let decoder = JSONDecoder()
let personEntity = try? decoder.decode(PersonData.self, from: data)
if let personEntity = personEntity {
    print(personEntity)
    print(personEntity.person.name)
}

Manual encode decode

Đối với dạng JSON data như này, chúng ta còn có một cách khác để xử lý data cho phù hợp, dễ dàng hơn như dưới:

Define struct (chú ý, lúc này không thể hiện struct conform to Codable nữa, mà sẽ conform to Encodable và Decodable một cách riêng biệt) :

struct Person {
    var name: String
    var age: Int
    var page: URL
    var bio: String? 
 
    enum PersonKeys: String, CodingKey {
        case person
    } 
 
    enum PersonDetailKeys: String, CodingKey {
        case name
        case age
        case page
        case bio
    }
}

Ở đây có một khái niệm mới là CodingKey. Về cơ bản, CodingKey chính là enum define các "key" mà chúng ta muốn Swift sử dụng để decode các value tương ứng. Ở đây key PersonKeys.person sẽ tương ứng với key "person" trong JSON string, các enum khác cũng tương tự (đọc thêm về CodingKey ở phần sau)

Với trường hợp này, ta sử dụng nestedContainer để đọc các value ở phía sâu của JSON, sau đó gán giá trị tương ứng cho properties của Struct.

Implementation

extension Person: Decodable {
    init(from decoder: Decoder) throws {
        let personContainer = try decoder.container(keyedBy: PersonKeys.self) 
        let personDetailContainer = try personContainer.nestedContainer(keyedBy: PersonDetailKeys.self, forKey: .person)
        name = try personDetailContainer.decode(String.self, forKey: .name)
        age = try personDetailContainer.decode(Int.self, forKey: .age)
        page = try personDetailContainer.decode(URL.self, forKey: .page)
        bio = try personDetailContainer.decodeIfPresent(String.self, forKey: .bio)
    }
}

Đây chính là phần implement để đọc ra các value ở tầng sâu của JSON, sau đó gán lại vào các properties tương ứng của struct. Các đoạn code trên có ý nghĩa như sau:

. personContainer là container tương ứng với toàn bộ JSON string
. personDetailContainer là container tương ứng với value của key person
. Nếu có các level sâu hơn thì ta lại tiếp tục sử dụng nestedContainer để đọc sâu vào trong
. Nếu một property nào đó (key value nào đó của json) mà có thể không trả về, thì sử dụng decodeIfPresent để decode (nếu không có value thì gán bằng nil)
Note: Đối với việc Encode thì cũng làm tương tự, tham khảo source code đi kèm (link cuối bài)
Với cách làm này, thì khi gọi đến properties của struct, đơn giản ta chỉ cần personEntity.name là đủ.

Swift Codable coding key
Trong đa số các trường hợp thì client sẽ sử dụng json format mà server đã định sẵn, do đó có thể gặp các kiểu json có format như sau:

{
    "person_detail": {
        "first_name": "Nhat",
        "last_name": "Hoang",
        "age": 29,
        "page": "https://magz.techover.io/"
    }
}

Đối với kiểu json như này, để struct có thể codable được thì cần phải define properties dạng person_detail, first_name. Điều này vi phạm vào coding convention của Swift. Trong trường hợp này chúng ta sử dụng Coding key để mapping giữa properties của Struct và key của JSON.

Implementation

struct Person {
    var firstName: String
    var lastName: String
    var age: Int
    var page: URL
    var bio: String? 
 
    enum PersonKeys: String, CodingKey {
        case person = "person_detail"
    } 
 
    enum PersonDetailKeys: String, CodingKey {
        case firstName = "first_name"
        case lastName = "last_name"
        case age
        case page
        case bio
    }
}

Với trường hợp này, khi sử dụng đoạn code decode như

var personDetailContainer = personContainer.nestedContainer(keyedBy: PersonDetailKeys.self, forKey: .person)

hay:

try personDetailContainer.encode(firstName, forKey: .firstName)

thì khi đó, Swift sẽ sử dụng các key json tương ứng là person_detail hoặc first_name.




Comments

Popular posts from this blog

Alamofire vs URLSession

Alamofire vs URLSession: a comparison for networking in Swift Alamofire and URLSession both help you to make network requests in Swift. The URLSession API is part of the foundation framework, whereas Alamofire needs to be added as an external dependency. Many  developers  doubt  whether it’s needed to include an extra dependency on something basic like networking in Swift. In the end, it’s perfectly doable to implement a networking layer with the great URLSession API’s which are available nowadays. This blog post is here to compare both frameworks and to find out when to add Alamofire as an external dependency. Build better iOS apps faster Looking for a great mobile CI/CD solution that has tons of iOS-specific tools, smooth code signing, and even real device testing? Learn more about Bitrise’s iOS specific solutions! This shows the real power of Alamofire as the framework makes a lot of things easier. What is Alamofire? Where URLSession...

Swift Tool Belt, Part 1: Adding a Border, Corner Radius, and Shadow to a UIView with Interface Builder

During my iOS work, I’ve assembled a set of code that I bring with me on every iOS project. I’m not talking about large frameworks or CocoaPods here. These are smaller Swift extensions or control overrides that are applicable to many projects. I think of them as my tool belt. In this post, I’ll show you an extension that will add a border, a corner radius, and a shadow to any UIView, UIButton, or UILabel and allow you to preview what it will look like in Interface Builder. Back in 2014, I wrote a blog post on Expanding User-Defined Runtime Attributes in Xcode where I added a border, corner radius, and shadow to a UIView using Interface Builder’s user-defined runtime attributes. This solution had no type checking—you had to type the property you wanted to modify by hand and often had to look up what it was called. You also had to run your project in order to see the effect of the runtime attribute. Starting with Xcode 6 , there is a new mech...

Frame vs Bounds in iOS

This article is a repost of an answer I wrote on Stack Overflow . Short description frame = a view’s location and size using the parent view’s coordinate system ( important for placing the view in the parent) bounds = a view’s location and size using its own coordinate system (important for placing the view’s content or subviews within itself) Details To help me remember frame , I think of a picture frame on a wall . The picture frame is like the border of a view. I can hang the picture anywhere I want on the wall. In the same way, I can put a view anywhere I want inside a parent view (also called a superview). The parent view is like the wall. The origin of the coordinate system in iOS is the top left. We can put our view at the origin of the superview by setting the view frame’s x-y coordinates to (0, 0), which is like hanging our picture in the very top left corner of the wall. To move it right, increase x, to move it down increase y. To help me remember bound...