JavaScript Object Notation, или сокращенно JSON является самым распространенным способом связи с сервером и получения информации с него. Он чрезвычайно популярен из-за простоты в использовании и восприятии.

Рассмотрим следующий фрагмент JSON:
[
  {
    "person": {
      "name": "Dani",
      "age": "24"
    }
  },
  {
    "person": {
      "name": "ray",
      "age": "70"
    }
  }
]


В Objective-C, парсинг и десериализация JSON достаточно простая:
NSArray *json = [NSJSONSerialization JSONObjectWithData:JSONData options:kNilOptions error:nil];
NSString *age = json[0][@"person"][@"age"];
NSLog(@"Dani's age is %@", age);


В Swift, это более сложный процесс из-за опциональных (optionals) типов и типобезопасности:
var json: Array!
do {
  json = try NSJSONSerialization.JSONObjectWithData(JSONData, options: NSJSONReadingOptions()) as? Array
} catch {
  print(error)
}
 
if let item = json[0] as? [String: AnyObject] {
  if let person = item["person"] as? [String: AnyObject] {
    if let age = person["age"] as? Int {
      print("Dani's age is \(age)")
    }
  }
}


В представленном выше коде, необходимо проверять каждый объект до его использования optional binding. Это защитит ваш код; но чем сложнее JSON, тем более громоздким становится код.

В Swift 2.0, был введено оператор guard для того, чтобы избавиться от вложенных выражений if:
guard let item = json[0] as? [String: AnyObject],
  let person = item["person"] as? [String: AnyObject],
  let age = person["age"] as? Int else {
    return;
}
print("Dani's age is \(age)")

Все еще многословно? Но как это упростить?

В этой статье по работе с JSON используется простейший способ парсинга JSON – используя популярную общедоступную библиотеку Gloss.

Так, в частности вы будете использовать Gloss для парсинга и преобразования JSON, который вмещает в себе 25 популярных приложений в US App Store. Так же просто, как и в Objective-C!

С чего начать


Скачайте стартовый playground для это статьй.

Поскольку пользовательский интерфейс не нужен, будем работать исключительно с playground'oм.

Откройте Swift.playground в Xcode и изучите его.

Заметка: Вы можете заметить, что Project Navigator по умолчанию закрыт. Если это так, нажмите Command+1 чтобы вывести его на экран. У вас должно получится так как на изображению ниже.



Стартовый файл playground вмещает несколько исходных и ресурсных файлов, которые полностью сосредоточены на парсинге JSON. Обратите внимание на структуру playground'a:

  • Папка Resources содержит ресурсы, доступ к которым может быть получен через ваш Swift-код
  • topapps.json: Содержит строку для парсинга JSON.
  • Папка Sources содержит дополнительные исходные файлы, к которыми ваш код в playground имеет доступ. Добавление файлов поддержки .swift в эту папку должен быть с обдуман, это принесет простоту чтения вашего playground'a.
  • App.swift: Структура старого простого Swift представляет собой приложение. Ваша цель — разбор JSON на коллекции объектов.
  • DataManager.swift: Управляет извлечением данных из локальной сети или Интернета. Используйте методы этого файла для последующей загрузки JSON.


Как только вы разобрались в данном playground'e, продолжайте читать статью!

Первичный способ разбора JSON в Swift


Во-первых, начнем с нативного способа парсинга JSON в Swift – то есть не используя внешних библиотек. С помощью этого, вы оцените преимущества использования такой библиотеки как Gloss.

Заметка: Если вы уже изучили недостатки нативного способа парсинга JSON и хотите перейти к Gloss, пропустите следующий абзац.

Чтобы дать название приложению #1 в App Store, разберем предоставленный JSON файл.

Перед тем как начать работу со словарями, задайте дополнительное имя (alias) вверху playground'a:

typealias Payload = [String: AnyObject]

Добавьте код обратного вызова getTopAppsDataFromFileWithSuccess как показано ниже:
DataManager.getTopAppsDataFromFileWithSuccess { (data) -> Void in
 
  var json: Payload!
 
  // 1
  do {
    json = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions()) as? Payload
  } catch {
    print(error)
    XCPlaygroundPage.currentPage.finishExecution()
  }
 
  // 2
  guard let feed = json["feed"] as? Payload,
    let apps = feed["entry"] as? [AnyObject],
    let app = apps.first as? Payload
    else { XCPlaygroundPage.currentPage.finishExecution() }
 
  guard let container = app["im:name"] as? Payload,
    let name = container["label"] as? String
    else { XCPlaygroundPage.currentPage.finishExecution() }
 
  guard let id = app["id"] as? Payload,
    let link = id["label"] as? String
    else { XCPlaygroundPage.currentPage.finishExecution() }
 
  // 3
  let entry = App(name: name, link: link)
  print(entry)
 
  XCPlaygroundPage.currentPage.finishExecution()
}


