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 Decodable
e 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 JSONDecoder
fará 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,JSONDecoder
lanç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()
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)"
}
}
Resumindo tudo isso, gostaria de observar que o surgimento do protocolo Decodable
em conjunto com a classe JSONDecoder
simplificou significativamente nossa vida no que está associado à análise de dados do servidor. No entanto, é importante notar que, JSONDecoder
no 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".