NSCodingNSObjectProtocol이라는 클래스 프로토콜이 필요하다. 그리고 그 프로토콜은 구제체가 따를 순 없다. NSCoding을 사용해서 인코딩하고 싶다면 가장 쉬운 방법이 클래스로 만들어 NSObject를 상속받는 것이다.

나는 구조체를 NSCoding으로 감쌀 수 있는 말끔한 방법을 찾았고 거추장스러운 작업 없이 저장할 수 있다. 예제로서 Coordinate를 사용할 것이다.
struct Coordinate: JSONInitializable {
let latitude: Double
let longitude: Double
init(latitude: Double, longitude: Double) {
self.latitude = latitude
self.longitude = longitude
}
}

두개의 스칼라 프로퍼티를 가지는 간단한 한 타입이다. 이제 NSCoding을 따르고 Coordinate를 감싸는 클래스를 만들어보자.
class EncodableCoordinate: NSObject, NSCoding {
var coordinate: Coordinate?
init(coordinate: Coordinate?) {
self.coordinate = coordinate
}
required init?(coder decoder: NSCoder) {
guard
let latitude = decoder.decodeObject(forKey: "latitude") as? Double,
let longitude = decoder.decodeObject(forKey: "longitude") as? Double
else { return nil }
coordinate = Coordinate(latitude: latitude, longitude: longitude)
}
func encode(with encoder: NSCoder) {
encoder.encode(coordinate?.latitude, forKey: "latitude")
encoder.encode(coordinate?.longitude, forKey: "longitude")
}
}

이 로직을 다른 유형으로 사용하는 것이 좋으며 단일 책임 원칙에 더 잘 지킨다. 눈치 빠른 독자는 위 클래스에 Coordinate 프로퍼티의 EncodableCorrdinate가 옵셔널이라는 것을 눈치 챘을 것이지만, 꼭 그럴 필요는 없음을 알 수 있을 것이다. 우리는 옵셔널이 아닌 Coordinate를 받는(혹은 실패하게 만드는) 생성자를 만들 수 있는데, 그렇다면 이미 init(coder:) 메소드는 불가능하다. 그리하여 EnabledCoordinate 클래스 인스턴스를 들고 있을때 항상 coordinate를 가지고 있음을 보장해주어야한다.

그러나 더블(Double)타입(혹은 어떤 원시 타입)을 인코딩 할 때 NSCoder 동작 방법의 특징으로 인해 Any?를 반환하는 decodeObejct(forKey:)로 추출해낼 수 없게 되었다. DoubledecodeDouble(forKey:)로 하여 특정 윈시타입에 특정 메소드가 있다. 불행히도 이 특정 메소드는 옵셔널을 반환하지 않으며, 키가 맞지 않거나 오류가 나면 0.0을 반환해 버린다. 이러한 이유 때문에 coordinate 프로퍼티를 옵셔널로 두기로 했고 옵셔널로 인코딩한다. 그리하여 나는 decodeObject(forKey:)를 사용해 Double?을 얻어냄으로써 추가적인 안정성을 보장했다.

이제 Coordinate 오브젝트를 인코딩/디코딩 하기 위해 EncodableCoordinate를 하나 만들고 NSKeyedArchiver로 디스크에 저장할 수 있다.
let encodable = EncodableCoordinate(coordinate: coordinate)
let data = NSKeyedArchiver.archiveRootObject(encodable, toFile: somePath)

이러한 추가적인 오브젝트를 만드는게 이상적이진 않으며, 나는 "가능하면 나를 캐싱해줘" 글에서 사용한 SKCache와같은 오브젝트로 작업하기를 좋아한다. 그리하여 내가 인코더와 인코딩 된 사이의 관계를 형식화 할 수 있다면, 아마 매번 NSCoding 컨테이너를 만들지 않아도 될 것이다.

그 끝으로 두가지 프로토콜을 추가하자.
protocol Encoded {
associatedtype Encoder: NSCoding
var encoder: Encoder { get }
}
protocol Encodable {
associatedtype Value
var value: Value? { get }
}
view raw Encode.swift hosted with ❤ by GitHub
그리고 우리의 두가지 타입에 대해 적용시킨다.
extension EncodableCoordinate: Encodable {
var value: Coordinate? {
return coordinate
}
}
extension Coordinate: Encoded {
var encoder: EncodableCoordinate {
return EncodableCoordinate(coordinate: self)
}
}

이렇게하여 이제 타입 시스템은 오브젝트 쌍에서 타입과 값 사이에서 어떻게 변환하여 넣고 빼는지 안다.
class Cache<T: Encoded> where T.Encoder: Encodable, T.Encoder.Value == T {
//...
}
view raw Cache.swift hosted with ❤ by GitHub

블로그 포스팅에서 나온 SKCache 오브젝트는 Encoded 타입을 넘어 제네릭으로 업그레이드 되어왔다. 인코더 값 타입이 그 자신이라는 조건으로, 이것은 이 두 타입 사이에 쌍방향으로 변환할 수 있게 해준다.

이 타입에서 마지막 퍼즐조각인 savefetch 메소드가 남아있다. saveencoder를 잡아두고있고(사실 이것은 NSCoding을 따르는 오브젝트이다) path로 저장해둔다.
func save(object: T) {
NSKeyedArchiver.archiveRootObject(object.encoder, toFile: path)
}
view raw save.swift hosted with ❤ by GitHub

패칭은 약간의 컴파일러 댄스(compiler dance)를 포함한다. 우리는 언아카이브(unarchive)된 오브젝트를 T.Encoable로 캐스팅 해야하는데, 이것은 encoder 타입이다. 그리고 그 값을 잡아두고, 동적으로 그것을 캐스팅하여 T로 돌려준다.
func fetchObject() -> T? {
let fetchedEncoder = NSKeyedUnarchiver.unarchiveObject(withFile: storagePath)
let typedEncoder = fetchedEncoder as? T.Encoder
return typedEncoder?.value as T?
}

이제 캐시를 사용하기위해 한 인스턴스를 만들고 Coordinate 제네릭으로 만든다.
let cache = Cache<Coordinate>(name: "coordinateCache")
view raw UseCache.swift hosted with ❤ by GitHub

이렇게 하면 좌표 구조체를 쉽게 저장하고 검색할 수 있다.
cache.save(object: coordinate)
view raw UseCache2.swift hosted with ❤ by GitHub
이것을 가지면 NSCoding을 이용해 구조체를 인코딩할 수 있고, 단일 책임 원칙에 따렴, 타입 세이프티하게 만들어준다. 



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,