[решено] cloudKit + локальное хранилище


#1

Во что нашла

Это случаем не та штука, которая дает возможность работать в приложении с локальной базой (словно с корДатой), а когда интернет появится - оно отправит в облако данные … ?


#2

Это тот же cloudKit ))) Если вы про Advanced Local Caching, то это просто локальный кэш (как, например, у Firebase), пока соединения нет. Не уверен, что данные сохранятся после выгрузки из памяти приложения, хотя могу ошибаться.

Для вас самый простой вариант по-моему такой:

У него есть 2 поля, которые заполняются и сохраняются в корДату - это сама дата + какой-то текст.

Ту, конечно, кордата не нужна. Храните данные в локальном файле типа ключ-значеие - это .plist.

Модель у вас видимо такая:

class YourClass {

        var date: Date
        var comment: String

    init(date: Date, comment: String) {
            self.date = date
            self.comment = comment
    }

}

Для записи в файл и чтения от туда нам понадобиться преобразовывать наши данные в NSDictionary и обратно.

Для этого (записи в файл) создадим в классе вычисляемой свойство, например, ** dictionary**, которое будет являться словарём NSDictionary и по ключам “date” и “comment” там будут храниться date и comment )). А для загрузки из файла нам понадобится дополнительный инициализатор, который будет принимать NSDictionary и возвращать наши date и comment
получится следущее:

class YourClass {

    var date: Date
    var comment: String

    var dictionary : NSDictionary {
        let dictionary = NSDictionary(objects: [date, comment], forKeys: ["date" as NSCopying, "comment" as NSCopying])
        return dictionary
    }

    init(date: Date, comment: String) {
        self.date = date
        self.comment = comment
    }

    convenience init(dictionary: NSDictionary) {
  
        var date: Date
        var comment: String
  
        if let dateInit = dictionary.object(forKey: "date") as? Date {date = dateInit} else {date = Date()}
        if let commentInit = dictionary.object(forKey: "comment") as? String {comment = commentInit} else {comment = ""}

        self.init(date: date, comment: comment)
    } 
}

Здесь нужно обратить внимание на второй дополнительный инициализатор. Он специально сделан не как первый, в виде прямой непосредственной инициализации (или как это правильно называется я не знаю :slight_smile: )

init(dictionary: NSDictionary) {
    self.name = dictionary.object(forKey: "name") as! String
...

В нём сначала создана переменная, а потом мы извлекаем значение через привязку опционала (if let) к промежуточной переменной, и если смогли извлечь, то его и инициализируем, если нет, то присваиваем какое-то дефолтоне значение.
Зачем это сделано: такой тип инициализатора позволяет “извлекать” несуществующие данные, по этим ключам присваивать дефолтные значения и не упасть программе. В нашем случае это может случиться, если вы решите добавить потом новый параметр в класс, чтобы расширить функционал, при обновлении программы. А люди кто пользуется уже, у них уже есть сохранённые данные без этих параметров в файле. И при обычном инициализаторе, через прямое присвоение self.name = dictionary.object(forKey: "name") as! String, извлекая несуществующий ключ из словаря, всё упадёт и ничего не загрузится… Пользователь не обрадуется.

Модель есть.
Естественно у вас наверное экземляры класса храняться в массиве для удобного отображения в таблице, например

yourMassYourClass: [YourClass]

Так, начнём сохранять массив в файл, например в служебную дирректорию самого приложения. можно сделать через инстанс, но я просто объясню через вычисляемые глобальные переменные:

Так выглядит путь к папке:

var pathForSaveLibrary : String {
let path = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.libraryDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)[0]
return path

}

Добавим имя файла, например, data.plist:

var pathForSaveData : String {
let path = pathForSaveLibrary + "/data.plist"
return path

}

Теперь нам надо записать файл с нашим массивом на локальный диск устройства.

func saveData() {
var arrayYourMassYourClass = NSArray()
for item in yourMassYourClass {
    arrayYourMassYourClass = arrayYourMassYourClass.adding(item. dictionary) as NSArray
}
arrayYourMassYourClass.write(toFile: pathForSaveData, atomically: true)   

}

Тут должно быть всё понятно:

  • разбираем наш массив на элементы
  • каждый элемент (экземпляр нашего класса) преобразовываем через свойство ** dictionary** и добавляем к временному массиву типа NSArray
    Через простой метод .write записываем наш временный массив по нужному адресу на диске (pathForSaveData) - всё, наш массив с данными на диске в файле (и без всякой кордаты)

Загружаем похоже:

