아카이브를 사용하는 이유
간편성: 아카이브를 사용하면 Core Data 보다 느리지만 덜 복잡하고 객체를 간단하게 파일로 만들고 읽을 수 있습니다.
데이터 보존: 아카이브를 사용하여 객체의 현재 상태와 데이터를 영구적으로 저장할 수 있습니다. 이를 통해 앱이 종료되거나 재시작되더라도 사용자 데이터를 보존할 수 있습니다. 예를 들어, 사용자 설정, 게임 진행 상황, 또는 다른 중요한 데이터를 저장할 수 있습니다.
데이터 교환: 아카이브된 객체를 파일 또는 네트워크를 통해 다른 앱 또는 기기로 전송할 수 있습니다. 이를 통해 데이터를 공유하고 다른 플랫폼 또는 앱과 호환성을 유지할 수 있습니다.
상태 저장 및 복원: 앱이 사용자 상호작용 또는 화면 전환을 관리하는 경우, 현재 화면 또는 앱 상태를 아카이브하여 나중에 복원할 수 있습니다. 이는 사용자가 앱을 중지하고 나중에 다시 시작할 때 이전 상태를 복원하는 데 도움이 됩니다.
데이터 공유: 아카이브된 데이터를 공유함으로써 사용자 간에 데이터를 교환하고 공유하는 데 사용할 수 있습니다. 예를 들어, 앱 간 데이터 공유, 게임 세션 공유, 또는 클라우드 서비스와의 데이터 동기화에 활용할 수 있습니다.
앞서 설명하기 전에 알아야할 것
Object Grapth란?
객체 지향 프로그래밍에서는 간접적이든 직접적이든 참조를 통해 관계를 맺게 되는데 이 관계를 통해 형성된 그룹을 Object Graph라고 합니다. 그리고 Object Grapth의 전체 혹은 일부를 파일로 저장하거나 다른 프로세스로 보내서 재구성할 수 있도록 변환하는 작업을 Archiving이라고 합니다.
아카이브란
- NSKeyedArchiver는 디스크와 직접 상호 작용할 수 있는 기능을 제공합니다.
- encoding과 decoding을 가능하게 해주는 NSCoding 프로토콜을 사용합니다.
- NSCoding은 아카이빙과 배포를 위한 프로토콜로 아카이빙을 할 수 있게 객체 인스턴스를 Encoding 하거나 다시 객체로 Decoding 하기 위한 두 개의 메서드만 있습니다.
- func encode(with aCoder: NSCoder), required init?(coder aDecoder: NSCoder)
class Robot: NSCoding {
var name : String = ""
var nemesis : Robot
var model : Int
func encode(with Encoder: NSCoder) {
Encoder.encode(name, forKey: "name")
Encoder.encode(nemesis, forKey: "nemesis")
Encoder.encode(model, forKey: "model")
}
// 메시지를 받으면 인자로 전달된 NSCoder객체 안의 모든 프로퍼티들을 인코딩한다. 데이터 스트림은 키-값 쌍으로 구성되어 파일시스템에 저장된다.
required init?(coder Decoder: NSCoder) {
name = Decoder.decodeObject(forKey: "name") as! String
nemesis = Decoder.decodeObject(forKey: "nemesis") as! Robot?
model = Decoder.decodeInteger(forKey: “model")
}
}
- NSCoder 클래스: 메모리상에 있는 객체 인스턴스 변수를 다른 형태로 변환하기 위한 추상 클래스로 NSKeyedArchiver, NSKeyedUnarchiver, NSPortCoder 와 같은 하위 클래스 구현체를 사용합니다.
아카이빙(archiving), 인코딩(encoding), 디코딩(decoding)에 대해 살펴볼 것입니다.
프로페셔널한 앱은 값의 컬렉션과 사용자 정의 데이터 유형을 포함하는 더 정교한 모델을 필요로 합니다. 이를 위해 Foundation은 `NSCoder` 클래스를 제공합니다. 이 클래스는 값을 데이터 구조체로 인코딩하고 디코딩하여 저장하는 용도로 사용됩니다.
NSCoder 또는 객체는 객체 자체뿐만 아니라 연결 및 계층 구조를 보존하기 위해 사용됩니다. 예를 들어, 두 객체가 서로를 참조하는 속성을 가질 수 있는데, 이 경우 아카이빙을 사용하면 두 객체가 인코딩되어 저장되고 필요할 때 다시 디코딩되어 연결됩니다. 이를 객체 그래프라고 하는 구조를 형성합니다.
`NSCoder` 클래스는 객체의 값을 인코딩하고 디코딩하기 위한 모든 필요한 메서드를 제공하지만, 모든 작업은 `NSCoder`의 하위 클래스 인 `NSKeyedArchiver`와`NSKeyedUnarchiver`의 인스턴스에 의해 수행됩니다.
`NSKeyedArchiver` 클래스는 객체 그래프를 인코딩하고 데이터를 구조체나 파일에 저장하기 위해 다음과 같은 archivedData(withRootObject: Any, requiringSecureCoding: Bool)타입 메서드를 제공합니다.
`NSKeyedUnarchiver` 클래스는 데이터 구조체에서 객체 그래프를 디코딩하기 위해 다음과 같은 타입 메서드를 제공합니다. unarchivedObject(ofClass: Class, from: Data) 이러한 메서드를 구현하면 from 인자로 명시된 데이터를 decode하여 원본 Object Grapth를 반환받습니다. ofClass 인자는 decode할 데이터의 타입을 지정합니다.
다음 예제 코드에서 .onAppear에 FileManager를 이용해 설정한 경로에 quotes.dat 파일이 존재하는지 확인 후 만약 존재한다면 NSKeyedUnarchiver 클래스의 unarchivedObject 메서드를 사용하여 NSString 타입의 content(quotes.dat)의 값을 decode하여 result 변수에 넣습니다. 여기서 unarchivedObject 메서드를 사용할때는 반드시 NSSecureCoding 프로토콜 준수해야하므로 NSString으로 설정합니다.
struct ContentView: View {
@State private var myquote: String = "Undefined"
var body: some View {
VStack {
Text(myquote)
.padding()
Spacer()
}
.onAppear {
let manager = FileManager.default
let documents = manager.urls(for: .documentDirectory, in: .userDomainMask)
let docURL = documents.first!
let fileURL = docURL.appendingPathComponent("quotes.dat")
let filePath = fileURL.path
if manager.fileExists(atPath: filePath) {
if let content = manager.contents(atPath: filePath) {
if let result = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSString.self, from: content) as String? {
myquote = result
}
}
} else {
let quote = "Fiction is the truth inside the lie"
if let fileData = try? NSKeyedArchiver.archivedData(withRootObject: quote, requiringSecureCoding: false) {
manager.createFile(atPath: filePath, contents: fileData, attributes: nil)
}
}
}
}
}
만일 처음 앱을 실행하게 된다면 quotes.dat 는 존재하지 않는 파일이므로 "Fiction is the truth inside the lie"가 encoding 되어서 파일이 생성됩니다.
그리구 이후에 다시 앱을 접속하게 되면 quote.dat 파일이 존재하므로 파일에 있는 데이터를 NSString 타입으로 Decoding 하여 Text로 값을 띄웁니다.
NSKeyedArchiver 와 NSKeyedUnarchiver 클래스는 다음과 같은 프로퍼티 리스트와 사용됩니다 (NSNumber, NSString, NSDate, NSArray,NSDictionary,NSData, equivalents in Swift). 만약 list 타입인 데이터를 기록(archive)하고 싶다면 프로퍼티 리스트를 사용해야합니다. Foundation은 PropertyListEncoder와 PropertyListDecoder 클래스를 제공합니다. 이 두 클래스들은 다음과 같은 메서드들을 제공합니다 encode(Value), decode(Type, from: Data)
사용법은 다음과 같습니다. 먼저 데이터를 Codable 프로토콜을 준수하는 구조체로 정의합니다.
struct Book: Codable {
var title: String
var author: String
var year: Int
var cover: String?
}
만일 구조체가 아닌 Class의 인스턴스를 저장하고 싶을 떄는 Class를 아래와 같이 만듭니다.
class Book: NSCoding {
var title : String = ""
var author : String = ""
var year : Int
var cover: String?
func encode(with Encoder: NSCoder) {
Encoder.encode(title, forKey: "name")
Encoder.encode(author, forKey: "author")
Encoder.encode(year, forKey: "year")
Encoder.encode(cover, forKey: "cover")
}
// 메시지를 받으면 인자로 전달된 NSCoder객체 안의 모든 프로퍼티들을 인코딩한다. 데이터 스트림은 키-값 쌍으로 구성되어 파일시스템에 저장된다.
required init?(coder Decoder: NSCoder) {
title = Decoder.decodeObject(forKey: "title") as! String
author = Decoder.decodeObject(forKey: "author") as! String
year = Decoder.decodeInteger(forKey: "year")
cover = Decoder.decodeObject(forKey: "cover") as! String
}
}
이후 만일 경로에 파일이 존재한다면 Book 타입으로 가져온 데이터를 decode 하고 decode 된 데이터를 bookInFile에 저장합니다.
init() {
manager = FileManager.default
let documents = manager.urls(for: .documentDirectory, in: .userDomainMask)
docURL = documents.first!
bookInFile = BookViewModel(book: Book(title: "", author: "", year: 0, cover: nil))
let fileURL = docURL.appendingPathComponent("userdata.dat")
let path = fileURL.path
if manager.fileExists(atPath: path) {
if let content = manager.contents(atPath: path) {
let decoder = PropertyListDecoder()
if let book = try? decoder.decode(Book.self, from: content) {
bookInFile.book = book
}
}
}
}
다음은 데이터를 기록(archive)하는 작업입니다. book 데이터를 encode하여 지정한 경로에 저장과 커버를 저장하는 코드입니다.
func saveBook(book: Book) {
let fileURL = docURL.appendingPathComponent("userdata.dat")
let path = fileURL.path
let encoder = PropertyListEncoder()
if let data = try? encoder.encode(book) {
if manager.createFile(atPath: path, contents: data, attributes: nil) {
bookInFile = BookViewModel(book: book)
}
}
}
책의 이름이 list로 저장되어 있다면 다음처럼 설정하여 대응할 수 있습니다.
func storeCover() -> String? {
let placeholder = UIImage(named: "bookcover")
let imageName = "image-\(UUID()).dat"
if let imageData = placeholder?.pngData() {
let fileURL = docURL.appendingPathComponent(imageName)
let path = fileURL.path
if manager.createFile(atPath: path, contents: imageData, attributes: nil) {
return imageName
}
}
return nil
}
여러개의 데이터를 한번에 저장하기 위해서는 다음처럼 map을 사용할 수 있습니다. 대신 PropertyListEncoder() 클래스의 encode 메서드를 사용하여 list 타입인 list를 encode 하였습니다.
func saveModel() {
let list = userData.map({ value in
return value.book
})
let fileURL = docURL.appendingPathComponent("userdata.dat")
let path = fileURL.path
let encoder = PropertyListEncoder()
if let data = try? encoder.encode(list) {
manager.createFile(atPath: path, contents: data, attributes: nil)
}
}
다음은 코드 저장을 위한 전체 코드 입니다.
struct Book: Codable {
var title: String
var author: String
var year: Int
var cover: String?
}
struct BookViewModel: Identifiable {
let id: UUID = UUID()
var book: Book
var title: String {
return book.title.capitalized
}
var author: String {
return book.author.capitalized
}
var year: String {
return String(book.year)
}
var cover: UIImage {
if let imageName = book.cover {
let manager = FileManager.default
let docURL = manager.urls(for: .documentDirectory, in: .userDomainMask).first!
let imageURL = docURL.appendingPathComponent(imageName)
let path = imageURL.path
if let coverImage = UIImage(contentsOfFile: path) {
return coverImage
}
}
return UIImage(named: "nopicture")!
}
}
class ApplicationData: ObservableObject {
@Published var userData: [BookViewModel] = []
var manager: FileManager
var docURL: URL
init() {
manager = FileManager.default
let documents = manager.urls(for: .documentDirectory, in: .userDomainMask)
docURL = documents.first!
let fileURL = docURL.appendingPathComponent("userdata.dat")
let path = fileURL.path
if manager.fileExists(atPath: path) {
if let content = manager.contents(atPath: path) {
let decoder = PropertyListDecoder()
if let list = try? decoder.decode([Book].self, from: content) {
userData = list.map({ value in
return BookViewModel(book: value)
})
}
}
}
}
func saveBook(book: Book) {
userData.append(BookViewModel(book: book))
saveModel()
}
func saveModel() {
let list = userData.map({ value in
return value.book
})
let fileURL = docURL.appendingPathComponent("userdata.dat")
let path = fileURL.path
let encoder = PropertyListEncoder()
if let data = try? encoder.encode(list) {
manager.createFile(atPath: path, contents: data, attributes: nil)
}
}
func storeCover() -> String? {
let placeholder = UIImage(named: "bookcover")
let imageName = "image-\(UUID()).dat"
if let imageData = placeholder?.pngData() {
let fileURL = docURL.appendingPathComponent(imageName)
let path = fileURL.path
if manager.createFile(atPath: path, contents: imageData, attributes: nil) {
return imageName
}
}
return nil
}
}
이후 다음과 같은 UI를 사용하여 위에 만든 기능들을 활용할 수 있습니다.
struct ContentView: View {
@EnvironmentObject var appData: ApplicationData
@State private var openSheet: Bool = false
var body: some View {
NavigationStack {
List {
ForEach(appData.userData) { book in
RowBook(book: book)
}
}
.navigationBarTitle("Books")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
openSheet = true
}, label: {
Image(systemName: "plus")
})
}
}
.sheet(isPresented: $openSheet) {
InsertBookView()
}
}
}
}
struct RowBook: View {
let book: BookViewModel
var body: some View {
HStack(alignment: .top) {
Image(uiImage: book.cover)
.resizable()
.scaledToFit()
.frame(width: 80, height: 100)
.cornerRadius(10)
VStack(alignment: .leading, spacing: 2) {
Text(book.title)
.bold()
Text(book.author)
Text(book.year)
.font(.caption)
Spacer()
}.padding(.top, 5)
Spacer()
}.padding(.top, 10)
}
}
struct InsertBookView: View {
@EnvironmentObject var appData: ApplicationData
@Environment(\.dismiss) var dismiss
@State private var inputTitle: String = ""
@State private var inputAuthor: String = ""
@State private var inputYear: String = ""
var body: some View {
VStack {
HStack {
Text("Title:")
TextField("Insert Title", text: $inputTitle)
.textFieldStyle(.roundedBorder)
}
HStack {
Text("Author:")
TextField("Insert Author", text: $inputAuthor)
.textFieldStyle(.roundedBorder)
}
HStack {
Text("Year:")
TextField("Insert Year", text: $inputYear)
.textFieldStyle(.roundedBorder)
}
HStack {
Spacer()
Button("Save") {
let newTitle = inputTitle.trimmingCharacters(in: .whitespaces)
let newAuthor = inputAuthor.trimmingCharacters(in: .whitespaces)
let newYear = Int(inputYear)
if !newTitle.isEmpty && !newAuthor.isEmpty && newYear != nil {
let coverName = appData.storeCover()
appData.saveBook(book: Book(title: newTitle, author: newAuthor, year: newYear!, cover: coverName))
dismiss()
}
}
}
Spacer()
}.padding()
}
}
'SwiftUI' 카테고리의 다른 글
EnvironmentObject (0) | 2023.11.15 |
---|---|
NSCoding (0) | 2023.11.13 |
Bundle (0) | 2023.11.12 |
FileDocument (0) | 2023.11.12 |
Files and Directories - 파일 디렉터리 접근 (0) | 2023.11.11 |