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 } 

ссылка на оригинал статьи http://habrahabr.ru/post/270063/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *