Aumento de la lealtad del analizador de datos del servidor en iOS

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 Decodabley 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 JSONDecoderhará 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,JSONDecoderarroja 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() // Compilation error
products.first?.image.wrappedValue.load() // Success

( ), .



— . - , ( 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 Decodablejunto con la clase JSONDecodersimplificó 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 JSONDecoderen 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".


All Articles