func loadData() {
    if let loadArray = NSArray(contentsOfFile: pathForSaveData) {
        yourMassYourClass = []
        var tempData : YourClass!
        for itemArray in loadArray {
            tempData = YourClass(dictionary: itemArray as! NSDictionary)
            yourMassYourClass.append(tempData)
        }
    } else {
        yourMassYourClass = []
    }
}

С помощью NSArray(contentsOfFile: ... через if let создаём массив NSArray из указаного файла. Если он там есть, то начинаем извлекать данные, используя дополнительный инициализатор нашего класса. Если же файла нет, то создаём пустой массив, какбуд-то при первом запуске ))

Всё, данные загружены из файла - ничего сложного.

  1. Ну и бэкап в icloud пользователя я уже описывал, но повторю здесь:

Первое, что нужно - это прописать нужные ключи в info.plist приложения. Они указаны здесь и выглядят так:

<key>NSUbiquitousContainers</key>
<dict>
<key>iCloud.com.example.MyApp</key>
<dict>
<key>NSUbiquitousContainerIsDocumentScopePublic</key>
<true/>
<key>NSUbiquitousContainerSupportedFolderLevels</key>
<string>Any</string>
<key>NSUbiquitousContainerName</key>
<string>MyApp</string>
</dict>
</dict>

копировать лучше из ссылки выше, чтобы не закралось каких-нибудь скрытых символов.
Важно: ключ iCloud.com.example.MyApp должен выглядеть как минимум так: iCloud.YOUR_BUNDLE_IDENTIFIER, а не iCloud.com.YOUR_BUNDLE_IDENTIFIER как могло показаться на первый взгляд. Хотя у меня вроде и даже в виде просто YOUR_BUNDLE_IDENTIFIER работало. В общем, имейте ввиду этот момент ))

Сохранение файла в icloud:

func saveToIcloud (localPath: String) throws {
    //is iCloud working?
    guard let iCloudDocumentsURL = FileManager.default.url(forUbiquityContainerIdentifier: "YourIdentifire")?.appendingPathComponent("yourFileName") else {print("iCloud is NOT working!"); return}
    print("create container successfully")
    //Create the Directory if it doesn't exist
    if !FileManager.default.fileExists(atPath: iCloudDocumentsURL.path, isDirectory: nil) {
        //This gets skipped after initial run saying directory exists, but still don't see it on iCloud
        do {
            try FileManager.default.createDirectory(at: iCloudDocumentsURL, withIntermediateDirectories: true, attributes: nil)
        } catch {
            print("error create dir to icloud - \(error.localizedDescription)")
            return
        }
    }
    //If file exists on iCloud remove it
    var isDir:ObjCBool = false
    if FileManager.default.fileExists(atPath: iCloudDocumentsURL.path, isDirectory: &isDir) {
        do {
            try FileManager.default.removeItem(at: iCloudDocumentsURL)
        } catch {
            print("error remove old file")
            return
        }
    }
    //copy from my local to iCloud
    do {
        try FileManager.default.copyItem(atPath: localPath, toPath: iCloudDocumentsURL.path)
    } catch {
        print("error copy local file to icloud")
        return
    }
    print("saving to icloud has been successfully")
}

Здесь сначала проверяется доступность айклауда впринципе (guard let iCloudDocumentsURL = ), путём создания адреса к файлу (пути сохранения в виде URL). При использовании параметра .url(forUbiquityContainerIdentifier:, вы обращаетесь к контейнеру приложения, который для каждого приложения свой, и который не виден пользователю в документах icloud, и виден только как занимающий место.
Если ссылка создалась, значит айклауд доступен, всё ок. Если нет, то return ))) Отсутсвие доступа может быть связано с отсутсвием инета или тем, что на устройстве просто не вошли в icloud.
Интересно, что во всех примерах ни кто из “учетелей” не указывают идентификатор контейнера FileManager.default.url(forUbiquityContainerIdentifier: “YourIdentifire”) и передают туда nil, хотя сама эпл здесь пишет следющее про этот метод: “Do not pass nil to that method because doing so returns the app’s default container, which is different for each app. Explicitly specifying the container identifier always yields the correct container directory.” - не передавайте nil, тк контейнер по-умолчанию отличается для каждого приложения…лучше указывайте явно (кто лучше знает английский - уточнит этот момент :wink: .)
Так что передаём туда идентификатор контейнера YourIdentifire (любое значение)