Вот что происходит:
  1. Сначала вы десериализируете данные используя NSJSONSerialization.
  2. Необходимо проверить каждое значение индексов в объекте JSON, чтобы предупредить появление nil. Как только найдено допустимое значение, продолжайте искать следующие объекты. Пройдя все индексы, вы получите значения name и link с которыми предстоит работать. Учтите, что если хотя бы один элемент JSON окажется непредвиденным, имя приложение не будет выведено на экран. Это желательная обработать в этом случаи.
  3. Последний шаг – инициализация объекта App используя значения name и link и вывод их в консоль.

Сохраните и запустите playground; вы увидите следующее в консоли отладчика:

App(name: "Game of War - Fire Age", link: "https://itunes.apple.com/us/app/game-of-war-fire-age/id667728512?mt=8&uo=2")


Да — “Game of War – Fire Age” это приложение #1 в файле JSON.

Потребовалось довольно большое количество код для получения названия первого приложения – пора посмотреть, как с этим справится Gloss.

Знакомство с преобразованием объектов с JSON


Преобразование объектов (Object mapping) представляет собой технику превращения объектов c JSON в объекты Swift. После определения моделей (model objects) и правил отображения (mapping rules), Gloss проделывает сложную работу исполняя вместо вас парсинг.

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

Звучит неплохо, да? Посмотрим, как это работает!

Разбор JSON с помощью Gloss


Чтобы все выглядело безупречно, создайте новый playground Gloss.playground, затем скопируйте файл topapps.json в папку Resources и DataManager.swift и папку Sources.

Внедрение Gloss в проект


Довольно просто внедрить Gloss в ваш проект:
  1. Нажмите эту ссылку Gloss Repo Zip File и сохраните библиотеку в подходящее место.
  2. Распакуйте ее и перетащите папку Gloss-master/Gloss/Gloss в папку Sources вашего playground.


Ваш Project Navigator должен выглядеть следующим образом:
Project Navigator

Это всё! Теперь Gloss добавлен в ваш проект и вы можете начать разбор JSON без головной боли!

Заметка: Gloss можно установить так же через Cocoapods. Поскольку playground их пока не поддерживают, этот способ можно использовать только при работе с проектами.

Преображение JSON в объекты


Сначала выясните как ваш объект относится к вашему JSON документу.

Необходимо, чтобы объект соответствовала протоколу Decodeable, который может декодировать их с JSON. Чтобы сделать это, осуществите инициализацию init?(json: JSON) как сказано в протоколе.

Обратите внимание на структуру topapps.json и создайте модель данных.

TopApps

Модель TopApps представляет собой объект высшего уровня, который вмещает в себе одну пару ключ-значения (key-value)
{
  "feed": {
    ...
  }
}

Создайте новый файл с названием TopApps.swift и поместите его в папке Sources вашего playground'a; добавьте следующий код:
public struct TopApps: Decodable {
 
  // 1
  public let feed: Feed?
 
  // 2
  public init?(json: JSON) {
    feed = "feed" <~~ json
  }
}


  1. Определите параметры для модели. В этом случае, он будет один. Позже вы добавите объект Feed.
  2. Реализуя пользовательский инициализатор, убедитесь, что TopApps соответствует протоколу. Вы должно быть удивитесь, что такое <~~ ! Это Encode Operator, который определяется в файле Gloss'sOperators.swift. В принципе, это говорит о том, что Gloss перемещает значения, что принадлежат ключу feed и зашифровывает (encode) их. Feed это так же объект Decodable; поэтому Gloss передаст ответственность за шифрование.

Feed
Объект Feed очень похож на объект высшего уровня. У него есть две пары ключ-значения, но поскольку нам интересны только 25 популярных приложений, нет необходимости обрабатывать объект author.
{
  "author": {
    ...
  },
  "entry": [
    ...
  ]	
}


Создайте новый файл с названием Feed.swift в папке Sources вашего playground'a и опишите его следующим образом:
public struct Feed: Decodable {
 
  public let entries: [App]?
 
  public init?(json: JSON) {
    entries = "entry" <~~ json
  }
}


