Swift. NSCoding Tutorial for iOS: How to Permanently Save App Data

NSCoding에 대해서 알아봅니다. Restoration Part: 1

Posted by MinJun Ju on Tuesday, October 16, 2018 Tags: Swift Raywenderlich NSCoding Restoration   26 minute read

NSCoding Tutorial for iOS: How to Permanently Save App Data의 필요한 부분을 의역했습니다.


Table of contents


NSCoding Tutorial for iOS: How to Permanently Save App Data

NSCoding 튜토리얼에서 앱의 데이터를 저장하고(save) 데이터 베이스에서 지속 시켜(persist)앱을 종료한 후에 해당 상태에서 앱을 다시 시작할수 있도록 하는 방법을 학습합니다.

이 튜토리얼은 Swift 4.2, iOS 12, Xcode10으로 업데이트 되었습니다.

iOS의 디스크에 데이터를 저장하는 방법은 많이 있습니다 - raw file, APIs, Property List, Serialization, Core Data, Realme같은 라이브러리, NSCoding 등이 있습니다.

파일과 폴더는 재미있게 동작합니다.

무거운 데이터를 사용하는 앱에서, Core Data, Realm은 종종 최고의 방법이 됩니다. 가벼운 데이터를 사용할때는 NScoding이 더 좋습니다. 왜냐하면 쉽게 체택하여 사용할수 있기 때문입니다.

Swift 4는 다른 가벼운 대안인 Codable을 제공합니다. NSCoding과 굉장히 비슷하지만 몇가지 차이점이 있습니다.

디스크에 데이터를 지속하기 위한 간단한 방법이 필요하다면 NSCoding 과 Codable은 좋은 선택입니다. 하지만, 둘다 복잡한 객체 그래프 생성과 쿼리하는것을 지원하지 않습니다. 이 기능들이 필요하다면, Core Data, Realm, 다른 데이터 베이스 솔루션을 사용해야 합니다.

NSCoding의 주된 단점은 Foundation에 의존해야 한다는 것입니다. Codable은 어떤 프레임워크도 의존하지 않지만 Swift에 모델을 작성해야합니다.

Codable은 또한 NSCoding이 제공하지 않는 다양한 기능도 제공합니다. 예를들어 모델을 JSON으로 쉽게 직렬화 하기위해 사용할수 있습니다.

많은 Foundation 과 UIKit 클레스는 NSCoding을 사용합니다. 왜냐하면 iOS 2.0 이후부터 사용되어 왔기 때문입니다. - 예, 10년! Codable은 Swift 4에서 추가되었고, 약 1년 전이었고 많은 Apple 클레스가 Objective-C로 작성되었기 때문에 곧 Codable을 지원하기 위해 업데이트되지 않을것 입니다.

앱에서 NSCoding 또는 Codable을 사용하는것에 상관없이 둘다 어떻게 동작하는지 이해하는것이 중요합니다. 이 튜토리얼에서 NSCoding에 대한 모든것을 배우게 됩니다.


Getting Started

Scary Creatures이라는 샘플 프로젝트를 통해서 작업하게 됩니다. 앱을 생물체의 사진을 저장하고 이것들이 얼마나 무서운지 평가할수 있습니다. 하지만 이 시점에서는 데이터가 데이터 베이스에 유지되지 않으므로 앱을 다시 시작하면 추가한 모든 생물체사진이 사라집니다. 결과적으로 아직 이 앱은 유용하지 않습니다.

이 튜토리얼에서 NScodingFileManger를 사용하여 각 생물체의 데이터를 저장하고 로드 합니다. 그 후 NSSecureCoding을 알게되고 앱에서 데이터 로딩을 증진시키기 위해 무엇을 할수 있는지 알게 됩니다.

여기에서 시작 프로젝트를 다운받고 시작 프로젝트를 엽니다.

작업 해야하는 주된 파일은 다음과같습니다.

  • ScaryCreatureData.swift:는 생물체의 간단한 이름 및 등급 데이터를 포함합니다.
  • ScaryCreatureDoc.swift:는 thumbnail 이미지, 큰 이미지를 데이터를 포함하고 있는 생물체의 완성된 정보를 포함합니다.
  • MasterViewController.swift:는 모든 저장된 생물체 목록을 화면에 보여줍니다.
  • DetailViewController.swift:는 선택된 생물체의 자세한 정보를 보여주고 이 생물체를 평가할수 있게합니다.

