Недавно, читая пост про 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
}
Комментарии (8)
WEStor
03.11.2015 22:49+1Я пытался работать с optJSON и другие решения пробовал, но все же перешел на SwiftyJSON. Как и писал в статье. Там и синтаксис очень удобный, и библиотека хорошо тестами покрыта, и разработка постоянно продолжается.)
Вы, конечно, постарались. Если проект будет развиваться, то возьму его на заметку)Shannon
03.11.2015 23:15SwiftyJSON более прожорлив к процессору (и расходу батареи), у меня в то время когда SwiftyJSON жрал 100% проца и интерфейс тормозил, OptJSON использовал всего 30%
На небольших данных это не так заметно, но всё же стоит иметь ввиду
Эта упрощенная версия OptJSON очень порадовала
WEStor
03.11.2015 22:55Можно еще сделать расширение протокола JSONValue чтобы возвращал разные там intValue, stringValue без as. Вообще в Swift 2 все намного проще и интрересней, чем было раньше. Тогда ограничений было намного боьльше. Возможно, поэтому автор и использовал некоторые костыльные решения. Я и в своем коде до сих пор их нахожу и исправляю на более нормальные варианты swift 2.
Shannon
03.11.2015 23:04Именно этого не хватало в optJSON, а точнее именно это и мешало, так что результат отличный
DenHeadless
05.11.2015 10:42-1Попробуйте SwiftyJSON — библиотека написана намного более качественно, и код парсинга сокращает в разы.
freeOne
Синтаксис страшненький на мой взгляд.
смотрелось бы не так вырвиглазно
mallexxx
Так тоже можно, более того, до этого у меня так и было сделано (правда в ObjC классе).
Но при этом, во-первых нужно создавать специальный объект для работы с Dictionary, во вторых не очень красива конструкция «presets.1» (можно представить ситуацию, когда неправильно сформированный JSON имеет строковый ключ «1» вместо извлечения по индексу).
Кроме того, в реальной жизни эта конструкция будет выглядеть немного страшнее: «workplan.presets.\(index).id». Ну и лучше-бы на мой взгляд по subscript извлекать, а не методом .get()
А если захочется все-таки вытащить Dictionary preset, а потом из него уже все мапить: json.get(«workplan.presets.1»)? Получается дальше уже не получится так красиво, либо еще раз его придется заворачивать во что-то, либо функцией Get возвращать сразу dict в обертке, но это уже слишком :)