[Из песочницы] 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
}