[Из песочницы] JSON в Swift 2.0 без анестезии

Работа с JSON — слишком привычное и ежедневное занятие, чтобы уделять ей много внимания. Тем не менее, реализация некоторых вещей в Swift выглядит слишком сложной и вызывает зубовную боль каждый раз, когда ее видишь.

Недавно, читая пост про SwiftyVK, нашел там ссылку на статью про OptJSON, позволяющую сильно упростить работу с JSON в Swift. И хотя подход, описанный в статье, действительно интересен, меня не покидало ощущение, что это все-равно слишком сложно.

Я попробовал еще немного упростить библиотеку OptJSON, и вот что получилось:

let obj = json?["workplan"]?["presets"]?[1]?["id"] as? Int


Исходный код библиотеки можно посмотреть по ссылке на GitHub:

Скачав единственный файл OptJSON.swift, я добавил его в пустой, только что созданный тестовый проект под Apple TV. Xcode ругнулся, что версия Swift в файле слишком старая и предложил обновить код. Я не стал возражать. По факту исправления коснулись лишь удаления хэш-символов (#).

Также я включил в проект JSON-конфиг, который использовал ранее в другом проекте и попробовал написать тестовый код для извлечения какого-нибудь значения «по-старинке»:

if let data = NSData(contentsOfFile: NSBundle.mainBundle().pathForResource("config", ofType: "json")!) {
  do {
    let obj = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? [String: AnyObject]
    let a = obj?["workplan"] as? [String: AnyObject]
    let b = a?["presets"] as? [AnyObject]
    print(b)
    let c = b?[1] as? [String: AnyObject]
    print(c)
    let d = c?["id"] as? Int
    print(d)
  } catch {
    print("Error!")
  }
}


Да, в Objective C действительно было в разы проще. Теперь попробуем использовать OptJSON для тех же целей:

Если длинно
if let data = NSData(contentsOfFile: NSBundle.mainBundle().pathForResource("config", ofType: "json")!) {
  do {
    let obj = try NSJSONSerialization.JSONObjectWithData(data, options: [])
     let v = JSON(obj)
     let a = v?[key:"workplan"]
     let b = a?[key:"presets"]
     print(b)
     let c = b?[index:1]
     print(c)
     let d = c?[key:"id"]
     print(d)
  } catch {
     print("Error!")
  }
}



Или если коротко:

if let data = NSData(contentsOfFile: NSBundle.mainBundle().pathForResource("config", ofType: "json")!) {
  do {
    let obj = try NSJSONSerialization.JSONObjectWithData(data, options: [])
    let d = JSON(obj)?[key:"workplan"]?[key:"presets"]?[index:1]?[key:"id"]
    print(d)
  } catch {
    print("Error!")
  }
}


Уже неплохо! Но все равно, не покидает ощущение, что можно все сделать проще. И ведь можно! Я полез в OptJSON.swift и, первым делом, меня удивила конструкция:

public func JSON(object: AnyObject?) -> JSONValue? {
  if let some: AnyObject = object {
    switch some {
     case let null as NSNull:        return null
     case let number as NSNumber:    return number
     case let string as NSString:    return string
     case let array as NSArray:      return array
     case let dict as NSDictionary:  return dict
     default:                        return nil
    }
  } else {
     return nil
  }
}


Не долго думая, я ее заменил на

public func JSONValue(object: AnyObject?) -> JSONValue? {
  if let some = object as? JSONValue {
    return some
  } else {
    return nil
  }
}


А если нет разницы, зачем платить больше? Следующим шагом было убрать именованные параметры, так сильно бесящие при вызове subscript:

[key:"presets"]?[index:0]?

Сказано — сделано! Xcode ругнулся, что ему не нравится возвращаемый тип JSONValue? вместо ожидаемого subscript-ом AnyObject?.. Чтож, попутно сносим обертку возвращаемых значений в JSON(). Код обрел примерно следующий вид:

extension NSArray : JSONValue {
  public subscript( key: String) -> JSONValue? { return nil }
  public subscript( index: Int) -> JSONValue? { return index < count && index >= 0 ? self[index] : nil }
}

extension NSDictionary : JSONValue {
  public subscript( key: String) -> JSONValue? { return self[key] }
  public subscript( index: Int) -> JSONValue? { return nil }
}


Запустив проект, я понял, почему автор решил использовать именованные параметры, а именно, выполнение функции уходило в глубокую рекурсию, пытаясь вызвать self[key]. Но в конце концов, почему для расширения используются классы NSArray и NSDictionary, а для извлечения объекта — чуждый objectForKeyedSubscript?! Ведь есть же родные для этих классов методы objectForKey: и objectAtIndex:, используем их:

extension NSArray : JSONValue {
  public subscript( key: String) -> JSONValue? { return nil }
  public subscript( index: Int) -> JSONValue? { return index < count && index >= 0 ? self.objectAtIndex(index) as? JSONValue : nil }
}

extension NSDictionary : JSONValue {
  public subscript( key: String) -> JSONValue? { return self.objectForKey(key) as? JSONValue }
  public subscript( index: Int) -> JSONValue? { return nil }
}


А раз пошла такая пьянка, то функция JSON() для обертки объектов нам в принципе не нужна, хватит и простого as? JSONValue. Заменим ее внутренности для чего-нибудь более удобного, например загрузка JSON объекта из строки, NSData или из содержимого NSURL, а заодно избавимся от необходимости использования новомодного try-catch:

public func JSON(object: AnyObject?, options: NSJSONReadingOptions = []) -> JSONValue? {
  let data: NSData
  if let aData = object as? NSData {
    data = aData
  } else if let string = object as? String, aData = string.dataUsingEncoding(NSUTF8StringEncoding) {
    data = aData
  } else if let url = object as? NSURL, aData = NSData(contentsOfURL: url) {
    data = aData
  } else {
    return nil
  }
  if let json = try? NSJSONSerialization.JSONObjectWithData(data, options: options) {
    return json as? JSONValue
  }
  return nil
}


После этих преобразований, конечный код приобретает следующий вид:

if let v = JSON(NSBundle.mainBundle().URLForResource("config", withExtension: "json")) {
  let a = v["workplan"]
  let b = a?["presets"]
  print(b)
  let c = b?[1]
  print(c)
  let d = c?["id"]
  print(d)
}


А если короче, то и вовсе:

let json = JSON(NSBundle.mainBundle().URLForResource("config", withExtension: "json"))
let obj = json?["workplan"]?["presets"]?[1]?["id"] as? Int
print(obj)


И никаких именованных параметров! Спасибо за внимание.

Просмотреть полный код доработанного файла
import Foundation

public protocol JSONValue: AnyObject {
    subscript(key: String) -> JSONValue? { get }
    subscript(index: Int) -> JSONValue? { get }
}

extension NSNull : JSONValue {
    public subscript(key: String) -> JSONValue? { return nil }
    public subscript(index: Int) -> JSONValue? { return nil }
}

extension NSNumber : JSONValue {
    public subscript(key: String) -> JSONValue? { return nil }
    public subscript(index: Int) -> JSONValue? { return nil }
}

extension NSString : JSONValue {
    public subscript( key: String) -> JSONValue? { return nil }
    public subscript( index: Int) -> JSONValue? { return nil }
}

extension NSArray : JSONValue {
    public subscript( key: String) -> JSONValue? { return nil }
    public subscript( index: Int) -> JSONValue? { return index < count && index >= 0 ? self.objectAtIndex(index) as? JSONValue : nil }
}

extension NSDictionary : JSONValue {
    public subscript( key: String) -> JSONValue? { return self.objectForKey(key) as? JSONValue }
    public subscript( index: Int) -> JSONValue? { return nil }
}


public func JSON(object: AnyObject?, options: NSJSONReadingOptions = []) -> JSONValue? {
    let data: NSData
    if let aData = object as? NSData {
        data = aData
    } else if let string = object as? String, aData = string.dataUsingEncoding(NSUTF8StringEncoding) {
        data = aData
    } else if let url = object as? NSURL, aData = NSData(contentsOfURL: url) {
        data = aData
    } else {
        return nil
    }
    if let json = try? NSJSONSerialization.JSONObjectWithData(data, options: options) {
        return json as? JSONValue
    }
    return nil
}


© Habrahabr.ru