관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] UserDefaults에 struct 형태 저장 방법 (“Attempt to insert non-property list object” 오류, UserDefaultsWrapper, UserDefaultManager, propertyWrapper, UserDefault) 본문

iOS 응용 (swift)

[iOS - swift] UserDefaults에 struct 형태 저장 방법 (“Attempt to insert non-property list object” 오류, UserDefaultsWrapper, UserDefaultManager, propertyWrapper, UserDefault)

jake-kim 2021. 9. 14. 23:35

UserDefaults를 이해하기 위한 기본 지식

  • Byte buffer: 연속적으로 할당된 raw bytes를 저장하는 역할
    • random access가 가능하여 데이터를 key-value쌍으로 저장하고 로드할때 용이
    • 보통 스위프트에서 메모리나 디스크에 객체의 정보를 저장할 때 ByteBuffer를 사용하여 저장

  • Data: 메모리에서의 byte buffer
    • 객체를 byte buffer형태로 취하게 할 수 있는 구조체

UserDefauls의 원리

  • 저장: 요청 > struct 객체 > Data형 > 메모리, 디스크에 저장
    • *아카이빙: 객체를 Data형과 같이 바이트형태로 변경하는 작업이며 객체를 메모리, 디스크에 저장할 수 있는 파일 형식으로 만드는 것
  • 로드: 요청 > 메모리, 디스크에서 저장된 형태 탐색 > Data형 > struct 객체 > 획득
    • *언아카이빙: 메모리, 디스크에 저장된 Data형태의 바이트형태를 스위프트의 struct 객체와 같은 형태로 변경하는 것
  • NSCoding을 이용한 아카이빙, 언아카이빙 개념 참고

UserDefaults에 struct형

  • 기본 타입인 Int, Double, String은 아카아빙, 언아카이빙이 내부적으로 UserDefaults를 사용할 때 적용이 되어서 바로 사용 가능하지만, struct같은 경우 아카이빙, 언아카이빙 작업이 별도로 필요
  • 아카이빙, 언아카이빙을 하지 않으면 아래처럼 에러 발생 "Attempt to insert non-property list object"

  • 아카이빙, 언아카이빙을 위해서 UserDefaults를 사용할 struct에 Codable 프로토콜 준수
struct Person: Codable {
    let name: String
    let age: Int
}
  • 저장
    • JSONEncoder를 이용하여 객체를 아카이빙: 객체를 Memory, Disk에 저장할 수 있는 Data형으로 변환 후 변환된 Data형으로 UserDefaults에 저장
let person = Person(name: "jake", age: 20)

let encoder = JSONEncoder()

/// encoded는 Data형
if let encoded = try? encoder.encode(person) {
    UserDefaults.standard.setValue(encoded, forKey: "person")
}
  • 로드
    • decode 없이(언아카이빙) 로드 시 Data형태로 로드
struct Person: Codable {
    let name: String
    let age: Int
}

let person = Person(name: "jake", age: 20)

let encoder = JSONEncoder()
if let encoded = try? encoder.encode(person) {
    UserDefaults.standard.setValue(encoded, forKey: "person")
}

let savedPerson = UserDefaults.standard.object(forKey: "person")
print(savedPerson) // Optional(<7b226e61 6d65223a 226a616b 65222c22 61676522 3a32307d>)
  • JSONDecoder를 이용 언아카이빙하여 로드
if let savedData = UserDefaults.standard.object(forKey: "person") as? Data {
    let decoder = JSONDecoder()
    if let savedObject = try? decoder.decode(Person.self, from: savedData) {
        print(savedObject) // Person(name: "jake", age: 20)
    }
}

propertyWrapper를 이용하여 아카이빙, 언아카이빙 내부적으로 구현

  • propertyWrapper 개념 참고
  • 아이디어
    • propertyWrapper는 특정 프로퍼티에 관한 성격을 더하는 역할 - wrapping되어 computed property 내부인 get과 set을 정의
    • static var로 UserDefaults에 접근하는 computedProperty를 만드는데, 이 프로퍼티에서 get할때는 언아카이빙을 하고, set할때는 아카이빙을 해서 구조체도 저장되도록 구현
  • 프로퍼티 시그니처 예시) 사용할때 UserDefaultManager.personList으로 접근하여 set, get이 가능하고, UserDefaults의 key값은 "personList"이고 defaultValue는 nil로 초기화되어 사용할수 있는 @UserDefaultWrapper정의

 

struct UserDefaultsManager {
    @UserDefaultWrapper(key: "personList", defaultValue: nil)
    static var personList: [Person]?
    
    @UserDefaultWrapper(key: "wordList", defaultValue: nil)
    static var wordList: [Word]?
}
  • UserDefaultWrapper 정의
@propertyWrapper
struct UserDefaultWrapper<T: Codable> {
    private let key: String
    private let defaultValue: T?

    init(key: String, defaultValue: T?) {
        self.key = key
        self.defaultValue = defaultValue
    }
    
    wrappedValue: T? {
    	get {
 		// 언아카이빙 정의: JSONDecoder를 이용
        }
        
        set {
		// 아카이빙 정의: JSONEnocder를 이용
        }
    }
    
}
  • 아카이빙, 언아카이빙 구현
var wrappedValue: T? {
    get {
        if let savedData = UserDefaults.standard.object(forKey: key) as? Data {
            let decoder = JSONDecoder()
            if let lodedObejct = try? decoder.decode(T.self, from: savedData) {
                return lodedObejct
            }
        }
        return defaultValue
    }
    set {
        let encoder = JSONEncoder()
        if let encoded = try? encoder.encode(newValue) {
            UserDefaults.standard.setValue(encoded, forKey: key)
        }
    }
}
  • 사용하는 쪽
struct Person: Codable {
    let name: String
    let age: Int
}

let person1 = Person(name: "jake", age: 20)
let person2 = Person(name: "Kim", age: 20)

UserDefaultsManager.personList = [person1, person2]
let savedPeople = UserDefaultsManager.personList
print(savedPeople)

* 전체 소스 코드: https://github.com/JK0369/UserDefaultsExample

 

* 참고

- byte buffer: https://apple.github.io/swift-nio/docs/current/NIO/Structs/ByteBuffer.html

- Data: https://developer.apple.com/documentation/foundation/data

Comments