데이터를 디바이스에 저장하지 않고 iCloud 에 저장하여 효율적으로 디스크를 관리할 수 있습니다
iCloud를 사용하면 단일 값들이나 문서들을 저장할 수 있습니다.
특징
Key-Value storage system: iCloud의 User Default 시스템입니다. User Default 와 같이 Key-Value 로 데이터를 설정하고 가져오지만 디바이스가 아닌 iCloud 에 데이터를 저장한다는 가장 큰 차이점이 있습니다.
앱의 preferences 나 상태 또는 유저의 각각의 기기들의 설정 등을 저장할 수 있으며 이제부터 iCloud 사용하는 방법을 다루겠습니다.
iCloud 추가하기
iCloud에 데이터를 저장하기 위해서 우선 Signing & Capabilities - Targets - +Capability - iCloud 를 추가해줍니다.
- iCloud를 추가하면 Key-value storage 를 사용할 수 있는 체크박스를 확인할 수 있으며 체크가 안돼있으면 체크해줍니다.
Services 옵션
- Key-Value Storage: 간단한 앱 설정이나 소량의 데이터를 저장하고 빠르게 동기화해야 할 때 사용. 용량 제한이 있음(앱당 1MB).
- iCloud Documents: 파일 기반의 데이터를 저장하고 사용자가 여러 장치에서 접근할 수 있도록 할 때 적합. iCloud Drive와 연결
- CloudKit: 구조화된 데이터베이스가 필요하고, 여러 사용자 간의 데이터 공유나 백엔드 기능이 필요한 앱에 적합.
Key-Value 를 사용해 iCloud에 데이터 저장하는 방법
Foundation은 NSUbiquitousKeyValueStore 클래스를 정의하고 있으며 해당 클래스를 통해 Key-Value 서비스를 제공합니다.
NSUbiquitousKeyValueStore 를 통해 제공하는 Key-Value 서비스를 통해 데이터를 저장할 수 있는 메서드가 있습니다.
- set(Value, forKey: String) - 이 메서드는 String, Bool, Data, Double. Int64, dictionaries, arrays ... 타입의 값을 가져옵니다
- bool(forKey: String) - 이 메서드는 Bool 타입의 값을 가져옵니다
- double(forKey: String) - 이 메서드는 Double 타입의 값을 가져옵니다
- longLong(forKey: String) - 이 메서드는 Int64 타입의 값을 가져옵니다
- string(forKey: String) - 이 메서드는 String 타입의 값을 가져옵니다
- array(forKey: String) - 이 메서드는 Array 타입의 값을 가져옵니다
- dictionary(forKey: String) - 이 메서드는 Dictionary 타입의 값을 가져옵니다
- data(forKey: String) - 이 메서드는 Data 타입의 값을 가져옵니다
- object(forKey: String) - 이 메서드는 객체를 가져옵니다
위에서 나온 메서드들로 데이터를 저장 및 불러오려면 NSUbiquitousKeyValueStore 객체를 초기화 시켜야 합니다.
NSUbiquitousKeyValueStore 객체는 iCloud 로부터 데이터를 클라우드에 저장하고 다운받는 것을 처리하지만 데이터를 실시간으로 동기화 시키지는 않습니다.
다시말해 아이패드에서 데이터가 변경되었을 때 아이폰에서 해당 변경된 데이터를 실시간으로 직접적으로 반영시킬 수 없습니다.
저장된 데이터 다른 기기에 반영하기
하지만 이러한 문제를 해결하기 위해서 클래스에서는 다음과 같은 notification을 제공하고 있습니다.
didChangeExternallyNotification - 이 notification은 Key-Valeu 저장소의 변화가 감지될 때 시스템으로부터 받는 알림입니다.
model 로부터 변경되는 정보를 관리하는 것이 가장 좋은 방법입니다. 그렇기 때문에 Observable 프로퍼티를 사용하여 로컬의 value를 저장하고 몇개의 메서드를 사용하여 iCloud 로부터 value를 저장하고 불러올 수 있습니다.
저장된 값 불러오기
다음은 클라우드 사용을 가능하게 해주는 @ObservationIgnored 프로퍼티로 지정된 NSUbiquitousKeyValueStore 객체입니다.
import SwiftUI
import Observation
@Observable class ApplicationData {
var control: Double = 0
@ObservationIgnored let storage: NSUbiquitousKeyValueStore
위에서 언급했듯이 데이터를 저장 및 불러오기 위해서는 아래처럼 NSUbiquitousKeyValueStore 를 초기화 시켜야 합니다.
- 초기화가 되었으면 storage.double(forKey: _) 처럼 iCloud 에 저장된 Key가 control 인 Double 타입 value 데이터를 가져옵니다.
- 만일 존재하지 않는 데이터라면 0이 반환됩니다.
init() {
storage = NSUbiquitousKeyValueStore()
control = storage.double(forKey: "control")
}
새로운 값 저장하기
다음은 새로운 값을 설정하는 메서드 구현입니다. 해당 새로운 값을 설정하기 위해서는 우선 Key 값이 control 로 설정된 value가 변경하려는 값과 동일한지 확인하고 동일하지 않다면 .set(value, forKey: _) 메서드를 사용하여 데이터를 새롭게 설정합니다.
마지막으로 synchronize() 메서드를 사용해야 시스템이 해당 변경 사항을 iCloud에 즉시 보내는 것을 보장합니다.
func valueChanged(value: Double) {
if control != storage.double(forKey: "control") {
storage.set(value, forKey: "control")
storage.synchronize()
}
}
값 불러오기
이 메서드는 for loop 를 위해 async 로 선언됩니다. 이것은 didChangeExternallyNotification 의 notification 을 기다립니다.
즉 다른 기기로부터 데이터가 변경되었을 때 notification은 시스템으로부터 post 되어 자동으로 iCloud 로부터 새로운 값을 가져와 프로퍼티를 업데이트를 가능하게 해줍니다.
func valueReceived() async {
let center = NotificationCenter.default
let name = NSUbiquitousKeyValueStore.didChangeExternallyNotification
for await notification in center.notifications(named: name, object: storage) {
if notification.name == name {
await MainActor.run {
control = storage.double(forKey: "control")
}
}
}
}
Key-Value Storage System 의 문제점
Key-Value storage system 은 작은 크기의 값을 저장하도록 설계되었습니다. 이는 설정과 같은 간단한 값들을 의미하며 유용하지만 저장하는데 있어 크기가 좀 있는 데이터를 저장할 수 없다는 문제점이 있습니다. (현재는 1 MB 보다 큰 데이터를 저장할 수 없습니다)
Key-Value Storage System 문제점 해결
이런 문제점을 해결하기 위해서 대체 가능한 방법은 iCloud의 document 옵션을 활성화를 하는 것입니다.
OS는 container 를 사용하여 iCloud에 파일들을 저장합니다. container 는 iCloud 에 저장될 파일들이 저장될 폴더 입니다
이 container 에서 파일이 추가, 수정, 삭제 될 때 시스템은 자동으로 변경 사항을 iCloud 에 전달하여 다른 기기들에도 변경 사항을 iCloud 통해 동기화 합니다.
그럼 이제 container 를 추가합니다.
- 주의할 점은 container 의 이름은 유일해야 하며 앱의 번들 ID를 사용하는 것이 가장 좋은 방법입니다.
iCloud container 는 다른 기기들로부터 서로 내용을 공유하기 때문에. Ubiquitous Container 라고 불립니다.
File Manager 클래스는 Ubiquitous Container 를 사용할 수 있는 프로퍼티들과 메서스들을 포함하고 있습니다.
container의 데이터는 기기에 저장되어 시스템에 의해 자동적으로 다른기기와 동기화되지만, iCloud 를 사용하여 File Manager 클래스가 해결할 수 없는 문제들을 해결할 수도 있으며 가장 중요한 것은 Coordination 입니다.
동기화가 안되는 문제를 해결
신뢰할 수 없는 네트워크 연결 때문에 iCloud는 서로 다른 버전의 같은 파일들을 찾을 수 있습니다. (기기간 동기화가 안되는 문제)
- 변경 사항은 iCloud 에 도달하기 전에 다른 기기에서 데이터를 사용하는 상황이 예로 있겠습니다.
iCloud 에 저장된 데이터를 사용할 때 업데이트가 안된 데이터와 업데이트가 된 데이터를 각각 다른 기기에서 사용하는 것은 치명적입니다.
이런 문제를 해결하기 위해서 애플은 Uidocument 클래스를 제공하고 있습니다.
UIDocument 클래스
Uidocument 는 iCloud 의 파일들을 관리하기 위해서 만들어진 클래스입니다.
이 클래스는 어떤 크기의 파일이던 수정 및 동기화 시킬 수 있으며 (progression reports, automatic thumbnail generation, undo manager 등등)와 같은 기기의 문서들을 쉽게 수정할 수 있는 툴도 제공하고 있습니다.
이 클래스는 직접적으로 코드로 작성하도록 만들어진 것이 아닌 앱의 데이터와 파일간 데이터를 저장시킬 수 있는 인터페이스 같은 존재입니다, 그렇기 때문에 이 클래스를 다루기 위해서는 subclass 를 만들어 메서드들을 override 해야 합니다.
UIDocument 객체 만들기
subclass 를 만든 이후에는 아래 initializer 를 사용하여 객체를 만들어야 합니다.
UIDocument(fileURL: URL) - 이 initializer 는 UIDocument 객체를 만듭니다. fileURL 매개변수는 iCloud 컨테이너에 저장된 파일의 위치를 나타냅니다.
UIDocument 메서드로 데이터 저장 및 가져오기
추가로 아래 메서드들은 파일에 데이터를 제공하거나 가져오기 위해 subclass 에서 반드시 override 해야합니다.
contents(forType: String) - 이 메서드는 UIDocument 객체가 데이터를 파일에 저장할 때 사용되는 메서드 입니다.
메서드는 반드시 document의 데이터와 함께 객체로 반환됩니다. forType 매개변수는 파일을 구분할 수 있도록 합니다.
load(fromContents: Any, ofType: String?) - 이 메서드는 UIDocument 객체가 파일로부터 데이터를 불러올 때 사용합니다.
fromContents 매개변수는 파일의 내용을 담고 있는 객체이며
ofType 매개변수는 파일의 타입을 지정하는 String 입니다. (기본값으로는 파일의 확장자로 인해 결정됩니다)
UIDocument Asynchronous 메서드로 파일 관리하기
객체들 만든 이후에는 클래스에서 제공되는 Asynchronous Method들을 통해 파일을 관리할 수 있습니다.
open() - 이 비동기적 메서드는 UIDocument 객체에 파일을 열어 내용을 불러오는 요청을 하며 성공 여부를 Bool 값으로 반환합니다.
save(to: URL, for: SaveOperation) - 이 비동기적 메서드는 UIDocument 데이터를 파일에 저장하도록 요청합니다.
for 매개변수는 enumeration 값으로 operation 의 타입을 나타냅니다.
for 매개변수에는 forCreating(파일을 최초로 생성)과 forOverwriting(파일의 현재 버전을 오버라이드)이 존재하며 성공 여부를 Bool 값을 반환합니다.
close(): 이 비동기적 메서드는 변경된 내용을저장하고 저장 성공 여부를 Bool 값으로 반환합니다.
Metadata Query
기기에 저장되지 않은 파일들도 존재할 수 있기에 파일들의 리스트를 가져오는 것이 불가능하므로, iCloud 에 저장된 파일들에 접근하는 것은 복잡합니다. 대신 우리가 할 수 있는 또 다른 방법은 파일과 관련된 정보를 가져오는 것이며 데이터를 메타데이터라고 합니다.
메타데이터는 파일의 이름, 생성 날짜 등 해당 파일과 관련된 모든 정보를 의미합니다. 파일의 메타데이터를 가져오기 위해 Foundation에서는 NSMetadataQuery 클래스를 정의합니다. 이 클래스는 필요한 속성과 메서드를 제공하여 정보를 검색하고 업데이트를 확인합니다.
다음은 우리가 사용하는 속성들이며, NSMetadataQuery 클래스에는 새 데이터가 이용 가능할 때 보고하는 알림도 포함되어 있습니다.
이 중 자주 사용되는 프로퍼티들은 다음과 같습니다.
predicate - 이 프로퍼티는 query를 위한 predicate를 설정하거나 반환합니다. NSPredicate? 타입입니다.
sortDescriptors - 이 프로퍼티는 query를 위한 정렬 descriptor들을 설정하거나 반환합니다. [NSSortDescriptor] 타입입니다.
searchScopes - query의 scope 를 나타내는 값을 설정하거나 가져옵니다.
- NSMetadataQueryUbiquitousDocumentsScope (iCloud container에 있는 문서 파일들을 찾습니다)
- NSMetadataQueryUbiquitousDataScope (iCloud container에 없는 문서 파일들을 찾습니다)
results - 이 프로퍼티는 query의 결과들을 배열로 담습니다. 기본값으로 [NSMetadataItem] 객체를 포함하고 있습니다.
resultCount - query 를 통해 생성된 결과의 갯수
result(at: Int) - 이 메서드는 query로부터 받은 배열 결과값 인덱스에 해당하는 NSMetadataItem 객체를 반환합니다.
start() - query 를 시작시킵니다.
stop() - query 를 중단시킵니다.
enableUpdates() - 이 메서드는 query 업데이트를 활성화 시킵니다.
disableUpdates() - 이 메서드는 query 업데이트를 비활성화 시킵니다.
새로운 데이터가 사용 가능할 때 알림을 보고하며 다음과 같이 호출되는 콜백 함수도 존재합니다.
NSMetadataQueryDidUpdate - 이 알림은 query의 결과가 변화할 때 post 됩니다.
NSMetadataQueryDidFinishGathering - 이 알림은 query가 모든 정보를 가져오는 것이 완료될 때 post 됩니다.
Predicate로 원하는 정보만 가져오기
파일을 검색할 때 사용하는 predicate는 Swift Data에서 사용한 것과 유사하지만, Foundation 클래스인 NSPredicate에서 생성됩니다. 이 클래스는 필터링하고자 하는 값의 조건과 비교를 지정하는 형식의 문자열을 입력받습니다. 다음은 초기화 방법입니다.
NSPredicate(format: String, argumentArray: [Any]?) - 이 initializer 란 format 매개변수로 NSPredicate 객체를 만듭니다. argumentArray 매개변수는 format 에서 자리 표시자로 사용되는 옵셔널 배열 값입니다.
let predicate = NSPredicate(format: "age >= %d AND name == %@", argumentArray: [30, "John"])
위처럼 predicate를 만들었으면 이제 원하는 데이터를 가져올 수 있겠습니다.
func startQuery() {
query = NSMetadataQuery()
query?.predicate = NSPredicate(format: "%K == %@", argumentArray: [NSMetadataItemFSNameKey, "example.txt"])
query?.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope] // iCloud Drive
let name = NSNotification.Name.NSMetadataQueryDidFinishGathering
for await notification in center.notifications(named: name, object: metaData) {
if notification.name == name {
await getFiles()
}
}
query?.start()
}
그리고 쿼리의 결과는 results 속성에 의해 NSMetadataItem 객체 배열의 형태로 반환됩니다.
if let results = query.results as? [NSMetadataItem] {
DispatchQueue.main.async {
self.items = results
}
}
단일 Document 데이터 저장하는 간단한 코드
먼저 파일의 메타데이터를 가져오기 위해 NSMetadataQuery 인스턴스를 만들어줍니다.
searchScopes 를 NSMetadataQueryUbiquitousDocumentsScope 로 사용하여 iCloud Container에 있는 문서를 찾습니다.
이후 NSMetadataQueryDidFinishGathering 를 설정하여 query가 모든 정보를 가져오는 것이 완료될 때 post 됩니다.
해당 알림은 NotificationCenter 를 사용하여 만약 Center로 부터 받은 알림과 동일하다면 모든 정보를 가져왔다는 것으로 간주하여 Query를 통해 가져온 파일을 확인합니다.
이후 NSMetadataQueryDidUpdate 를 설정하여 query의 결과가 변화할 때 post 됩니다.
해당 알림은 NotificationCenter 를 사용하여 만약 Center로 부터 받은 알림과 동일하다면 Query 를 통해 받은 데이터가 변경되었다는 것으로 간주하여 Query를 통해 가져온 파일을 업데이트 합니다.
@Observable class ApplicationData {
var listOfFiles: [FileInfo] = []
@ObservationIgnored var document: MyDocument!
@ObservationIgnored var metaData: NSMetadataQuery!
init() {
metaData = NSMetadataQuery()
metaData.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]
Task(priority: .high) {
let center = NotificationCenter.default
let name = NSNotification.Name.NSMetadataQueryDidFinishGathering
for await notification in center.notifications(named: name, object: metaData) {
if notification.name == name {
await createFile()
}
}
}
Task(priority: .high) {
let center = NotificationCenter.default
let name = NSNotification.Name.NSMetadataQueryDidUpdate
for await notification in center.notifications(named: name, object: metaData) {
if notification.name == name {
let wrapper = NotificationWrapper(value: notification)
await updateFiles(notification: wrapper)
}
}
}
metaData.start()
}
문서 추가하기
다음은 getFile 메서드로 NSMetadataQueryDidFinishGathering 로 인해 데이터가 불러왔을 때 실행되는 코드입니다.
만약 가져온 데이터가 존재한다면 가장 첫 번째 데이터를 document 에 담습니다.
forAttribute 에서 NSMetadataItemURLKey 를 통해 파일의 URL을 가져오고 해당 URL인 커스텀 Document에 저장합니다.
만약 존재하지 않는다면 새로운 문서를 생성합니다.
manager.url(forUbiquityContainerIdentifier: nil)는 iCloud 컨테이너의 URL을 가져오고 이 값이 nil이면 기본 iCloud 컨테이너에 접근합니다.
기본 iCloud 컨테이너에서 Documents 아래 myfile.dat 이라는 파일 경로를 추가하며 새로운 MyDocument 객체를 생성합니다.
MyDocument 객체는 URL과 빈 데이터를 포함하고 있으며 save 를 통해 이미 존재하면 Overwrite, 없으면 새로 Create 합니다.
- MyDocument 클래스는 UIDocument 의 SubClass 로 save 메서드를 사용하여 데이터를 저장할 수 있습니다
@MainActor
func createFile(name: String) async {
if metaData.resultCount > 0 {
let file = metaData.result(at: 0) as! NSMetadataItem
let fileURL = file.value(forAttribute: NSMetadataItemURLKey) as! URL
document = MYDocument(fileURL: fileURL)
} else {
let manager = FileManager.default
if let fileURL = manager.url(forUbiquityContainerIdentifier: nil) {
let docuementURL = fileURL.appendingPathComponent("Documents/ myfile.dat")
document = MyDocument(fileURL: documentURL)
document.fileContent = Data()
if manager.fileExists(atPath: documentURL.path) {
let _ = await document.save(to: documentURL, for: .forOverwriting)
} else {
let _ = await document.save(to: documentURL, for: .forCreating)
}
}
}
}
문서에 데이터 저장하기
문서에 데이터를 저장하는 방법은 다음과 같습니다.
UIDocument.save(to: URL, for: SaveOperation) 메서드를 사용하여 데이터를 파일에 저장하도록 요청합니다.
.forOverwriting 을 사용하여 파일의 현재 버전을 오버라이딩을 하며 성공 여부는 Bool로 받지만 따로 사용하지 않아 let _ 로 받습니다.
@MainActor
func saveDocument(url: URL, content: String) async {
if let data = content.data(using: .utf8) {
document.fileContent = data
let _ = await document.save(to: url, for: .forOverwriting)
}
}
문서 저장된 데이터 가져오기
다음은 문서에 저장된 데이터를 URL를 통해 접근합니다.
이후 UIDocument.open() 메서드를 사용하여 해당 위치의 데이터를 가져옵니다.
- 이 비동기적 메서드는 UIDocument 객체에 파일을 열어 내용을 불러오는 요청으로 성공 여부를 Bool 타입으로 반환합니다
만약 성공적으로 데이터를 가져오면 데이터를 String 타입으로 변환하여 반환합니다.
@MainActor
func openDocument(url: URL) async -> String {
document = MyDocument(fileURL: url)
let success = await document.open()
if success {
if let data = document.fileContent {
return String(data: data, encoding: .utf8) ?? ""
}
}
return ""
}
다중 Document 데이터 저장하는 간단한 코드
단일 Document 데이터를 저장하는 것 보다 실제로 많은 애플리케이션은 유저들이 더 많은 Document들을 관리하고 만들 수 있도록 합니다. 다중 Document 데이터를 저장하는 방법은 단일 Document 데이터를 저장하는 방법과 다르지 않습니다.
먼저 Query를 정의하고 NotificationCenter 를 통해 변경 또는 추가되는 데이터들을 모델에 업데이트 및 추가하여 UI에 반영합니다.
단일 Document 와 다른 점은 이번에는 각 컨테이너의 document에 대한 정보를 저장합니다.
데이터 가져오기
다음은 getFile 메서드로 NSMetadataQueryDidFinishGathering 로 인해 데이터가 불러왔을 때 실행되는 코드입니다.
만약 가져온 데이터의 개수가 1개 이상이라면 해당 데이터들을 files 상수로 담고 for 문을 돌립니다.
NSMetadataItemFSNameKey 를 통해 해당 NSMetadataItem의 이름을 가져와 contains 메서드로 중복되지 않는 데이터를 확인하면 해당 데이터를 추가해줍니다.
@MainActor
func getFiles() {
if metaData.resultCount > 0 {
let files = metaData.results as! [NSMetadataItem]
for item in files {
let fileName = item.value(forAttribute: NSMetadataItemFSNameKey) as! String
if !listOfFiles.contains(where: { $0.name == fileName }) {
let documentURL = item.value(forAttribute: NSMetadataItemURLKey) as! URL
listOfFiles.append(FileInfo(name: fileName, url: documentURL))
}
}
listOfFiles.sort(by: { $0.name < $1.name })
}
}
데이터 업데이트하기
다음은 업데이트하는 메서드입니다. NSMetadataQueryDidUpdate 로 알림받아 데이터를 업데이트하려면 우선 데이터가 엉키기 않게 disableUpdate() 메서드를 실행합니다.
NSMetadataQueryUpdateRemovedItemsKey를 통해 삭제된 값들을 for 문을 통해 기기에서도 삭제합니다.
NSMetadataQueryUpdateAddedItemsKey를 통해 추가된 값들을 for 문을 통해 기기에 업데이트를 시켜줍니다.
마지막으로 변경된 값 반영이 완료되었으면 enableUpdate() 메서드를 통해 업데이트를 다시 가능하게 해줍니다.
@MainActor
func updateFiles(notification: NotificationWrapper) {
metaData.disableUpdates()
let manager = FileManager.default
if let modifications = notification.value.userInfo {
if let removed = modifications[NSMetadataQueryUpdateRemovedItemsKey] as? [NSMetadataItem] {
for item in removed {
let name = item.value(forAttribute: NSMetadataItemFSNameKey) as! String
if let index = listOfFiles.firstIndex(where: { $0.name == name }) {
listOfFiles.remove(at: index)
}
}
}
if let added = modifications[NSMetadataQueryUpdateAddedItemsKey] as? [NSMetadataItem] {
for item in added {
let name = item.value(forAttribute: NSMetadataItemFSNameKey) as! String
if !listOfFiles.contains(where: { $0.name == name }) {
if let fileURL = manager.url(forUbiquityContainerIdentifier: nil) {
let documentURL = fileURL.appendingPathComponent("Documents/\(name)")
listOfFiles.append(FileInfo(name: name, url: documentURL))
}
}
}
listOfFiles.sort(by: { $0.name < $1.name })
}
}
metaData.enableUpdates()
}
'SwiftUI' 카테고리의 다른 글
WWDC20 Data Enssentials in SwiftUI (EnvironmentObject, ObservableObject, ObservedObject, StateObject, AppStorage, SceneStorage, 생명주기) (1) | 2024.10.03 |
---|---|
SwiftUI - MapKit 에 대한 모든 것 (0) | 2024.09.08 |
Swift - 대소문자 바꾸기 (아스키코드, map) 2가지 방법 (0) | 2024.08.23 |
SwiftUI - 버튼, 텍스트 터치 영역 확장시키기 (0) | 2024.08.18 |
SwiftUI - 커스텀 달력 만들기 (0) | 2024.08.17 |