App
Самым последним объектом, который надо описать является объект App. Приложение представлено в виде такой схемы:
{
  "im:name": {
    "label": "Game of War - Fire Age"
  },
  "id": {
    "label": "https://itunes.apple.com/us/app/game-of-war-fire-age/id667728512?mt=8&uo=2",	
    ...
  },
  ...
}


Создайте новый файл с названием App.swift в папке Sources вашего playground'a и добавьте следующий код:
public struct App: Decodable {
 
  // 1
  public let name: String
  public let link: String
 
  public init?(json: JSON) {
    // 2
    guard let container: JSON = "im:name" <~~ json,
      let id: JSON = "id" <~~ json
      else { return nil }
 
    guard let name: String = "label" <~~ container,
      link: String = "label" <~~ id
      else { return nil }
 
    self.name = name
    self.link = link
  }
}


  1. И Feed и TopApp использовали опциональные свойства (optional properties). Свойство может быть определено как не опциональное (non-optional) только в том случае, если использованный JSON всегда будет содержать значения для заполнения их.
  2. Не обязательно создавать объект для каждой составляющей в JSON. Например, в этом случае нет смысла создавать модель для in:name и id. Во время работы с неопциональными и вложенными объектами, не забывайте проверять nil.


Теперь, когда ваши классы готовы, остается позволить Gloss делать свою работу!
Откройте файл playground'a и добавьте следующий код:
import UIKit
import XCPlayground
 
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
 
DataManager.getTopAppsDataFromFileWithSuccess { (data) -> Void in
  var json: [String: AnyObject]!
 
  // 1
  do {
    json = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions()) as? [String: AnyObject]
  } catch {
    print(error)
    XCPlaygroundPage.currentPage.finishExecution()
  }
 
  // 2
  guard let topApps = TopApps(json: json) else {
    print("Error initializing object")
    XCPlaygroundPage.currentPage.finishExecution()
  }
 
  // 3
  guard let firstItem = topApps.feed?.entries?.first else {
    print("No such item")
    XCPlaygroundPage.currentPage.finishExecution()
  }
 
  // 4
  print(firstItem)
 
  XCPlaygroundPage.currentPage.finishExecution()
}


  1. Сначала десериализируйте данные используя NSJSONSerialization. Мы уже делали это раньше.
  2. Инициализируйте экземпляр объекта TopApps с помощью данных с JSON через конструктор.
  3. С помощью первой входной подачи получите приложение #1
  4. Выводите объект app в консоль.


Серьезно — это — весь код, в котором мы нуждаетесь.

Сохраните и запустите ваш playground; вы снова успешно получили название приложения, но более изящным способом.
App(name: "Game of War - Fire Age", link: "https://itunes.apple.com/us/app/game-of-war-fire-age/id667728512?mt=8&uo=2")


Все это относится к разбору локальных данных. Но как на счет разбора данных с удаленного ресурса?

Получение JSON с сети


Настало время сделать этот проект более реальным. Обычно, вы получаете данные c удаленного ресурса, а не с локальных файлов. Можно легко получить рейтинги с App Store с помощью сетевого запроса.

Откройте DataManager.swift и найдете URL для получения лучших приложений:
let TopAppURL = "https://itunes.apple.com/us/rss/topgrossingipadapplications/limit=25/json"

Затем добавьте следующий метод к реализации DataManager:
public class func getTopAppsDataFromItunesWithSuccess(success: ((iTunesData: NSData!) -> Void)) {
  //1
  loadDataFromURL(NSURL(string: TopAppURL)!, completion:{(data, error) -> Void in
      //2
      if let data = data {
        //3
        success(iTunesData: data)
      }
  })
}


Код выше выглядит довольно знакомым; но вместо получения данных с локального файла, мы использовали NSURLSession, чтобы получить данные с iTunes. Вот что конкретно происходит:

  1. Сначала вы вызываете метод loadDataFromURL; он требует URL-адрес и функцию замыкания, которая передает объект NSData.
  2. Используя дополнительную привязку, удостоверяемся в существовании данных.
  3. В конечном счете, вы предаете данные успешному завершению, как делали раньше.