앱을 빌드하고 실행합니다. 앱이 어떻게 동작하는지 확인합니다.


Implementing NSCoding

NSCoding은 디스크에 지속시킬수 있는 데이터 버퍼로 데이터의 부호화(encode)와 해독(decode)을 지원하기 위한 데이터 클레스로 구현가능한 프로토콜 입니다.

실제로 NSCoding을 구현하는것은 매우 쉽습니다. NSCoding을 사용하는게 유용한 이유를 찾을수 있을 것입니다.

먼저 ScaryCreatureData.swift를 열고 NSCoding를 클레스 선언에 다음과 같이 추가합니다.

class ScaryCreatureData: NSObject, NSCoding

두개의 메소드가 클레스에 추가됩니다.

func encode(with aCoder: NSCoder) {
  //add code here
}

required convenience init?(coder aDecoder: NSCoder) {
  //add code here
  self.init(title: "", rating: 0)
}

NSCoding을 따르도록 클레스를 만들려면 두개의 요구된 메소드를 구현해야 합니다. 첫번째 메소드는 개체를 부호화(encode) 합니다. 두번째 메소드는 새로운 객체를 인스턴스화 하기위해 해독(decode) 합니다.

encode(with:)는 인코더(encoder) 이고 init(coder:)는 디코더(decoder) 입니다.

Note: convenience가 여기에 있는것은 NSCoding의 요구된 사항이 아닙니다. 이게 여기있는 이유는 designated initializer(init(title:rating:))을 초기화 이내에서 호출했기 때문입니다. 이것을 지우려고 한다면, Xcode는 에러를 발생시킬것입니다. 이것을 자동으로 고치기를 선택하면 editor는 같은 키워드를 다시 추가합니다.

두 메소드를 구현하기 전에, 클레스의 시작 부분에 다음과같이 enum을 추가하여 코드를 정리합니다.

enum Keys: String {
  case title = "Title"
  case rating = "Rating"
}

별것 아닌것처럼 보이지만, 작성하는 이유가 있습니다. Codable keys는 String data type를 사용합니다. 문자열은 컴파일러가 실수를 잡아내지 못하는 오타(misspell)를 만들기 쉽습니다. 열거형을 사용함으로써 컴파일러는 일관된 키 이름을 사용하고 Xcode는 완성된 입력 코드를 제공합니다.

이제 재미있는 부분을 준비 했습니다. encode(with:)에 다음 코드를 추가합니다.

aCoder.encode(title, forKey: Keys.title.rawValue)
aCoder.encode(rating, forKey: Keys.rating.rawValue)

encode(_:forkey:)는 첫번째 매개변수로 제공된 값을 기록하고 키에 묶습니다(bind). 제공된 값은 반드시 NSCoding 프로토콜을 따르는 타입 이어야 합니다.

init?(coder:)의 시작부분에 다음 코드를 추가합니다.

let title = aDecoder.decodeObject(forKey: Keys.title.rawValue) as! String
let rating = aDecoder.decodeFloat(forKey: Keys.rating.rawValue)

위의 코드는 부호화(decode)와 정확히 반대입니다 지정된 키에서 제공된 NSCoder 객체에서 값을 읽습니다. 두값을 저장한 이후 앱을 보통때 처럼 다시 시작할때 동일한 두 값을 읽으려고 합니다.

해독한(decode) 생물체 데이터를 실제로 구성 해야합니다. 다음 줄을

self.init(title: "", rating: 0)

다음과같이 변경합니다.

self.init(title: title, rating: rating)

이겁니다. 이 작은 몇줄의 코드로 ScaryCreatureDataNSCoding을 따릅니다.


Loading and Saving to Disk

저장된 생물체 데이터를 읽고 쓰기 위해 디스크에 접근하기 위한 코드를 추가해야합니다.

효율적으로 하기 위해 - 모든 데이터를 한꺼번에 로드하지 않습니다.


Adding the Initializer

