Swift(스위프트): Codable을 사용하여 사용자 정의 오브젝트를 UserDefaults에 저장 (프로토콜 지향 접근 방식)
원문
소개
iOS는 Int, String, Float, Double, Bool, URL, Data 또는 이러한 유형의 컬렉션과 같은 UserDefaults에 직접 저장할 수 있는 여러 유형의 객체를 지원합니다.
하지만 위의 목록에 없는 사용자 정의 오브젝트를 UserDefaults에 저장하려면 어떻게 해야 할까요?🤔
다른 타입의 오브젝트를 저장하려면 일반적으로 해당 개체를 아카이브하여
NSData인스턴스를 생성해야 합니다.
이제 사용자 정의 오브젝트를 UserDefaults에 저장하려면 먼저 이를 Data 오브젝트로 변환해야 한다는 것을 알게 되었습니다. 다행히도 가능한 몇 가지 옵션이 있습니다. 이를 위해 Codable을 사용할 것입니다.
어떤 개발자도 프로젝트에 장황한 반복 코드 (boilerplate code)를 갖고 싶어하지 않으므로 프로토콜을 사용하여 수행하겠습니다. 우리는 한 번 작성하면 프로젝트의 어느 곳에서나 사용할 수 있는 메서드를 만들 것입니다.
프로토콜 정의
UserDefaults에서 사용자 정의 개체를 저장하고 검색할 수 있는 메서드 요구 사항을 선언하는 프로토콜 ObjectSavable을 정의해 보겠습니다.
1
2
3
4
protocol ObjectSavable {
func setObject<Object>(_ object: Object, forKey: String) throws where Object: Encodable
func getObject<Object>(forKey: String, castTo type: Object.Type) throws -> Object where Object: Decodable
}
setObject메소드는 유형이Encodable프로토콜을 준수하는 오브젝트와 연관된 키 이름을 사용하여UserDefaults에 저장할 수 있도록 합니다.getObject메소드는UserDefaults에서 연관된 오브젝트를 검색할 키와Decodable프로토콜을 준수하는 타입을 사용하여UserDefault로부터 데이터를 가져올 수 있도록 합니다. 타입 자체를 전달하기 위해 Metatype😕을 사용했습니다.
메타타입(
Metatype) 유형은 클래스(class) 타입, 구조체(struct) 타입,enum타입 및 프로토콜(protocol) 타입을 포함한 모든 타입의 타입(type of any type,을 나타냅니다.
UserDefaults 확장 구현
ObjectSavable 프로토콜이 준비되었습니다. UserDefaults 클래스를 준수하고 요구 사항에 대한 구현을 제공하겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
extension UserDefaults: ObjectSavable {
func setObject<Object>(_ object: Object, forKey: String) throws where Object: Encodable {
let encoder = JSONEncoder()
do {
let data = try encoder.encode(object)
set(data, forKey: forKey)
} catch {
throw ObjectSavableError.unableToEncode
}
}
func getObject<Object>(forKey: String, castTo type: Object.Type) throws -> Object where Object: Decodable {
guard let data = data(forKey: forKey) else { throw ObjectSavableError.noValue }
let decoder = JSONDecoder()
do {
let object = try decoder.decode(type, from: data)
return object
} catch {
throw ObjectSavableError.unableToDecode
}
}
}
setObject사용자 지정 오브젝트를Data오브젝트로 변환하는 데 사용할JSONEncoder오브젝트를 만들었습니다. 변환된Data오브젝트가 있으면 주어진 키를 사용하여 이를UserDefaults에 저장합니다.getObject주어진 키에 대해UserDefaults에서 오브젝트를 검색합니다.guard문을 사용하여 검색된 데이터 오브젝트를 프로퍼티에 안전하게 바인딩합니다. 마지막으로 이Data오브젝트를 주어진 유형으로 디코딩하고 반환합니다.
참고로 위의 방법 단계 중 하나라도 실패하면 개발자에게 무엇이 잘못되었는지 알려주는 관련 에러가 발생합니다.
에러를 관리하기 위해 LocalizedError 프로토콜을 준수하는 String 타입의 enum인 ObjectSavableError를 만들겠습니다.
1
2
3
4
5
6
7
8
9
enum ObjectSavableError: String, LocalizedError {
case unableToEncode = "Unable to encode object into data"
case noValue = "No data object found for the given key"
case unableToDecode = "Unable to decode object into given type"
var errorDescription: String? {
rawValue
}
}
실제 사용 예제
이제 위의 방법을 사용하여 오브젝트를 저장하고 검색하는 방법을 살펴보겠습니다.
다음과 같이 구조체 Book 을 선언합니다.
1
2
3
4
5
struct Book: Codable {
var title: String
var authorName: String
var pageCount: Int
}
저장 (Save)
Book의 오브젝트를 만들고 setObject 메서드에 키와 함께 전달합니다. 이 방법은 실패 시 에러가 발생할 수 있으므로 do-catch 문을 사용합니다.
1
2
3
4
5
6
7
let playingItMyWay = Book(title: "Playing It My Way", authorName: "Sachin Tendulkar & Boria Mazumder", pageCount: 486)
let userDefaults = UserDefaults.standard
do {
try userDefaults.setObject(playingItMyWay, forKey: "MyFavouriteBook")
} catch {
print(error.localizedDescription)
}
검색 (Retrieve)
forKey에 키 이름을 입력하고, castTo에 저장된 오브젝트의 타입을 [타입이름].self 형식으로 입력합니다
1
2
3
4
5
6
7
8
9
10
let userDefaults = UserDefaults.standard
do {
let playingItMyWay = try userDefaults.getObject(forKey: "MyFavouriteBook", castTo: Book.self)
print(playingItMyWay)
} catch {
print(error.localizedDescription)
}
// Output
// Book(title: "Playing It My Way", authorName: "Sachin Tendulkar & Boria Mazumder", pageCount: 486)
이것이 사용자 정의 유형을 UserDefaults에 저장하고 검색하는 방법입니다.
전체 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import Foundation
protocol ObjectSavable {
func setObject<Object>(_ object: Object, forKey: String) throws where Object: Encodable
func getObject<Object>(forKey: String, castTo type: Object.Type) throws -> Object where Object: Decodable
}
enum ObjectSavableError: String, LocalizedError {
case unableToEncode = "Unable to encode object into data"
case noValue = "No data object found for the given key"
case unableToDecode = "Unable to decode object into given type"
var errorDescription: String? {
rawValue
}
}
extension UserDefaults: ObjectSavable {
func setObject<Object>(_ object: Object, forKey: String) throws where Object: Encodable {
let encoder = JSONEncoder()
do {
let data = try encoder.encode(object)
set(data, forKey: forKey)
} catch {
throw ObjectSavableError.unableToEncode
}
}
func getObject<Object>(forKey: String, castTo type: Object.Type) throws -> Object where Object: Decodable {
guard let data = data(forKey: forKey) else { throw ObjectSavableError.noValue }
let decoder = JSONDecoder()
do {
let object = try decoder.decode(type, from: data)
return object
} catch {
throw ObjectSavableError.unableToDecode
}
}
}