Swift 5 (Xcode 11 Betas 5 и 6) — как записать в файл JSON?

Этот вопрос задавался довольно много раз за эти годы, но он снова изменился в Swift 5, особенно в последних двух бета-версиях.

Чтение файла JSON кажется довольно простым:

func readJSONFileData(_ fileName: String) -> Array<Dictionary<String, Any>> {

    var resultArr: Array<Dictionary<String, Any>> = []

    if let url = Bundle.main.url(forResource: "file", withExtension: "json") {

        if let data = try? Data(contentsOf: url) {

            print("Data raw: ", data)

            if let json = try? (JSONSerialization.jsonObject(with: data, options: []) as! NSArray) {

                print("JSON: ", json)

                if let arr = json as? Array<Any> {

                    print("Array: ", arr)

                    resultArr = arr.map { $0 as! Dictionary<String, Any> }

                }

            }

        }

    }

    return resultArr

}

Но писать невероятно сложно, и все предыдущие методы, найденные на этом сайте, потерпели неудачу в Swift 5 на Xcode 11 бета-версиях 5 и 6.

Как записать данные в файл JSON в Swift 5?

Я пробовал эти подходы:

Не было никаких ошибок, кроме предупреждений об устаревании, и когда я их исправил, это просто не работало.


person Jack Bashford    schedule 26.08.2019    source источник
comment
Он не изменился. Оба процесса с JSONSerialization и JSONEncoder такие же, как всегда. Вместо того, чтобы показывать нам несвязанный код для «чтения», который отлично работает, почему бы вам не показать нам код для «записи», который у вас не работает? Или поделитесь ссылкой или двумя на «все предыдущие методы, найденные на этом сайте, не увенчались успехом».   -  person Rob    schedule 27.08.2019
comment
Излишне говорить, что если вы собираетесь использовать методы, которые выдают ошибки, используйте try, а не try?, заверните его в do-catch и напечатайте error в catch. Система выдачи ошибок точно сообщит вам, что происходит не так. Возможно, вы пытаетесь сохранить в пакет (который доступен только для чтения). Возможно, объект, который вы пытаетесь преобразовать, имеет некоторые свойства/значения, которые не могут быть представлены в формате JSON. Невозможно сказать без воспроизводимого примера. Но если вы не ловите ошибки, вы летите вслепую.   -  person Rob    schedule 27.08.2019
comment
Я бы посоветовал проверить Codable для его достижения. Кроме того, это может быть полезно medium.com /@ахмадфайяс/.   -  person Ahmad F    schedule 27.08.2019
comment
@Rob Скажем так, у меня есть данные JSON - как мне записать их в file.json? Он находится в каталоге моего проекта — доступен ли он для записи? А если нет, то как мне сделать его доступным для записи?   -  person Jack Bashford    schedule 27.08.2019
comment
Скажем так, у меня есть некоторые данные JSON — как мне записать их в file.json? В чем смысл некоторого JSON? Это словарь или обычный текст?   -  person Ahmad F    schedule 27.08.2019
comment
Отредактировано @Rob, лучше?   -  person Jack Bashford    schedule 27.08.2019
comment
@AhmadF в этом контексте представляет собой массив словарей со строковыми ключами и значениями.   -  person Jack Bashford    schedule 27.08.2019
comment
@Rob, не могли бы вы опубликовать код в этом комментарии? Это выглядело весьма полезным.   -  person Jack Bashford    schedule 27.08.2019
comment
@JackBashford Было бы лучше, если бы вы отредактировали свой вопрос, чтобы показать вашу попытку и объяснить, что происходит, а не просто ссылаться на другие вопросы/ответы.   -  person Paulw11    schedule 27.08.2019


Ответы (3)


Предположим на секунду, что у вас есть какая-то случайная коллекция (либо массивы, либо словари, либо какая-то их вложенная комбинация):

let dictionary: [String: Any] = ["bar": "qux", "baz": 42]

Затем вы можете сохранить его как JSON в каталоге «Поддержка приложений» следующим образом:

do {
    let fileURL = try FileManager.default
        .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
        .appendingPathComponent("example.json")

    try JSONSerialization.data(withJSONObject: dictionary)
        .write(to: fileURL)
} catch {
    print(error)
}

Обоснование того, почему мы теперь используем каталог «Поддержка приложений», а не папку «Документы», см. в Рекомендации по хранению данных в iOS или обратитесь к Руководство по программированию файловой системы. Но, несмотря ни на что, мы используем эти папки, а не папку «bundle» приложения, которая доступна только для чтения.

И чтобы прочитать этот файл JSON:

do {
    let fileURL = try FileManager.default
        .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
        .appendingPathComponent("example.json")

    let data = try Data(contentsOf: fileURL)
    let dictionary = try JSONSerialization.jsonObject(with: data)
    print(dictionary)
} catch {
    print(error)
}

При этом обычно мы предпочитаем использовать строго типизированные пользовательские типы, а не случайные словари, когда на программиста ложится бремя проверки отсутствия опечаток в именах ключей. В любом случае, мы делаем эти пользовательские типы struct или class совместимыми с Codable:

struct Foo: Codable {
    let bar: String
    let baz: Int
}

Тогда мы бы использовали JSONEncoder вместо старого JSONSerialization:

let foo = Foo(bar: "qux", baz: 42)
do {
    let fileURL = try FileManager.default
        .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
        .appendingPathComponent("example.json")

    try JSONEncoder().encode(foo)
        .write(to: fileURL)
} catch {
    print(error)
}

И чтобы прочитать этот файл JSON:

