Meningkatkan loyalitas parser data server di iOS

Ketika mengembangkan aplikasi seluler, dengan satu atau lain cara, kita dihadapkan dengan kebutuhan untuk mengurai data server ke dalam model aplikasi internal. Dalam sebagian besar kasus, data ini datang dalam format JSON. Dimulai dengan Swift 4, alat utama untuk mem-parsing JSON adalah menggunakan protokol Decodabledan objek JSONDecoder.


Pendekatan ini sangat menyederhanakan proses penguraian data dan mengurangi jumlah kode boilerplate. Dalam kebanyakan kasus, cukup mudah untuk membuat model dengan properti yang dinamai seperti bidang di objek JSON dan JSONDecoderakan melakukan sisanya untuk Anda. Kode minimum, manfaat maksimum. Namun, pendekatan ini memiliki satu kelemahan, yaitu, kesetiaan parser yang sangat rendah. Saya akan menjelaskan. Jika ada perbedaan antara model data internal (objek yang dapat didekodekan) dan apa yang muncul di JSON,JSONDecodermelempar kesalahan dan kami kehilangan seluruh objek. Mungkin, dalam beberapa situasi, model perilaku ini lebih disukai, terutama ketika datang, misalnya, untuk transaksi keuangan. Tetapi dalam banyak kasus akan berguna untuk membuat proses parsing lebih loyal. Dalam artikel ini saya ingin berbagi pengalaman dan berbicara tentang cara utama untuk meningkatkan loyalitas yang sama ini.


Memfilter objek yang tidak valid


Nah, item pertama tentu saja adalah memfilter objek yang tidak valid. Dalam banyak situasi, kami tidak ingin kehilangan seluruh objek jika salah satu objek bersarang tidak valid. Ini berlaku untuk objek tunggal dan array objek. Saya akan memberi contoh. Misalkan kita membuat aplikasi untuk menjual barang dan di salah satu layar kita mendapatkan daftar barang dalam bentuk ini.


{
    "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)"
    }
}

Meringkas semua hal di atas, saya ingin mencatat bahwa kemunculan protokol Decodablebersama dengan kelas JSONDecodersecara signifikan menyederhanakan hidup kita dalam apa yang terkait dengan penguraian data server. Namun, perlu dicatat bahwa JSONDecodersaat ini memiliki loyalitas yang sangat rendah dan untuk meningkatkannya (jika perlu) Anda perlu bekerja sedikit dan menulis beberapa bungkus. Saya berpikir bahwa di masa depan semua fitur ini akan terwujud dalam objek itu sendiri JSONDecoder, karena relatif baru-baru ini dia bahkan tidak tahu bagaimana mengkonversi kunci dari snakecase ke camelcase dan string to date, dan sekarang semua ini tersedia "di luar kotak".


All Articles