ScaryCreatureDoc.swift를 열고 클레스의 끝에 다음 코드를 추가합니다.

var docPath: URL?
  
init(docPath: URL) {
  super.init()
  self.docPath = docPath    
}

docPathScaryCreatureData 정보가 위치한 디스크 위치를 저장합니다. 여기서 속임수는 객체의 초기화할때가 아니라 메모리에 처음 접근할때 정보를 로드하는것 입니다.

그러나 완전 새로운 생물체를 생성한다면, 이 path는 nil 입니다. 파일은 아직 생성되지 않았기 때문입니다. 새로운 생명체가 생성될때는 언제나 이것이 설정되는걸 보장하기 위해 Bookkeeping code를 작성합니다.


Adding Bookkeeping Code

ScaryCreatureDoc의 시작 부분에 다음 enum을 추가하세요.

enum Keys: String {
  case dataFile = "Data.plist"
  case thumbImageFile = "thumbImage.png"
  case fullImageFile = "fullImage.png"
}

다음으로 thumbImage의 getter을 다음과같이 교체합니다.

get {
  if _thumbImage != nil { return _thumbImage }
  if docPath == nil { return nil }

  let thumbImageURL = docPath!.appendingPathComponent(Keys.thumbImageFile.rawValue)
  guard let imageData = try? Data(contentsOf: thumbImageURL) else { return nil }
  _thumbImage = UIImage(data: imageData)
  return _thumbImage
}

다음과같이 fullImage의 getter를 다음과같이 교체합니다.

get {
  if _fullImage != nil { return _fullImage }
  if docPath == nil { return nil }
  
  let fullImageURL = docPath!.appendingPathComponent(Keys.fullImageFile.rawValue)
  guard let imageData = try? Data(contentsOf: fullImageURL) else { return nil }
  _fullImage = UIImage(data: imageData)
  return _fullImage
}

자체 폴더에서 각 생명체들을 저장하기 때문에 생명채 doc를 저장하기 위해 다음으로 가능한 폴더를 제공할 헬퍼 클레스를 생성해야합니다.

ScaryCreatureDatabase.swift라는 이름의 새로운 Swift 파일을 추가하고 다음 코드를 추가합니다.

class ScaryCreatureDatabase: NSObject {
  class func nextScaryCreatureDocPath() -> URL? {
    return nil
  }
}

잠시 후에 새로운 클레스로 더 많은것을 추가 할것입니다. 우선은 ScaryCreatureDoc.swift로 돌아가서 클레스의 끝에 다음을 추가합니다.

func createDataPath() throws {
  guard docPath == nil else { return }

  docPath = ScaryCreatureDatabase.nextScaryCreatureDocPath()
  try FileManager.default.createDirectory(at: docPath!,
                                          withIntermediateDirectories: true,
                                          attributes: nil)
}

createDataPath()는 이름에서 말하는 것과 정확히 일치합니다. 데이터베이스에서 가능한 path로 docPath 속성을 채우고, docPath가 nil일때 폴더를 생성합니다. 그렇지 않으면, 이미 올바르게 생성 되었음을 의미합니다.


Saving Data

ScaryCreateData를 디스크에 저장하기 위한 로직을 추가합니다. createDatePath()를 정의 이후에 다음 코드를 추가합니다.

func saveData() {
  // 1
  guard let data = data else { return }
    
  // 2
  do {
    try createDataPath()
  } catch {
    print("Couldn't create save folder. " + error.localizedDescription)
    return
  }
    
  // 3
  let dataURL = docPath!.appendingPathComponent(Keys.dataFile.rawValue)
    
  // 4
  let codedData = try! NSKeyedArchiver.archivedData(withRootObject: data, 
                                                    requiringSecureCoding: false)
    
  // 5
  do {
    try codedData.write(to: dataURL)
  } catch {
    print("Couldn't write to save file: " + error.localizedDescription)
  }
}

위의 코드가 각각 하는것들 입니다.

  1. 데이터가 있다고 보장합니다. 그렇지 않으면 저장할것이 없다고 판단하고 반환합니다.
  2. 생성된 폴더 내부에 데이터를 저장하기 위한 준비에서 createDataPath()를 호출합니다.
  3. 정보를 작성할 경로를 구축합니다.
  4. 이전에 NSCoding을 체택한 ScaryCreatureData인스턴스 데이터를 부호화(encode)합니다
  5. 세번째 단계에서 생성한 파일의 경로에 부호화된 데이터를 작성합니다.

init(title:rating:thumbImage:fullImage:)끝에 다음 코드를 추가합니다.

saveData()

이것은 새로운 인스턴스가 생성된 이후에 데이터를 저장할것은 보장합니다.

좋습니다. 이제 데이터 저장하는 부분을 처리했습니다. 앱은 여전히 이미지를 저장하지 않지만 나중에 추가할것입니다.


Loading Data

위에서 언급한것처럼, 객체를 초기화하는 순간이 아니라, 처음으로 메모리로 접근했을때 정보를 로드 하는게 좋습니다. 생명체의 목록이 많을때 앱의 로딩 시간을 개선시킬수 있습니다.

Note: ScaryCreatureDoc에 있는 모든 속성은 private getters와 setters 으로 접근되어 집니다. 시작 프로젝트 그 자체가 이것으로 이득을 얻진 못하지만, 다음단계를 더 쉽게 진행할수 있도록 이미 추가되어 있습니다.

ScaryCreatureDoc.swift를 열고 data를 위한 getter을 다음과같이 교체합니다.

get {
  // 1
  if _data != nil { return _data }
  
  // 2
  let dataURL = docPath!.appendingPathComponent(Keys.dataFile.rawValue)
  guard let codedData = try? Data(contentsOf: dataURL) else { return nil }
  
  // 3
  _data = try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(codedData) as?
      ScaryCreatureData
  
  return _data
}

saveData()를 호출하여 생성한 저장된 ScaryCreatureData를 로드하기 위해 필요한 모든것입니다.

  1. 데이터가 이미 메모리에 로드된다면, 단지 그것을 반환합니다.
  2. 그렇지 않다면, Data 타입으로서 저장된 파일의 컨텐츠를 읽습니다.
  3. 이전에 부호화된 ScaryCreatureData 객체의 콘텐츠를 꺼내오고(Unarchive) 이들을 사용합니다.

이제 디스크에서 데이터를 저장하고 로드할수 있습니다. 하지만 앱을 사용하기 전에 해야할것이 더 남아있습니다.


Deleting Data

앱은 생물체를 삭제할수 있어야합니다. 마 너무 무서워서 머무르고 있을수도..:](…)

saveData()의 정의 이후에 다음 코드를 추가합니다.

func deleteDoc() {
  if let docPath = docPath {
    do {
      try FileManager.default.removeItem(at: docPath)
    }catch {
      print("Error Deleting Folder. " + error.localizedDescription)
    }
  }
}

이 메소드는 생명체 데이터 내부에 있는 파일이 포함된 전체 폴더를 삭제합니다.


Completing ScaryCreatureDatabase

이전에 생성한 ScaryCreatureDatabase는 두가지 일을 합니다. 첫번째 새로운 생물체 폴더를 생성하기 위한 가능한 경로를 제공하기 위해 이미 빈 메소드를 작성했습니다. 두번째는 이전에 저장한 모든 생물체 데이터를 로드하기 위함입니다.

이 두가지 기능중 하나를 구현하기 전에 생물체(데이터 베이스가 실제로 있는곳)을 저장한곳을 반환하는 도우미 메소드가 필요합니다.

ScaryCreatureDatabase.swift를 열고 맨위에 다음 코드를 추가합니다.

static let privateDocsDir: URL = {
  // 1
  let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
  
  // 2
  let documentsDirectoryURL = paths.first!.appendingPathComponent("PrivateDocuments")
  
  // 3
  do {
    try FileManager.default.createDirectory(at: documentsDirectoryURL,
                                            withIntermediateDirectories: true,
                                            attributes: nil)
  } catch {
    print("Couldn't create directory")
  }
  return documentsDirectoryURL
}()