В .appendingPathComponent("yourFileName") пишем наше имя файла data.plist или любое другое, тк неважно что вы будете копировать - запишитеся именно с таким именем, как укажите.
Пути можно комбинировать так .appendingPathComponent("yourFolder").appendingPathComponent("yourFileName") но я пишу сразу файл без дополнительных папок - он же у нас один ))

Дальше проверяется наличие папки по этому пути (if !FileManager.default.fileExists(atPath: iCloudDocumentsURL.path), и если её нет, то создаём. Это для первого запуска.

Далее по аналогии проверяется наличие файла, и если он есть, то он удаляется, тк иначе будет ошибка копирования.

Далее просто копируем наш локальный файл data.plist (в localPath передаём наш pathForSaveData) через метод try FileManager.default.copyItem(atPath: localPath, toPath: iCloudDocumentsURL.path)

ВСЁ. Файл скопирован в icloud пользователя.
Добавьте функцию saveToIcloud в конец нашей saveData и при каждом создании файла на диск, будет улетать копия в айклауд.

Загрузка. То же самое (передаём путь сохранения локального файла):

  • проверяем есть ли доступ к айклауд
  • проверяем есть ли вообще файл в айклоуд
  • копируем из айклауд резервный файл на локальный диск (перед копирование, проверяем, есть ли локальный файл. Если есть - удаляем)
func loadFromIcloud(localPath: String) throws {

guard let iCloudDocumentsURL = FileManager.default.url(forUbiquityContainerIdentifier: "YourIdentifire")?.appendingPathComponent("yourFileName") else { print("iCloud is NOT working!"); return }
print("create container successfully")

var isDir:ObjCBool = false

if FileManager.default.fileExists(atPath: iCloudDocumentsURL.path, isDirectory: nil) {
    
    if FileManager.default.fileExists(atPath: localPath, isDirectory: &isDir) {
        do {
            try FileManager.default.removeItem(atPath: localPath)
        } catch {
            print("error remove old local file: \(error.localizedDescription)")
            return
        }
    }
   
    do {
        try FileManager.default.copyItem(atPath: iCloudDocumentsURL.path, toPath: localPath)
    } catch {
        print("error copy files from icloud - \(error.localizedDescription)")
        return
    }
} else {
    print("No backup data")
}

}

Вот и ВСЁ )) Зачем вам корДата? ))
При обновлении приложения локальный файл не удаляется. Так что не стоит переживать за сохранность данных при выпуске обновлений. Бэкапим по сути только для переустановки приложения/новое устройство/копирование данных на другое наше устройство

З.Ы. Возможно где-то ошибся и что-то не так написал - опытные поправят, но вроде работает ))
З.З.Ы. Где там говорите платят за советы? :rofl::rofl::rofl::rofl:


Загрузка данных в программу, подскажите, как правильнее?
Просьба помочь новичку. TabBar + ViewController
#3

:rofl: админы говорят, платить будут )) сами не хотят работать

А если серьёзно, огромное спасибо, раз 5 прочитала, начало что-то доходить. Надо это всё переварить, потому что даже не смотря на то, что я разобралась с корДатой - вот это всё для меня темный лес и волки воют … буду разбираться.

ps. а в plist можно добавлять ещё значения? есть дата и текст, и ещё добавить тест?


#4

это в разы легче, чем корДата

Ну конечно. Вы просто добавляйте в свой класс новое свойсвто String и всё. Я же просто пример привёл.

Вот предыдущий пример с дополнительными “новыми” 4мя свойствами newItem

class YourClass {
var date: Date
var comment: String
var newItem1: String
var newItem2: Bool
var newItem3: Int
var newItem4: Double

var dictionary : NSDictionary {
    let dictionary = NSDictionary(objects: [date, comment, newItem1, newItem2, newItem3, newItem4], forKeys: ["date" as NSCopying, "comment" as NSCopying, "newItem1" as NSCopying, "newItem2" as NSCopying, "newItem3" as NSCopying, "newItem4" as NSCopying])
    return dictionary
}

init(date: Date, comment: String, newItem1: String, newItem2: Bool, newItem3: Int, newItem4: Double) {
    self.date = date
    self.comment = comment
    self.newItem1 = newItem1
    self.newItem2 = newItem2
    self.newItem3 = newItem3
    self.newItem4 = newItem4
}

convenience init(dictionary: NSDictionary) {
    var date: Date
    var comment: String
    var newItem1: String
    var newItem2: Bool
    var newItem3: Int
    var newItem4: Double
    
    if let dateInit = dictionary.object(forKey: "date") as? Date {date = dateInit} else {date = Date()}
    if let commentInit = dictionary.object(forKey: "comment") as? String {comment = commentInit} else {comment = ""}
    if let newItem1Init = dictionary.object(forKey: "newItem1") as? String {newItem1 = newItem1Init} else {newItem1 = ""}
    if let newItem2Init = dictionary.object(forKey: "newItem2") as? Bool {newItem2 = newItem2Init} else {newItem2 = false}
    if let newItem3Init = dictionary.object(forKey: "newItem3") as? Int {newItem3 = newItem3Init} else {newItem3 = 0}
    if let newItem4Init = dictionary.object(forKey: "newItem4") as? Double {newItem4 = newItem4Init} else {newItem4 = 0}
    
    self.init(date: date, comment: comment, newItem1: newItem1, newItem2: newItem2, newItem3: newItem3, newItem4: newItem4)
}

}