Откройте ваш playground и замените следующий код:
DataManager.getTopAppsDataFromFileWithSuccess { (data) -> Void in

на этот
DataManager.getTopAppsDataFromItunesWithSuccess { (data) -> Void in


Теперь вы получили настоящие данные с iTunes.

Сохраните и запустите ваш playground; вы увидите, что разбор информаций по-прежнему приводит к тому же конечному итогу:

App(name: "Game of War - Fire Age", link: "https://itunes.apple.com/us/app/game-of-war-fire-age/id667728512?mt=8&uo=2")


Значение выше может отличаться, поскольку популярные приложения App Store постоянно меняются.

Часто люди не заинтересованы лишь в ТОП приложениях App Store — они хотят видеть список всех ТОП приложений. Не нужно кодить, чтобы их получить. Достаточно добавить следующий фрагмент кода:
topApps.feed?.entries


Gloss – скелеты в шкафу


Не сложно заметить, что Gloss чудесно выполняет работу парсинга – но что за этим стоит? <~~ это пользовательский оператор (custom operator) для ряда функций Decoder.decode. Gloss имеет встроенную поддержку для декодирования многих типов:
  • Простые типы (Decoder.decode)
  • Модели Decodable(Decoder.decodeDecodable)
  • Простые массивы (Decoder.decode)
  • Массивы и модели Decodable (Decoder.decodeDecodableArray)
  • Enum (Decoder.decodeEnum)
  • Массивы Enum (Decoder.decodeEnumArray)
  • NSURL (Decoder.decodeURL)
  • Массивы NSURL (Decode.decodeURLArray)


Заметка: Если у вас есть желание больше изучить Custom Operators, посмотрите статью: Operator Overloading in Swift Tutorial

В этой статье мы рассматривали декодируемые модели. Если вам необходимо что-то посложнее, расширьте Decoder и произведите собственную реализацию декодирования.

Конечно, с помощью Gloss можно так же конвертировать объекты в JSON. Если вам это интересно, просмотрите Encodable протокол.

Что дальше?


Вот и финальный playground.

Вы можете использовать playground как начальный шаг для создания нового приложения; просто замените URL получения данных удаленно с вашим собственным URL, управляйте собственными ключами и индексами вашего нового JSON, и вы создадите новый проект, который буде разбирать что-то, например, результатов футбольных матчей или другие данные полученные с сети.

Разработка Gloss продолжается тут на Github, так что следите за последними обновлениями.

Swift все еще эволюционирует, поэтому вы должны следить за новой документацией от Apple на случай будущих обновлений языка.

Надеюсь вам понравилась эта статья о работе с JSON. Если у вас есть вопросы или замечания, их можно обсудить ниже!

Комментарии (11)


  1. Shannon
    31.01.2016 09:53
    +4

    В Objective-C, парсинг и десериализация JSON достаточно простая:
    NSArray *json = [NSJSONSerialization JSONObjectWithData:JSONData options:kNilOptions error:nil];
    NSString *age = json[0][@"person"][@"age"];
    

    Используя swift, такая же простая:
    let jsonData = "{\"workplan\":{\"presets\":[{\"id\":0}, {\"id\":1}, {\"id\":2}]}}".dataUsingEncoding(NSUTF8StringEncoding)
    let jsoid = JSON(jsonData)
    
    let id = json?["workplan"]?["presets"]?[2]?["id"] as? Int
    print(id) // 2
    


  1. RedRover
    31.01.2016 10:35
    +2

    var json: [String: AnyObject]!
    <...>
    json = try NSJSONSerialization.JSONObj...
    

    Логичная, но медленная конструкция. Лучше использовать:

    var json: NSDictonary!
    

    А еще когда открываешь чужой код и видишь там что-то типо <~~ вместо вызова функции с именем дающим хоть какой-то намек на то что она делает испытываешь смешанные чувства.


  1. norlin
    31.01.2016 12:06

    А возможен ли вариант прямого преобразования json -> объект определённого класса / экземпляр struct? Конечно, при совпадении полей.


    1. yarmolchuk
      31.01.2016 13:02
      +1

      На сколько я понимаю то нет.



  1. kostyl
    31.01.2016 16:14
    +2

    Раньше чтобы написать статью на +7 надо было постараться, сейчас всякое гавно можно написать, типа как использовать какую-то обычную библиотеку в каком-то ЯП. Птьфу…


    1. yarmolchuk
      31.01.2016 16:20

      Всем не угодить, для некоторых эта статья будет полезной и принесет больше пользы чем статья про Run Loop.


      1. kostyl
        31.01.2016 16:23
        +2

        Да миллионы этих описаний работы разных библиотек, зачем их тут писать?


        1. yarmolchuk
          31.01.2016 16:26

          Что по Вашему мнению тут нужно писать?


          1. kostyl
            31.01.2016 16:34

            Ну точно не manual и readme библиотек


            1. yarmolchuk
              31.01.2016 16:50
              -2

              ждем от Вас крутых статей.