본문 바로가기

iOS/iOS

iOS) NSCoding과 Archive를 통한 데이터 저장 - (1)

decode 타입 지정 오류 해결

NSCoding과 NSKeyed(Un)Archiver

디바이스를 껐다 켜도 데이터가 영구적으로 남아있도록 하기 위해 직접 만든 클래스가 NSCoding이라는 프로토콜의 정의를 따르도록 했다. 자세하게 알아보지 않고 그냥 사용법만 대충 보고 구현해봤다. NSCoding프로토콜의 정의를 따르기 위해서는 requred initencode 메소드를 구현해야 한다. 개인적으로 이해한 바로는 encode가 데이터를 인코딩 하는 것이고 requred init에서는 인코딩된 데이터를 디코딩하여 쓰는 것이라 생각하고 있다. 위의 메소드를 직접 불러서 사용하기 보다는 NSKeyedArchiverNSKeyedUnarchiver를 통해 자동으로 불려지는 것 같다.

decode 타입 지정 오류

encode할 때에는 데이터 타입에 따라 구분이 없지만 디코딩 할때에는 다음과 같이 타입에 따라 다르게 써야한다. 처음에 타입의 다양성을 제대로 보지 않고 그냥 다 Object로 디코딩하고 뒤에 as로 타입을 확인하는 식으로 했다. 그랬더니 문제가 생겼다.

required init?(coder: NSCoder) {
    guard let brand = coder.decodeObject(forKey: "brand") as? String,
        let capacity = coder.decodeObject(forKey: "capacity") as? Int,
        let price = coder.decodeObject(forKey: "price") as? Int,
        let name = coder.decodeObject(forKey: "name") as? String,
        let manufacturedDate = coder.decodeObject(forKey: "manufacturedDate") as? Date,
        let shelfLife = coder.decodeObject(forKey: "shelfLife") as? Date,
        let calorie = coder.decodeObject(forKey: "calorie") as? Int else {
            return nil
    }
    self.brand = brand
    self.capacity = capacity
    self.price = price
    self.name = name
    self.manufacturedDate = manufacturedDate
    self.shelfLife = shelfLife
    self.calorie = calorie
}

func encode(with coder: NSCoder) {
    coder.encode(self.brand, forKey: "brand")
    coder.encode(self.capacity, forKey: "capacity")
    coder.encode(self.price, forKey: "price")
    coder.encode(self.name, forKey: "name")
    coder.encode(self.manufacturedDate, forKey: "manufacturedDate")
    coder.encode(self.shelfLife, forKey: "shelfLife")
    coder.encode(self.calorie, forKey: "calorie")
}

decodeObject 말고 다른 것을 쓸 수 있는 것에 대해서 바꿔줬더니 해당 오류는 해결되었다.

required init?(coder: NSCoder) {
    guard let brand = coder.decodeObject(forKey: "brand") as? String,
        let name = coder.decodeObject(forKey: "name") as? String,
        let manufacturedDate = coder.decodeObject(forKey: "manufacturedDate") as? Date,
        let shelfLife = coder.decodeObject(forKey: "shelfLife") as? Date
        else { return nil }
    self.brand = brand
    self.capacity = coder.decodeInteger(forKey: "capacity")
    self.price = coder.decodeInteger(forKey: "price")
    self.name = name
    self.manufacturedDate = manufacturedDate
    self.shelfLife = shelfLife
    self.calorie = coder.decodeInteger(forKey: "calorie")
}

XCTAssertEqual의 클래스 비교 오류

그렇지만 이번엔 다른 오류가 발생하였다. 분명 static func ==을 정의해줬음에도 해당 클래스의 description이 나왔다. 심지어 description이 동일함에도 불구하고 xcode는 beverage1beverage2가 다르다고 판단했다.

override var description: String {
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyyMMdd"
    //제조사, 용량, 가격, 브랜드, 제조일자
    return "\(name), \(capacity)ml, \(price)원, \(brand), \(formatter.string(from: manufacturedDate))"
}
static func ==(lhs: Beverage, rhs: Beverage) -> Bool {
    if lhs.brand != rhs.brand { return false }
    if lhs.capacity != rhs.capacity { return false }
    if lhs.price != rhs.price { return false }
    if lhs.name != rhs.name { return false }
    if lhs.manufacturedDate != rhs.manufacturedDate { return false }
    if lhs.shelfLife != rhs.shelfLife { return false }
    if lhs.calorie != rhs.calorie { return false }
    return true
}

아무래도 XCTAssertEqual==으로 비교하는 것이 아니고 객체 그 자체 즉 메모리 주소까지 비교하는 ===을 사용하는 것 같아서 찾아보니 그런 듯 하였다.

XCTAssertEqual for custom objects in Swift

오류 해결

결국 참고 문서에 나온 대로 XCTAssertEqual 대신 XCTAssert를 사용하였더니 문제는 해결되었다.

func testNSCoding() throws {
    let brand = "조지아"
    let capacity = 200
    let price = 2000
    let name = "고티카"
    let shelfLife = 100
    let calorie = 130
    let beverage1 = Beverage(brand: brand, capacity: capacity, price: price, name: name, shelfLife: shelfLife, calorie: calorie)
    guard let data = try? NSKeyedArchiver.archivedData(withRootObject: beverage1, requiringSecureCoding: false) else {
        XCTAssert(false, "beverage1 아카이브 실패")
        return
    }
    guard let beverage2 = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? Beverage else {
        XCTAssert(false, "beverage1 언아카이브 실패")
        return
    }
    XCTAssert(beverage1 == beverage2)
}