Al desarrollar aplicaciones móviles, de una forma u otra, nos enfrentamos a la necesidad de analizar los datos del servidor en modelos de aplicaciones internas. En la gran mayoría de los casos, estos datos vienen en formato JSON. Comenzando con Swift 4, la herramienta principal para analizar JSON es usar un protocolo Decodable
y un objeto JSONDecoder
.
Este enfoque simplificó enormemente el proceso de análisis de datos y redujo el número de código repetitivo. En la mayoría de los casos, es bastante simple crear modelos con propiedades nombradas como campos en el objeto JSON y JSONDecoder
hará el resto por usted. Código mínimo, beneficio máximo. Sin embargo, este enfoque tiene un inconveniente, a saber, la lealtad del analizador extremadamente baja. Lo explicaré. Si hay alguna discrepancia entre el modelo de datos interno (objetos decodificables) y lo que vino en JSON,JSONDecoder
arroja un error y perdemos todo el objeto. Quizás, en algunas situaciones, este modelo de comportamiento es preferible, especialmente cuando se trata, por ejemplo, de transacciones financieras. Pero en muchos casos sería útil hacer que el proceso de análisis sea más leal. En este artículo me gustaría compartir mi experiencia y hablar sobre las principales formas de aumentar esta misma lealtad.
Filtrar objetos no válidos
Bueno, el primer elemento es, por supuesto, filtrar objetos no válidos. En muchas situaciones, no queremos perder todo el objeto si uno de los objetos anidados no es válido. Esto se aplica tanto a objetos individuales como a matrices de objetos. Daré un ejemplo. Supongamos que hacemos una solicitud para vender productos y en una de las pantallas obtenemos una lista de productos en aproximadamente esta forma.
{
"products": [
{...},
{...},
....
]
}
, . . , , - . , JSONDecoder
“ ” .
, :
{
"id": 1,
"title": "Awesome product",
"price": 12.2,
"image": {
"id": 1,
"url": "http://image.png",
"thumbnail_url": "http://thumbnail.png"
}
}
, , image
. , , image , image = nil
. , , JSONDecoder
.
, JSONDecoder
2 : decode
decodeIfPresent
. optional
, nil
, , null
. .
, , . , init(decoder)
try?
nil
. , boilerplate . .
struct FailableDecodable<Value: Decodable>: Decodable {
var wrappedValue: Value?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
wrappedValue = try? container.decode(Value.self)
}
}
struct FailableDecodableArray<Value: Decodable>: Decodable {
var wrappedValue: [Value]
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var elements: [Value] = []
while !container.isAtEnd {
if let element = try? container.decode(Value.self) {
elements.append(element)
}
}
wrappedValue = elements
}
}
.
struct ProductList: Decodable {
var products:FailableDecodableArray<Product>
}
struct Product: Decodable {
let id: Int
let title: String
let price: Double
let image: FailableDecodable<Image>?
}
struct Image: Decodable {
let id: Int
let url: String
let thumbnailUrl: String
}
. , .
let products = productsList.products.wrappedValue
let image = products.first?.image.wrappedValue
FailableDecodableArray
. , RandomAccessCollection
MutableCollection
, wrappedValue
. FailableDecodable
. , , computed property
, , . , .
@propertyWrapper
Swift 5.1 — @propertyWrapper
.
@propertyWrapper
struct FailableDecodable<Value: Decodable>: Decodable {
...
}
@propertyWrapper
struct FailableDecodableArray<Value: Decodable>: Decodable {
...
}
struct ProductList: Decodable {
@FailableDecodableArray
var products:[Product]
}
struct Product: Decodable {
let id: Int
let title: String
let price: Double
@FailableDecodable
let image:Image?
}
, wrappedValue
. , , , , :)
, , optional
.
@FailableDecodable
let image:Image?
let image: FailableDecodable<Image>
, optional
Image?
, wrappedValue
optional
, .
Swift
@FailableDecodable?
let image:Image?
, , JSON nil
. @propertyWrapper
, 100% JSON.
@dynamicMemberLookup
dynamicMemberLookup
.
@dynamicMemberLookup
struct FailableDecodable<Value: Decodable>: Decodable {
var wrappedValue: Value?
subscript<Prop>(dynamicMember kp: KeyPath<Value, Prop>) -> Prop {
wrappedValue[keyPath: kp]
}
subscript<Prop>(dynamicMember kp: WritableKeyPath<Value, Prop>) -> Prop {
get {
wrappedValue[keyPath: kp]
}
set {
wrappedValue[keyPath: kp] = newValue
}
}
}
2 subscript
, readonly , read/write . .
struct ProductList: Decodable {
var products:FailableDecodableArray<Product>
}
struct Product: Decodable {
let id: Int
let title: String
let price: Double
let image: FailableDecodable<Image>?
}
@propertyWrapper
wrappedValue
, .
let imageURL = products.first?.image.url
, optional
. , , wrappedValue
, , .
products.first?.image.load()
products.first?.image.wrappedValue.load()
( ), .
— . - , ( JSON) Swift , “1” 1 — . JSONDecoder
, , . . , Product
.
{
"id": 1,
"title": "Awesome product",
"price": "12.2",
"image": {
"id": 1,
"url": "http://image.png",
"thumbnail_url": "http://thumbnail.png"
}
}
, , , — , , , , , . , .
struct Convertible<Value: Decodable & LosslessStringConvertible>: Decodable {
var wrappedValue: Value
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
guard let stringValue = try? container.decode(String.self),
let value = Value(stringValue) else {
wrappedValue = try container.decode(Value.self)
return
}
wrappedValue = value
}
}
struct Product: Decodable {
let id: Int
let title: String
let price: Convertible<Double>
let image: FailableDecodable<Image>?
}
, . FailableDecdable
@propertyWrapper
@dynamicMemberLookup
.
( ) . , , API - , , - . , , , , -, , , -, , . , , .
struct StringConvertible: Decodable {
var wrappedValue: String
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
guard let number = try? container.decode(Double.self) else {
wrappedValue = try container.decode(String.self)
return
}
wrappedValue = "\(number)"
}
}
Resumiendo todo lo anterior, me gustaría señalar que la aparición del protocolo Decodable
junto con la clase JSONDecoder
simplificó significativamente nuestra vida en lo que está asociado con el análisis de los datos del servidor. Sin embargo, vale la pena señalar que JSONDecoder
en este momento tiene una lealtad extremadamente baja y para mejorarla (si es necesario) debe trabajar un poco y escribir algunos envoltorios. Creo que en el futuro todas estas características se realizarán en el objeto en sí mismo JSONDecoder
, porque hace relativamente poco tiempo ni siquiera sabía cómo convertir las llaves de serpiente a camello y la cadena hasta la fecha, y ahora todo esto está disponible "fuera de la caja".