do {
    let fileURL = try FileManager.default
        .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
        .appendingPathComponent("example.json")

    let data = try Data(contentsOf: fileURL)
    let foo = try JSONDecoder().decode(Foo.self, from: data)
    print(foo)
} catch {
    print(error)
}

Дополнительные сведения о подготовке JSON из пользовательских типов см. в разделе Кодирование и декодирование пользовательских типов. статью или пример кода Использование JSON с пользовательскими типами.

person Rob    schedule 27.08.2019
comment
Итак, create: true создает файл, если он не существует, или...? - person Jack Bashford; 27.08.2019
comment
Вы используете create: true, если папка может не существовать. В случае каталога поддержки приложений он не создается автоматически, если вы не передадите true для параметра create. - person Rob; 27.08.2019
comment
И какой список/метод аргументов я бы использовал, если бы хотел, чтобы файл был создан, если он не существует? Извините, просто хочу подумать о возможных проблемах, которые я мог бы исправить. - person Jack Bashford; 27.08.2019
comment
Метод write уже создает для вас файл, если он еще не существует. - person Rob; 27.08.2019
comment
Круто, спасибо Роб! Я проверю это и вернусь к вам. - person Jack Bashford; 27.08.2019
comment
Привет Роб! Как я смогу прочитать этот example.json файл, в который пишет ваш код? Написание, похоже, работает, но я также хотел бы прочитать его, чтобы получить данные. - person Jack Bashford; 28.08.2019
comment
Это тот же процесс, но вы используете JSONDecoder().decode(_:from:) (или JSONSerialization.jsonObject(with:)). См. расширенный ответ выше. - person Rob; 28.08.2019
comment
Я могу написать JSON без проблем, но когда я снова запускаю функцию, которую написал, она просто заменяет единственную запись. Перед блоком do я добавляю массив и делаю try JSONEncoder().encode(array).write(to: fileURL), но безрезультатно. Любые идеи? - person fankibiber; 18.04.2020
comment
@fankibiber - Это пишет все array. Если полученный JSON содержит только новые записи, это означает, что закодированный вами array должен иметь только новые новые записи. Но это начинает выходить за рамки этого вопроса, поэтому я предлагаю вам подготовить воспроизводимый пример (MCVE) и напишите свой вопрос. Честно говоря, я подозреваю, что при подготовке MCVE вы, скорее всего, найдете источник проблемы, потому что просто невозможно, чтобы данные были в array, а в JSON отображались только новые записи. - person Rob; 18.04.2020
comment
@Роб, спасибо за ответ. Некоторое время назад я создал тему об этом: stackoverflow.com/questions/60745982/. К сожалению, я не могу добраться до корня проблемы, возможно, это что-то элементарное, но не повезло. :( - person fankibiber; 18.04.2020

В случае, если кто-то работает с пользовательскими объектами (как и я) и хочет более «универсальную» функцию с дженериками

Сохранить файл в диспетчере

func saveJSON<T: Codable>(named: String, object: T) {
    do {
        let fileURL = try FileManager.default
            .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
            .appendingPathComponent(named)
        let encoder = try JSONEncoder().encode(object)

        try encoder.write(to: fileURL)
    } catch {
        print("JSONSave error of \(error)")
    }
}

Читать файл из менеджера

func readJSON<T: Codable>(named: String, _ object: T.Type) -> T? {
     do {
         let fileURL = try FileManager.default
                .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
                .appendingPathComponent(named)
         let data = try Data(contentsOf: fileURL)

         let object = try JSONDecoder().decode(T.self, from: data)

        return object
    } catch {
        return nil
    }
}
person Misha Stone    schedule 31.12.2019

Отвечая на вопрос:

Скажем так, у меня есть некоторые данные JSON — как мне записать их в file.json? Он находится в каталоге моего проекта — доступен ли он для записи? А если нет, то как мне сделать его доступным для записи?

Предполагая, что «некоторый JSON» означает массив словарей как [<String, String>], вот простой пример того, как вы могли бы это сделать:

Учтите, что у нас есть следующий массив, который нам нужно записать в виде JSON в файл:

let array = [["Greeting":"Hello", "id": "101", "fruit": "banana"],
             ["Greeting":"Hola", "id": "102", "fruit": "tomato"],
             ["Greeting":"Salam", "id": "103", "fruit": "Grape"]]

Первое, что нужно сделать, это преобразовать его в JSON (как экземпляр Data). Есть более чем один вариант сделать это, давайте использовать JSONSerialization один:

do {
    let data = try JSONSerialization.data(withJSONObject: array, options: [])
    if let dataString = String(data: data, encoding: .utf8) {
        print(dataString)
    }
} catch {
    print(error)
}

На этом этапе вы должны увидеть в консоли:

[{"Приветствие":"Здравствуйте","фрукты":"банан","id":"101"},{"фрукты":"помидор","Приветствие":"Hola","id":"102 "},{"фрукты":"Виноград","Приветствие":"Салам","id":"103"}]

это наши данные, отформатированные как действительный JSON.

Далее нам нужно записать его в файл. Для этого мы будем использовать FileManager как:

do {
    let data = try JSONSerialization.data(withJSONObject: array, options: [])
    if let documentDirectoryUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
        let fileUrl = documentDirectoryUrl.appendingPathComponent("MyFile.json")
        try data.write(to: fileUrl)
    }
} catch {
    print(error)
}

Обратите внимание, что файл должен находиться в каталоге документов; В приведенном выше примере его имя должно быть «MyFile.json».

person Ahmad F    schedule 27.08.2019
comment
Спасибо за ответ Ахмад. Один вопрос - что такое каталог документов? Можете ли вы предоставить структуру каталогов проекта Xcode, выделив структуру каталогов, пожалуйста? - person Jack Bashford; 27.08.2019