Contents
Chào bạn đến với Fx Studio,
Bài viết hôm nay sẽ nói về chủ đề khá quen thuộc trong lập trình iOS. Đó là Encoding và Decoding trong Swift.
Chuẩn bị
Bạn chỉ cần làm việc với Playground là đủ rồi, không cần phải chuẩn bị gì nhiều thêm. Với Swift thì 4.0 trở lên.
1. Overview
Đây là một vấn đề 2 chiều, bạn hãy tưởng tượng ra một ví dụ như thế này:
- Lưu một đối tượng thành dữ liệu vào file/DB/gởi tới server
- Biến đổi dữ liệu nhận được thành đối tượng
Công việc này bạn lặp đi lặp lại hằng ngày, biến đổi và lưu trữ dữ liệu thành các đối tượng với kiểu dữ liệu phù hợp. Nhất là khi bạn làm việc với Networking, tương tác với API để lấy dữ liệu về. Nghe qua thì công việc có gì vất vả đâu nhĩ?
Nhưng với 1 đối tượng có tới cả chục thuộc tính, mà cấu trúc JSON bạn nhận được lại lồng rất nhiều vòng. Càng đau khổ hơn nữa là dữ liệu nhận được có thể đúng có thể sai. Trường hợp kinh điển như ví dụ sau:
Ví dụ:
- Bạn nhận được
"9999"
- Đó là kiểu String, nhưng người làm backend lại đang gởi cho bạn kiểu Int.
Vâng vâng và mây mây, cái trên chỉ nói tới một vấn đề đơn giản là chuyển đổi qua lại giữa các kiểu dữ liệu một cách nhanh chóng và an toàn nhất. Ta có:
- Quá trình chuyển đổi đó gọi là encoding hay còn được biết như là serialization.
- Quá trình chuyển đổi ngược lại từ dữ liệu đã mã hoá về dữ liệu ban đầu được gọi là decoding hay còn gọi là deserialization.
Chúng ta sẽ có 3 protocol sau:
1.1. Encodable
Giao thức Encodable được sử dụng bởi các loại có thể được mã hóa sang biểu diễn khác. Nó khai báo như sau:
func encode(to: Encoder) throws
1.2. Decodable
Giao thức Decodable được sử dụng bởi các loại có thể được giải mã. Được khai báo như sau:
init(from decoder: Decoder) throws
1.3. Codable
Codable là một phương thức chung khai báo cho việc encode và decode. Khai báo như sau:
typealias Codable = Encodable & Decodable
1.4. Ví dụ
Phải mục sở thị một lần mới biết được. Ta có 1 json object
như sau:
let json: [ String : Any ] = [ "name" : "Chuột FX", "age" : 18, "dog" : [ "name" : "Pochi", "age" : 2 ] ]
Để phục vụ cho Encoding và Decoding thì chúng ta mượn tạm 2 đối tượng sau:
let encoder = JSONEncoder() let decoder = JSONDecoder()
- Bước 1 : biến
json object
thànhdata
let data = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted)
- Bước 2 : biến
data
thànhString
let string = String(data: data, encoding: .utf8)!
Dữ liệu cuối cùng biến đổi được như sau:
{ "dog" : { "age" : 2, "name" : "Pochi" }, "age" : 18, "name" : "Chuột FX" }
Và bạn có thể làm ngược lại. Mô tả 1 json object
bằng 1 String
, rồi với Encoding và Decoding, để biến đổi nó thành Dictionary
.
2. Automatic Encoding and Decoding
Ta có các kiểu dữ liệu cơ bản từ Standard Library và Foundation framework như Int, Float, String … thì có thể được Encoding và Decoding một cách tự động. Và bạn muốn kiểu dữ liệu của mình tạo ra cũng tự động có được siêu năng lực như vậy thì:
Tất các các thuộc tính đều phải là các kiểu dữ liệu có thể Encoding và Decoding được.
Khà khà, lấy ví dụ cho dễ hình dung nào. Ta có 1 struct User
như sau:
struct User { var name : String var age : Int }
Tất cả những gì bạn cần làm để có thể Encoding và Decoding loại này là tuân thủ giao thức Codable, như sau:
struct User : Codable { var name : String var age : Int }
EZ Game! Bạn đã có thể làm điều đó, vì cả name
(String) và age
(Int) đều có thể mã hóa được. Điều này hoạt động tốt khi bạn chỉ sử dụng các loại đã Codable. Nhưng nếu loại của bạn bao gồm các loại tùy chỉnh khác cho thuộc tính thì sao?
Lại xem tiếp ví dụ, bây giờ mình có thêm 1 thuộc tính dog
với kiểu dữ liệu là Dog
:
struct User : Codable { var name : String var age : Int var dog : Dog }
Thì struct Dog
kia cũng phải tuân thủ đúng theo giao thức Codable. Và ta có khai báo như sau:
struct Dog : Codable { var name : String var age : Int }
Bây giờ thử xem biến đổi như thế nào nha:
- Tạo 2 đối tượng từ 2 struct
User
&Dog
let dog = Dog(name: "Milu", age: 2) let user = User(name: "Fx", age: 19, dog: dog)
- Vẫn dùng 2 đối tượng phục vụ Encoding và Decoding
let encoder = JSONEncoder() let decoder = JSONDecoder()
- Chuyển đổi đối tượng User thành
data
let data = try encoder.encode(user)
- Biến
data
thành String
let string = String(data: data, encoding: .utf8)!
Và đây là kết quả nhận được:
{"name":"Fx","age":19,"dog":{"name":"Milu","age":2}}
3. Encoding and Decoding Custom Types
Có rất nhiều kiểu dữ liệu mà bạn có thể biến đổi được, như: XML, Properties List, JSON … Và phần này thì ta sẽ tập trung vào việc biến đổi qua lại giữa
JSON và Custom Type
Tại sao là JSON? Đơn giản vì nó hiện tại đang rất là phổ biến. Được xem là chuẩn dữ liệu cho việc tương tác Networking.
3.1. JSONEncoder and JSONDecoder
JSON là viết tắt của JavaScript Object Notation và là một trong những cách phổ biến nhất để tuần tự hóa dữ liệu. Nó có thể dễ dàng đọc được bởi con người và dễ dàng cho máy tính phân tích và tạo ra.
Hai class mà chúng ta dùng để Encoding và Decoding như sau:
JSONEncode
JSONDecode
Bạn có thể mô tả đối tượng User
ở trên bằng JSON như sau:
{ "name": "Fx", "age": 19, "dog": { "name":"Milu", "age":2 } }
Bạn có thể dễ dàng hiểu được cá thể User trông như thế nào trước khi nó được tuần tự hóa thành JSON.
3.1.1. JSONEncoder
Khi bạn có một loại Codable, bạn có thể sử dụng JSONEncoder để chuyển đổi loại của bạn thành Data có thể được ghi vào một tệp hoặc được gửi qua mạng. Giả sử bạn có ví dụ User này:
let dog = Dog(name: "Milu", age: 2) let user = User(name: "Fx", age: 19, dog: dog)
Tạo đối tượng encode
let encoder = JSONEncoder()
Biến nó thành data
để tiện tay gởi đi đâu thì gởi
let data = try encoder.encode(user)
3.1.2. JSONDecoder
Muốn biến đổi ngược lại thì cần phải đối tượng giải mã và ta sử dụng class JSONDecoder
let decoder = JSONDecoder()
Việc còn lại thì khá là EZ
let anotherUser = try decoder.decode(User.self, from: data) print(anotherUser)
Kết quả in ra như sau:
User(name: "Fx", age: 19, dog: __lldb_expr_1.Dog(name: "Milu", age: 2))
3.2. Working With Custom JSON Keys
Nếu như một ngày đẹp trời, đồng đội của bạn trong dự án, là 1 backend. Quyết định tăng độ khó cho game, âm thầm đổi key
của cấu trúc JSON. Bạn lại phải dò lại từ đầu và thay đổi từng thuộc tính cho phù hợp. Và ông trời không tuyệt đường sống của ai bao giờ. Bạn xem tiếp phần sau.
let json: [ String : Any ] = [ "fullname" : "Chuột FX", "age" : 18, "dog" : [ "name" : "Pochi", "age" : 2 ] ]
Bạn chú ý fullname
là key mới, nó khác với property name
của User đã được khai báo ở trên. Công việc lúc này là chỉnh sửa lại một tí:
struct User : Codable { var name : String var age : Int var dog : Dog enum CodingKeys: String, CodingKey { case name = "fullname" case age case dog } }
Enum CodingKeys phù hợp với giao thức CodingKey, cho phép bạn đổi tên các thuộc tính cụ thể trong trường hợp định dạng được tuần tự hóa không phù hợp với các yêu cầu của API.
Lưu ý như sau:
- CodingKeys là một phép liệt kê lồng nhau trong kiểu của bạn.
- Nó phải là 1 loại CodingKey
- Các khoá phải là kiểu String
- Bạn phải bao gồm tất cả các thuộc tính trong bảng liệt kê, ngay cả khi bạn không có kế hoạch đổi tên chúng.
- Theo mặc định, phép liệt kê này được tạo bởi trình biên dịch, nhưng khi bạn cần đổi tên một khóa, bạn cần phải tự thực hiện nó.
Xem chúng hoạt động ổn không nào.
- Biến đổi
json object
thànhdata
let encoder = JSONEncoder() let data = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted)
- Biến đổi
data
thànhUser
let decoder = JSONDecoder() let user = try decoder.decode(User.self, from: data) print(user)
Kết quả in ra như sau:
User(name: "Chuột FX", age: 18, dog: __lldb_expr_1.Dog(name: "Pochi", age: 2))
4. Manual Encoding and Decoding
4.1. Subsclass
Khi bạn nhận được một sự thay đổi nữa từ người bạn thân thiện backend kia trong dự án của bạn. Bạn ta lần này không chỉ đổi tên thuộc tính mà còn thay đổi cả cấu trúc JSON.
{ "fullname" : "Chuột FX" "age" : 18, "pet" : "Milu" }
Và lần này độ khó cho game đã tăng lên rất nhiều. Bạn không thể xoá đi viết lại cả hàng trăm file source code. Nhưng trời xanh vẫn cho ta một con đường sống.
Bạn tiến hành sử dụng phương pháp cuối này. Tạo sub-class
để tối ưu cho việc Encoding và Decoding. Cũng như có thể tương thích với các kiểu dữ liệu khác không phải Codable
. Okay, chiến thôi!
Vẫn ví dụ trên, ta tạo 1 class tên là BaseUser
như sau:
class BaseUser : Codable { var name : String var age : Int var dog : Dog enum CodingKeys: String, CodingKey { case name = "fullname" case age case dog = "pet" } }
Mọi việc vẫn chưa có gì thay đổi quá nhiều. Bạn cần kế thừa protocol Codable
để có thể Encoding và Decoding. Ngoài ra, điều bắt buộc với các sub-class
thì cần phải khai báo enum CodingKeys
. Bạn chú ý phải tương thích với cấu trúc JSON.
Tiếp theo là cần tạo hoàn cảnh cho class này thực thi. Bạn có một json object
như sau:
let json: [ String : Any ] = [ "fullname" : "Chuột FX", "age" : 18, "pet" : "Milu" ]
Vẫn là công việc cũ, biến đổi nó thành data
. Cái này giả lập việc bạn nhận data
trực tiếp từ server hay đọc file.
let encoder = JSONEncoder() let data = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted)
OKAY! Sang 2 phần tiếp theo nào.
4.2. Encode function
Codable thực sự chỉ là một kiểu dành cho các giao thức Encodable và Decodable. Bạn cần triển khai encode(to: Encoder)
và mô tả cách mã hóa từng thuộc tính.
Chúng ta thêm function encode
vào cho BaseUser
func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(name, forKey: .name) try container.encode(age, forKey: .age) try container.encode(dog.name, forKey: .dog) }
Đầu tiên bạn lấy lại container
của bộ encoder. Đây là chế độ xem vào bộ lưu trữ của bộ encoder mà bạn có thể truy cập bằng các key.
Lưu ý cách bạn chọn thuộc tính nào để mã hóa cho key nào. Bạn sẽ thấy với key dog
, chúng ta lại dùng dog.name
của đối tượng.
4.3. Decode function
Cuối cùng là bạn biến đổi dữ liệu nhận được thành chính đối tượng của bạn. Tuy nhiên, lần này cần phải thao tác thêm. Và bạn sử dụng function decode
của protocol Decodable.
Bạn thêm function này vào BaseUser
required init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) name = try values.decode(String.self, forKey: .name) age = try values.decode(Int.self, forKey: .age) let pet = try values.decode(String.self, forKey: .dog) dog = Dog(name: pet, age: 0) }
Chúng ta sử dụng init
, vì đây là phương pháp tạo ra một đối tượng. Bạn cũng sẽ thấy việc biến đổi ngược lại:
- Lấy
values
từcontainer
của decoder - Trích xuất từng dữ liệu phù hợp với từng thuộc tính của đối tượng, chúng tương ứng với nhau qua CodingKeys
- Với thuộc tính nào cần phải biến đổi, thì thêm một chút gia vị cho nó đậm đà. Với
dog
, chúng ta cần phải tạo đối tượng mới từ struct Dog với dữ liệupet
choname
của Dog.
Xem hoạt động như thế nào, tiếp tục với đoạn code ở trên. Chúng ta sử dụng data
vừa được mã hoá để phục vụ cho việc giải mã.
let decoder = JSONDecoder() let baseUser = try decoder.decode(BaseUser.self, from: data) print(baseUser.dog)
Kết quả in ra như sau:
Dog(name: "Milu", age: 0)
OKAY! Chúng ta sẽ kết thúc bài viết này tại đây. Hi vọng chúng sẽ giúp ích được cho bạn trong việc chiến đấu với các anh bạn backend thân thiện. Và bạn có thể sử dụng bài viết này để bổ sung kiến thức cho series Lập trình iOS cho mọi người
Tạm kết
- Tìm hiểu về các protocol Encodable, Decodable và Codable
- Cách hoạt động của Encoding và Decoding
- Các cách chuyển đổi dữ liệu qua lại tự động
- Custom kiểu dữ liệu để có thể Codable
- Tuỳ biến các key với CodingKeys
- Xử lý bằng tay với các cấu trúc JSON phức tạp hoặc có điều chỉnh riêng với các function encode và decode
(Bài viết được tham khảo từ raywenderlich.com.)
Cảm ơn bạn đã đọc bài viết này!
Related Posts:
Written by chuotfx
Hãy ngồi xuống, uống miếng bánh và ăn miếng trà. Chúng ta cùng nhau đàm đạo về đời, về code nhóe!
2 comments
Leave a Reply Cancel reply
Fan page
Tags
Recent Posts
- Charles Proxy – Phần 1 : Giới thiệu, cài đặt và cấu hình
- Complete Concurrency với Swift 6
- 300 Bài code thiếu nhi bằng Python – Ebook
- Builder Pattern trong 10 phút
- Observer Pattern trong 10 phút
- Memento Pattern trong 10 phút
- Strategy Pattern trong 10 phút
- Automatic Reference Counting (ARC) trong 10 phút
- Autoresizing Masks trong 10 phút
- Regular Expression (Regex) trong Swift
You may also like:
Archives
- September 2024 (1)
- July 2024 (1)
- June 2024 (1)
- May 2024 (4)
- April 2024 (2)
- March 2024 (5)
- January 2024 (4)
- February 2023 (1)
- January 2023 (2)
- November 2022 (2)
- October 2022 (1)
- September 2022 (5)
- August 2022 (6)
- July 2022 (7)
- June 2022 (8)
- May 2022 (5)
- April 2022 (1)
- March 2022 (3)
- February 2022 (5)
- January 2022 (4)
- December 2021 (6)
- November 2021 (8)
- October 2021 (8)
- September 2021 (8)
- August 2021 (8)
- July 2021 (9)
- June 2021 (8)
- May 2021 (7)
- April 2021 (11)
- March 2021 (12)
- February 2021 (3)
- January 2021 (3)
- December 2020 (3)
- November 2020 (9)
- October 2020 (7)
- September 2020 (17)
- August 2020 (1)
- July 2020 (3)
- June 2020 (1)
- May 2020 (2)
- April 2020 (3)
- March 2020 (20)
- February 2020 (5)
- January 2020 (2)
- December 2019 (12)
- November 2019 (12)
- October 2019 (19)
- September 2019 (17)
- August 2019 (10)
Cảm ơn Bác đã viết ra những bài hay như này. Để ae lấn sân sang iOS hoặc newbie có cái nhìn trực quan hơn. <3
Thank you! <3