Aumentando a lealdade do analisador de dados do servidor no iOS

Ao desenvolver aplicativos móveis, de uma maneira ou de outra, somos confrontados com a necessidade de analisar dados do servidor em modelos de aplicativos internos. Na grande maioria dos casos, esses dados são fornecidos no formato JSON. Começando com o Swift 4, a principal ferramenta para analisar JSON está usando o protocolo Decodablee o objeto JSONDecoder.


Essa abordagem simplificou bastante o processo de análise de dados e reduziu o número de códigos padrão. Na maioria dos casos, é bastante simples criar modelos com propriedades nomeadas como campos no objeto JSON e JSONDecoderfará o resto por você. Código mínimo, benefício máximo. No entanto, essa abordagem tem uma desvantagem, a saber, a extremamente baixa lealdade do analisador. Eu vou explicar Se houver alguma discrepância entre o modelo de dados interno (objetos decodíveis) e o que veio no JSON,JSONDecoderlança um erro e perdemos o objeto inteiro. Talvez, em algumas situações, esse modelo de comportamento seja preferível, especialmente quando se trata, por exemplo, de transações financeiras. Mas, em muitos casos, seria útil tornar o processo de análise mais fiel. Neste artigo, gostaria de compartilhar minha experiência e falar sobre as principais maneiras de aumentar essa mesma lealdade.


Filtrando objetos inválidos


Bem, o primeiro item é, obviamente, filtrar objetos inválidos. Em muitas situações, não queremos perder o objeto inteiro se um dos objetos aninhados não for válido. Isso se aplica a objetos únicos e matrizes de objetos. Vou dar um exemplo. Suponha que façamos um pedido de venda de mercadorias e, em uma das telas, recebamos uma lista de mercadorias aproximadamente dessa 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)"
    }
}

Resumindo tudo isso, gostaria de observar que o surgimento do protocolo Decodableem conjunto com a classe JSONDecodersimplificou significativamente nossa vida no que está associado à análise de dados do servidor. No entanto, é importante notar que, JSONDecoderno momento, ele possui uma lealdade extremamente baixa e, para melhorá-la (se necessário), você precisa trabalhar um pouco e escrever alguns invólucros. Penso que, no futuro, todos esses recursos serão realizados no próprio objeto JSONDecoder, porque, relativamente recentemente, ele nem sabia como converter chaves de capa de cobra em camelcase e string até a data, e agora tudo isso está disponível "pronto para uso".


All Articles