#5

https://github.com/drewmccormack/ensembles решает вопрос, есть версия 2 … платная с поддержкой. Но и первую можно настроить если посидеть. + есть рабочий пример)


#6

Сомнительно конечно, но вдруг кто из опытных сталкивался:

всё, что я выше описывал, работает отлично на свежих ОС. Но вот на 10ке вообще ни как… А именно: не создаётся контейнер, хотя код написан Эппл ещё для АйОс8 (или даже для 5й)…

let iCloudDocumentsURL = FileManager.default.url(forUbiquityContainerIdentifier: "YourIdentifire")?.appendingPathComponent("yourFileName")
Отлично работает на 12шке и 11й, но на 10ке выдаёт nil, хотя и вошёл в симуляторе (и на реальном устройстве) в iCloud…

Даже не надеюсь на помощь, но вдруг :wink:

PS напомню, что доступ осущетвляется через обычный FileManager.default.url - может кто помнит, в 10ке может другой синтаксис был - вот тогда понятно где проблема ))


#7

Вроде решил. Кому-то (многим точно пригодится):

  • создание ссылки на папку у меня выше в примере несовсем верно было логически, но в 11й ОС работало, а вот в 10ке есть свои сюрпризы:
  1. URL cоздавать надо сначала на корневую папку, а не файл!

let iCloudDocumentsURL = FileManager.default.url(forUbiquityContainerIdentifier: "YourIdentifire")?.appendingPathComponent("yourFileName") - неправильно (хотя в 11й работает :smile: )

Правильно:
let iCloudDocumentsURL = FileManager.default.url(forUbiquityContainerIdentifier: nil)?.appendingPathComponent("yourRootFolderName") и тут два момента:

а) forUbiquityContainerIdentifier - выше я сам писал, что Эппл здесь настоятельно не рекомендует передавать nil при создании контейнера и надо передавать идентификатор, но везде все во всех примерах фигачат nil, ну и я туда же - не знаю, повлияло ли это на работоспособность в 10ке )))

б) Далее создаём уже ссылки на конкретные адреса файлов с указанием расширений, куда будем копировать:

if iCloudDocumentsURL != nil {
        let urlFile = iCloudDocumentsURL.appendingPathComponent("yourFileName").appendingPathExtension("yourFileExtention")
        // далее исполняемый код копирования из примера выше с учётом этих замечаний
        }
  1. И вот интересный момент:
    ОС11 - при создании корневой папки с любым названием даётся доступ к контейнеру (в примере ранее по сути создавались папки с именами файлов, куда копировались уже файлы по отдельности в разные папки. И доступ был.).
    ОС10 - вроде как доступ даётся только при создании корневой папки с дефолтным названием "Documents". Это не точно, но я везде сделал "Documents" да и пофиг - заработало нормально )))

#8

Всё опять не так, если кому-то интересна данная тема, кроме меня :smile: Оставлю здесь, чтоб было…

FileManager.default.url(forUbiquityContainerIdentifier: nil)...
При передаче в идентификатор не nil, а любой другой идентификатор ОС11 даёт доступ, а ОС10 не даёт. Это точно )) Вот и хз зачем он нужен - передавайте nil и всё норм на всех версиях.

  1. Второе вытекает из первого: раз на доступ влияет только идентификатор, то что там с папками, и в частности с "Documents" - можно размещать файлы в любы папки, но:
  • файлы, размещённые в "Documents" будут видны пользователю в iCloud и он сможет с ними работать (изменять/удалять) через finder или приложение “файлы”;
  • фалы, размещённые в других любых папках, недоступны пользователю через обычные средства. Их можно удалить только все целиком через настройки iCloud, удаление сервисных данных приложения.