PrivateDocuments라는 이름의 연산된 데이터 베이스 폴더 경로 값을 저장하는 편리한 변수 입니다. 이것은 다음과같이 동작합니다.

  1. 모든 app의 표준 폴더인 Documents 폴더를 가져옵니다.
  2. 모든 내용이 저장된 데이터베이스 폴더를 가리키는 경로를 구축합니다.
  3. 폴더가 존재 하지 않다면 폴더를 생성 하고 경로를 반환합니다.

위에서 언급한 두개의 함수를 구현할 준비가 되었습니다. 저장된 docs의 에서 데이터베이스를 로딩하여 시작합니다. 클레스의 아래에 다음 코드를 추가합니다.

class func loadScaryCreatureDocs() -> [ScaryCreatureDoc] {
  // 1
  guard let files = try? FileManager.default.contentsOfDirectory(
    at: privateDocsDir,
    includingPropertiesForKeys: nil,
    options: .skipsHiddenFiles) else { return [] }
  
  return files
    .filter { $0.pathExtension == "scarycreature" } // 2
    .map { ScaryCreatureDoc(docPath: $0) } // 3
}

디스크에 저장된 모든 .scarycreature파일을 로드하고 ScaryCreatureDoc 아이템 배열을 반환합니다.

  1. 데이터베이스 폴더의 모든 컨텐츠를 가져옵니다.
  2. .scarycreature로 끝나는 아이템을 포함하는 리스트를 필터합니다.
  3. 필터된 목록에서 데이터베이스를 로드하고 반환합니다.

그런 다음 새 documents를 저장하기 위해 가능한 다음 경로를 반환합니다. nextScaryCreatureDocPath()을 다음과같이 교체합니다.

// 1
guard let files = try? FileManager.default.contentsOfDirectory(
  at: privateDocsDir,
  includingPropertiesForKeys: nil,
  options: .skipsHiddenFiles) else { return nil }

var maxNumber = 0

// 2
files.forEach {
  if $0.pathExtension == "scarycreature" {
    let fileName = $0.deletingPathExtension().lastPathComponent
    maxNumber = max(maxNumber, Int(fileName) ?? 0)
  }
}

// 3
return privateDocsDir.appendingPathComponent(
  "\(maxNumber + 1).scarycreature",
  isDirectory: true)

이전 메소드와 마찬가지로, 데이터베이스의 모든 컨텐츠를 가져와서 필터링 하고 privateDocDir에 추가하고 반환합니다.

디스크의 모든 항목을 추적하는 쉬운 방법은 숫자로 폴더의 이름을 지정하는 것입니다. 가장 높은 숫자로된 이름의 파일을 찾으면 다음 사용가능한 경로를 쉽게 제공할수 있습니다.

Note: 숫자를 사용하는것은 document 기반 데이터베이스 폴더를 추적하고 이름을 지정하기 위한 방법일뿐입니다. 기존의 항목을 새 항목으로 바꾸지 않도록 각 폴더마다 고유한 이름을 가지게 선택할수 있습니다.


Trying It Out!

앱을 실행하기전에 privateDocsDir로 정의 속성클레스의 끝 return이전에 다음 코드를 추가합니다.

print(documentsDirectoryURL.absoluteString)

이렇게하면 시뮬레이터에서 앱을 실행할때 docs를 포함하는 폴더 위치를 정확히 알수 있습니다.

이제 앱을 실행하세요. 콘솔에서 값을 복사하지만 file://부분은 건너 띄고 /Users로 시작하여 /PrivateDocuments로 끝납니다.

Finder앱을 열고 다음 경로로 이동합니다.

폴더를 열면 콘텐츠는 다음과같이 보여집니다.

여기에 표시되는 아이템은 시작 프로젝트에서 구현한 MasterViewController.loadCreatures()에 의해서 생성 되었습니다. 앱을 실행한 매 시간, 디스크에 더 많은 documents가 추가됩니다. 이것은 실제로 올바르지 않습니다. 앱이 로드할때 디스크에서 데이터베이스의 콘텐츠를 읽어오지 않았기 때문입니다. 잠시후에 이 문제를 해결할것이지만 먼저 몇가지 사항을 구현해야합니다.

유저가 테이블뷰에서 삭제를 시작하면, 데이터베이스에서 생물체 데이터를 지우는게 필요합니다. MasterViewController의 tableView(_:commit:forRowAt:)의 구현을 다음으로 교체합니다.

if editingStyle == .delete {
  let creatureToDelete = creatures.remove(at: indexPath.row)
  creatureToDelete.deleteDoc()
  tableView.deleteRows(at: [indexPath], with: .fade)
}

마지막 고민 사항: 추가와 삭제 함수를 마무리 했지만 편집은 어떻게 하나요? 걱정하지마세요. Delete를 구현할것만큼 간단합니다.

DetailViewController.swift를 열고 rateViewRatingDidChange(rateView:newRating:), titleFieldTextChanged(_:)의 끝에 다음 라인을 추가합니다.

detailItem?.saveData()

이것은 간단하게 ScaryCreatureDoc객체에게 유저 인터페이스에서 그 자체의 정보가 변경될때 변경된 값을 다시 저장하라고 이야기합니다.


Saving and Loading Images

생명체 앱을 위해 마지막으로 남은것은 이미지를 저장하고 로드하는것입니다. 이것들을 파일 자체 내부 목록에 저장하지 않았습니다. 저장된 다른 데이터 바로 옆에 이미지 파일을 저장하는것이 훨씬 쉬울것입니다.

ScaryCreatureDoc.swift에서 클레스의 끝에 다음 코드를 추가합니다.

func saveImages() {
  // 1
  if _fullImage == nil || _thumbImage == nil { return }
  
  // 2
  do {
    try createDataPath()
  } catch {
    print("Couldn't create save Folder. " + error.localizedDescription)
    return
  }
  
  // 3
  let thumbImageURL = docPath!.appendingPathComponent(Keys.thumbImageFile.rawValue)
  let fullImageURL = docPath!.appendingPathComponent(Keys.fullImageFile.rawValue)
  
  // 4
  let thumbImageData = _thumbImage!.pngData()
  let fullImageData = _fullImage!.pngData()
  
  // 5
  try! thumbImageData!.write(to: thumbImageURL)
  try! fullImageData!.write(to: fullImageURL)
}

이전 saveData() 에서 기록했던것과 비슷합니다.

  1. 저장된 이미지가 있다고 보장합니다. 그렇지 않으면 함수를 종료합니다.
  2. 필요하다면 데이터 경로를 생성합니다.
  3. 디스크의 각 파일로 경로를 구축합니다.
  4. 각 이미지를 디스크에 기록하기 위한 준비를 하기위해 그 자체 PNG 데이터 표현으로 변경합니다.
  5. 그들 각각의 경로의 디스크에 생성된 데이터를 기록합니다.

프로젝트에서 saveImages()를 호출할 두 지점이 있습니다.

첫번째는 init(title:rating:thumblImage:fullImage:)입니다. ScaryCreatureDoc.swift를 열고 초기화 끝에 saveData()직후에 다음 코드를 추가합니다.

saveImages()

두번째는 DetailViewController.swift 내부의 imagePickerController(_:didFinishPickingMediaWithInfo:) 입니다. detailItem에서 이미지를 업데이트하는 dispatch 클로저를 찾을수 있습니다. 클로저의 끝에 다음 코드를 추가합니다.

self.detailItem?.saveImages()

이제 생명체를 저장, 업데이트, 삭제 할수 있습니다.

지금 빌드하여 실행하고 무서운 생명체를 디스크에서 복원하면 이미지를 가진것과 그렇지 않은것을 볼수 있습니다.


Xcode 디버그 콘솔에 프린트된 경로를 사용하여 해당 PrivateDocuments폴더를 삭제하고 다시 빌드하고 실행합니다. 그러면 다시 원래의 생명체 이미지들을 볼수 있습니다.


생명체를 저장한 이후 앱을 종료하고 다시 실행하면 저장했던것을 아직 볼수 없습니다. MasterViewController.swift를 열고 loadCreature()의 구현을 다음과같이 변경합니다.

creatures = ScaryCreatureDatabase.loadScaryCreatureDocs()

이렇게하면 미리 채워진 목록을 사용하는 대신 디스크에서 생물체를 로드합니다.

다시 빌드하고 실행합니다. 제목과 평점을 변경해보세요. 기본 화면으로 돌아가면 변경 사항이 디스크에 저장됩니다.



Implementing NSSecureCoding

iOS 6에서 Apple은 NSCoding의 위에 구축한 새로운 것을 소개했습니다. 다음 라인과같은 변수에 저장하기 위해 아카이브에서 값을 디코드 하는것을 주목할것입니다.

let title = aDecoder.decodeObject(forKey: Keys.title.rawValue) as! String

값을 읽을때, 이미 메모리에서 로드됬고 그후 알고 있어야 하는 데이터 타입으로 변환합니다. 무언가 잘못되어 이전에 작성된 객체의 타입을 요구한 데이터 타입으로 변환할수 없는 경우 객체가 메모리에서 완전히 로드된다음 캐스팅 시도가 실패합니다.

속임수는 일련의 동작입니다. 모든곳에서 객체를 사용하지 않지만, 객체는 이미 메모리에 완전하게 로드된 다음 캐스트가 실패된 이후 릴리즈 됩니다.

NSSecureCoding은 데이터를 나중에 검증하는 대신 해독(decode)할때 검증합니다. 그리고 가장 좋은 부분은 구현하기가 쉽다는 것입니다.

먼저 ScaryCreatureData.swift에서 클래스 선언을 NSSecureCoding프로토콜로 구현하여 클레스 선언을 다음과같이 만듭니다.

class ScaryCreatureData: NSObject, NSCoding, NSSecureCoding

그후 클레스의 끝에 다음 코드를 추가합니다.

static var supportsSecureCoding: Bool {
  return true
}

이것은 NSSecureCoding을 준수하기 위한 모든것이지만, 아직 이점을 얻지는 않았습니다.

encode(with:)의 구현을 다음과같이 변경합니다.

aCoder.encode(title as NSString, forKey: Keys.title.rawValue)
aCoder.encode(NSNumber(value: rating), forKey: Keys.rating.rawValue)

init?(coder:)를 다음과같이 변경합니다.

let title = aDecoder.decodeObject(of: NSString.self, forKey: Keys.title.rawValue) 
  as String? ?? ""
let rating = aDecoder.decodeObject(of: NSNumber.self, forKey: Keys.rating.rawValue)
self.init(title: title, rating: rating?.floatValue ?? 0)

새로운 초기화 코드를 보면, decodeObject(of:forKey)decodeObject(forKey:)(첫번째 매개변수는 클레스 입니다)와 다른 것을 알수 있습니다.

유감스럽게 NSSecureCoding을 사용하려면 Objctive-C에서 문자열 및 부동 소수점을 사용해야합니다. 그 이유는 NSString, NSNumber이 사용되고 그후 값이 Swift의 String, Float로 변환되어 되돌아가기 때문입니다.

마지막 단계는 NSKeyedArchiver에게 secure coding을 사용하라고 지시하는것입니다. ScaryCreatureDoc.swift에서 saveData()의 다음 라인을

let codedData = try! NSKeyedArchiver.archivedData(withRootObject: data,                                               requiringSecureCoding: false)

다음과같이 변경합니다.

let codedData = try! NSKeyedArchiver.archivedData(withRootObject: data, requiringSecureCoding: true)

여기에서는 requiringSecureCoding 매개변수에 false 대신 true를 전달합니다. 이렇게하면 NSKeyedArchiver가 객체와 그 자손을 보관할때 NSSecureCoding을 적용하도록 지시합니다.

Note: 이전에 NSSecureCoding 없이 작성된 파일은 현재 호환되지 않습니다. 이전에 저장된 데이터를 삭제하거나 시뮬레이터에서 앱을 제거해야 합니다. 실제 시나리오에서는 이전 데이터를 마이그레이션 해야합니다.


Where to Go From Here?

NSKeyedArchiverNSKeyedUnarchiver클레스는 데이터를 쉽게 부호화(encode)와 해독(decode) 하여 디스크에 기록할수 있는 유일한 방법이 아닙니다. JSON과같은 다른 많은 형식이 있습니다.

Swift 모델을 JSON으로 직렬화하는 가장 쉬운 방법은 Codable을 준수하는 것입니다. 이것에 대한 튜토리얼은 여기입니다.