Wstęp

Dzisiaj poruszę temat archiwizowania obiektów za pomocą protokołu Codable. Swift 4 wprowadził ten protokół, aby umożliwić przekształcanie naszych modeli do innych struktur danych typu Data. Warto zaznaczyć, iż do czasów Swift’a czwartego do tego typu zastosowania był wykorzystywany obiekt NSCoding, który też sprawdzał się dość dobrze, no ale po kilku latach czas pójść dalej. O problemach z NSCoding vs Codable będzie jeszcze dzisiaj słówko 😁, tymczasem 👉 do dzieła 🚀.

Codable

Dla porządku – od początku:

Codable to protokół wprowadzony wraz z releas’em czwartej wersji języka Swift. Składa się z dwóch pomniejszych protokołów Encodable i Decodable, które służą odpowiednio: serializacji i deserializacji obiektów. Wykorzystanie protokołu opisałem już wcześniej tutaj przy okazji opisywanych zmian właśnie po wprowadzeniu czwórki 😎. Pomimo powszechnego zastosowania protokołu do serializacji i deserializacji obiektów typu JSON, Codable ma dużo powszechniejsze zastosowanie, tym niemniej sprowadza się do kodowania obiektów na tym Data i odwrotnie.

Dla przykładu tworzymy prosty model Person 👇

struct Person {
	var name: String
	var lastName: String
	var age: Int
}

Żeby przygotować obiekt do serializacji – implementujemy protokół Codable do naszego obiektu. Przygotowujemy też od razu klucze, które będą potrzebne przy archiwizacji obiektu typu Peroson, no i oczywiście – konstruktor:

struct Person: Codable {
	var name: String
	var lastName: String
	var age: Int
	
	enum CodingKeys: String, CodingKey {
		case title
		case lastName
		case age
	}
	
	init(name: String, lastName: String, age: Int) {
		self.name = name
		self.lastName = lastName
		self.age = age
	}
}

Protokół Decodable pozwala nam zaimplementować konstruktor do dekodowania:

init(from decoder: Decoder) throws {
	let container = try decoder.container(keyedBy: CodingKeys.self)
	name = try container.decode(String.self, forKey: .name)
	lastName = try container.decode(String.self, forKey: .lastName)
	age = try container.decode(Int.self, forKey: .age)
}

Zaś Encodable posiada metodę encode, którą również możemy nadpisać:

func encode(to encoder: Encoder) throws {
	var container = encoder.container(keyedBy: CodingKeys.self)
	try container.encode(name, forKey: .title)
	try container.encode(lastName, forKey: .lastName)
	try container.encode(age, forKey: .age)
}

Ok 👍, wszystko gotowe, możemy przejść do archiwizowania.

Archiwizowanie obiektów do obiektu data

Na początek tworzymy sobie obiekt, którym będziemy się bawili. Zatem 👉

let person = Person(name: "Kuba", lastName: "Kornatowski", age: 18)

Teraz możemy stworzyć metody kolejno do 👉 enkodowania obiektu typu Person oraz dekodowania obiektu dla konkretnego klucza 🔑. Będzie to wyglądało następująco dla enkodowania ⬇️

func encode(_ person: Person, with key: String) {
	do {
		let personData = try PropertyListEncoder().encode(person)
		UserDefaults.standard.set(personData, forKey: key)
	} catch {
		print("Save ">Failed")
	}
}

I dekodowania ⬇️

func decodePerson(with key: String) -> Person? {
	guard let data = UserDefaults.standard.data(forKey: "person") else {
		return nil
	}
	do {
		return try PropertyListDecoder().decode(Person.self, from: data)
	} catch  {
		return nil
	}
}

Uff. Mamy to 👍. Wywołanie powyższych metod to już prosta sprawa, ale dla formalności ⬇️

let codingKey = "kuba"
encode(person, with: codingKey)

if let kuba = decodePerson(with: codingKey) {
	print(kuba.name)        //  Kuba
	print(kuba.lastName)    //  Kornatowski
}

Ok. Jesteśmy w stanie zapisać obiekty w UserDefaults. Tylko załóżmy, że mamy tych obiektów całą kolekcję. Czy zatem mamy zapisywać każdy obiekt z osobna? No właśnie…. trochę tak, trochę nie 🤪. Musimy najpierw z kolekcji elementów typu Person stworzyć kolekcję elementów typu Data i zapisać do UserDefaults. A to już akurat potrafimy, trzeba tylko lekko przerobić nasze metody encode i decode:

func encode(_ person: Person) -> Data? {
	do {
		return try PropertyListEncoder().encode(person)
	} catch {
		return nil
	}
}

func decodePerson(with data: Data) -> Person? {
	do {
		return try PropertyListDecoder().decode(Person.self, from: data)
	} catch  {
		return nil
	}
} 

I teraz już czysta formalność 😎:

// Coding key
let arrayCodingKey = "people"

// People array
let people = [
	Person(name: "Jan", lastName: "Kowalski", age: 40),
	Person(name: "Adam", lastName: "Nowak", age: 20)
]

var peopleData = [Data]()

// Encoding
for person in people {
	if let personData = encode(person) {
		peopleData.append(personData)
	}
}

UserDefaults.standard.set(peopleData, forKey: arrayCodingKey)

// Decoding
if let storeArray = UserDefaults.standard.array(forKey: arrayCodingKey) as? [Data] {
	for personData in storeArray {
		if let person = decodePerson(with: personData) {
			print(person.name) // Jan -> Adam
		}
	}
}

Główne różnice w obecnym podejściu vs NSCoding

Dla niektórych z Was ten model może wydawać się dziwny, bo przecież do tej pory każdy wykorzystywał starą, dobrą klasę NSCoding do serializacji obiektów. Hm… 🤔 To dlaczego w taki razie Codable i w ogóle po co było pisać nowy protokół, jak stary działał i – zresztą – działa?

Wyjaśniam 👉 z kilku powodów:

  1. Żeby móc korzystać z dóbr NSCoding – nasz obiekt musi być klasą. Natomiast Codable to protokół, który może być zaimplementowany wszędzie – czy będzie to klasa, struct czy enum. 💪 💪 💪.
  2. Codable w bardzo prosty sposób pozwala na bezpośrednią serializację i deserializację obiektów na inne typy danych, np property list lub JSON (o czy tutaj ➡️ https://swiftly.pl/co-nowego-w-swift-4-raz-jeszcze/
  3. Codable składa się z dwóch pod-protokołów, zatem nasz obiekt może służyć tylko enkodowaniu lub tylko dekodowaniu. Tak często jest z obiektami tworzonymi na podstawie danych z Rest API.

Coś pewnie by się jeszcze znalazło. Jeśli coś przegapiłem, dajcie znać w komentarzach 😁.

Summary

Podsumowując – Codable dla każdego, kto nie korzystał z tego typu serializacji, będzie bardzo wartościowym odkryciem, zaś dla osób korzystających dotychczas z NSCoding, polecam szybką migrację na Codable. Naprawdę dużo wartości dodanych + możliwość wyboru typu danych, które chcemy serializować dają moc i skalowalność waszych funkcji.

Mam wrażenie, że Swift wreszcie dojrzewa, z czego powinniśmy się bardzo, bardzo cieszyć 😁.

Na koniec jak zawsze proszę o jakiś feedback 🙏 czy się podobało. Możecie również pogadać ze mną na:

Twitter

Instagram

Snapchat

lub wpadnijcie na fanpage na Facebook’u!

Dzięki i do następnego wpisu!

@jkornat

Serializacja obiektów za pomocą protokołu Codable

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *