简体   繁体   中英

Swift custom decoding including another custom decoded model

I am currently building a food app using Yelp Fusion API. There are two models called Business and BusinessDatail in my project. To list up restaurants in a tableView, I fetch a restaurant list from backend and custom decode it as an array of Business, [Business]. And then, when a tableViewCell of Business is clicked, I fetch detailed business info and custom decode it as a BusinessDetail.

struct Business {
    let id: String
    let name: String
    let rating: Float?
    let price: String?
    let displayPhone: String?
    let imageUrl: URL
    let category: String
    let reviewCount: Int
    let coordinates: Coordinates
    let address: Address
}
struct BusinessDetail {

    let id: String
    let name: String
    let rating: Float?
    let price: String?
    let displayPhone: String?
    let imageUrl: URL
    let category: String
    let reviewCount: Int
    let coordinates: Coordinates
    let address: Address

    let isClaimed: Bool
    let isClosed: Bool
    let url: URL
    let phone: String?

    let photoURLs: [URL]

    var hours: [Hours]?
}

Both Business and BusinessDetail are custom decoded models and they are currently initialised this way.

extension Business: Decodable {
    enum CodingKeys: String, CodingKey {
        case id
        case name
        case rating
        case price
        case displayPhone = "display_phone"
        case imageUrl = "image_url"
        case category = "categories"
        case reviewCount = "review_count"
        case coordinates
        case address = "location"
        enum CategoryCodingKeys: String, CodingKey {
            case title
        }
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        id = try container.decode(String.self, forKey: .id)
        name = try container.decode(String.self, forKey: .name)
        rating = try container.decode(Float.self, forKey: .rating)
        price = try container.decodeIfPresent(String.self, forKey: .price)
        displayPhone = try container.decodeIfPresent(String.self, forKey: .displayPhone)
        imageUrl = try container.decode(URL.self, forKey: .imageUrl)

        var categoriesContainer = try container.nestedUnkeyedContainer(forKey: .category)
        var tempCategory = ""

        while !categoriesContainer.isAtEnd {
            let categoryContainer = try categoriesContainer.nestedContainer(keyedBy: CodingKeys.CategoryCodingKeys.self)
            let title = try categoryContainer.decode(String.self, forKey: .title)
            tempCategory += tempCategory == "" ? title: ", \(title)"
        }
        category = tempCategory

        reviewCount = try container.decode(Int.self, forKey: .reviewCount)
        coordinates = try container.decode(Coordinates.self, forKey: .coordinates)
        address = try container.decode(Address.self, forKey: .address)
    }
}
extension PlaceDetail: Decodable {
    enum CodingKeys: String, CodingKey {
        case id
        case name
        case rating
        case price
        case displayPhone = "display_phone"
        case imageUrl = "image_url"
        case category = "categories"
        case reviewCount = "review_count"
        case coordinates
        case address = "location"
        enum CategoryCodingKeys: String, CodingKey {
            case title
        }

        case isClaimed = "is_claimed"
        case isClosed = "is_closed"
        case url
        case phone
        case photoURLs = "photos"
        case hours
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(String.self, forKey: .id)
        name = try container.decode(String.self, forKey: .name)
        rating = try container.decode(Float.self, forKey: .rating)
        price = try container.decodeIfPresent(String.self, forKey: .price)
        displayPhone = try container.decode(String.self, forKey: .displayPhone)
        imageUrl = try container.decode(URL.self, forKey: .imageUrl)

        var categoriesContainer = try container.nestedUnkeyedContainer(forKey: .category)
        var tempCategory = ""
        while !categoriesContainer.isAtEnd {
            let categoryContainer = try categoriesContainer.nestedContainer(keyedBy: CodingKeys.CategoryCodingKeys.self)
            let title = try categoryContainer.decode(String.self, forKey: .title)
            tempCategory += tempCategory == "" ? title: ", \(title)"
        }

        category = tempCategory
        reviewCount = try container.decode(Int.self, forKey: .reviewCount)
        coordinates = try container.decode(Coordinates.self, forKey: .coordinates)
        address = try container.decode(Address.self, forKey: .address)

        isClaimed = try container.decode(Bool.self, forKey: .isClaimed)
        isClosed = try container.decode(Bool.self, forKey: .isClosed)
        url = try container.decode(URL.self, forKey: .url)
        phone = try container.decode(String.self, forKey: .phone)
        photoURLs = try container.decode([URL].self, forKey: .photoURLs)
        hours = try container.decodeIfPresent([Hours].self, forKey: .hours)
    }
}

Here is the json for a business list.

{
    "businesses": [
        {
            "id": "I1D8NHvMWf8oMYceTyLlHg",
            "name": "John Mills Himself",
            "image_url": "https://s3-media3.fl.yelpcdn.com/bphoto/OH84e6eP8zpzBECF0WvTXA/o.jpg",
            "is_closed": false,
            "url": "https://www.yelp.com/biz/john-mills-himself-brisbane?adjust_creative=0mCaOEYvfM9_oOaXgMuW6A&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=0mCaOEYvfM9_oOaXgMuW6A",
            "review_count": 55,
            "categories": [
                {
                    "alias": "coffee",
                    "title": "Coffee & Tea"
                },
                {
                    "alias": "bakeries",
                    "title": "Bakeries"
                },
                {
                    "alias": "wine_bars",
                    "title": "Wine Bars"
                }
            ],
            "rating": 4.5,
            "coordinates": {
                "latitude": -27.47151,
                "longitude": 153.025654
            },
            "transactions": [],
            "price": "$",
            "location": {
                "address1": "40 Charlotte St",
                "address2": "",
                "address3": "",
                "city": "Brisbane",
                "zip_code": "4000",
                "country": "AU",
                "state": "QLD",
                "display_address": [
                    "40 Charlotte St",
                    "Brisbane Queensland 4000",
                    "Australia"
                ]
            },
            "phone": "",
            "display_phone": "",
            "distance": 383.2254392716822
        },
        {
            "id": "KaIoCOg-IZJtLdN39Cw__Q",
            "alias": "strauss-brisbane",
            "name": "Strauss",
            "image_url": "https://s3-media2.fl.yelpcdn.com/bphoto/eYKx68uOaEY5k9Jt4TrQPw/o.jpg",
            "is_closed": false,
            "url": "https://www.yelp.com/biz/strauss-brisbane?adjust_creative=0mCaOEYvfM9_oOaXgMuW6A&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=0mCaOEYvfM9_oOaXgMuW6A",
            "review_count": 33,
            "categories": [
                {
                    "alias": "cafes",
                    "title": "Cafes"
                }
            ],
            "rating": 4.5,
            "coordinates": {
                "latitude": -27.469804,
                "longitude": 153.027384
            },
            "transactions": [],
            "price": "$",
            "location": {
                "address1": "189 Elizabeth St",
                "address2": "",
                "address3": "",
                "city": "Brisbane",
                "zip_code": "4000",
                "country": "AU",
                "state": "QLD",
                "display_address": [
                    "189 Elizabeth St",
                    "Brisbane Queensland 4000",
                    "Australia"
                ]
            },
            "phone": "+61732365232",
            "display_phone": "+61 7 3236 5232",
            "distance": 247.3760157828213
        }
}

And this is json response for individual BusinessDetail.

{
    "id": "I1D8NHvMWf8oMYceTyLlHg",
    "name": "John Mills Himself",
    "image_url": "https://s3-media3.fl.yelpcdn.com/bphoto/OH84e6eP8zpzBECF0WvTXA/o.jpg",
    "is_claimed": false,
    "is_closed": false,
    "url": "https://www.yelp.com/biz/john-mills-himself-brisbane?adjust_creative=0mCaOEYvfM9_oOaXgMuW6A&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_lookup&utm_source=0mCaOEYvfM9_oOaXgMuW6A",
    "phone": "",
    "display_phone": "",
    "review_count": 55,
    "categories": [
        {
            "alias": "coffee",
            "title": "Coffee & Tea"
        },
        {
            "alias": "bakeries",
            "title": "Bakeries"
        },
        {
            "alias": "wine_bars",
            "title": "Wine Bars"
        }
    ],
    "rating": 4.5,
    "location": {
        "address1": "40 Charlotte St",
        "address2": "",
        "address3": "",
        "city": "Brisbane",
        "zip_code": "4000",
        "country": "AU",
        "state": "QLD",
        "display_address": [
            "40 Charlotte St",
            "Brisbane Queensland 4000",
            "Australia"
        ],
        "cross_streets": ""
    },
    "coordinates": {
        "latitude": -27.47151,
        "longitude": 153.025654
    },
    "photos": [
        "https://s3-media3.fl.yelpcdn.com/bphoto/OH84e6eP8zpzBECF0WvTXA/o.jpg",
        "https://s3-media1.fl.yelpcdn.com/bphoto/L8dJYv1GCW1m5IvD0M3qXw/o.jpg",
        "https://s3-media4.fl.yelpcdn.com/bphoto/oUH4cJmPRuAs_jv_xRtXQg/o.jpg"
    ],
    "price": "$",
    "hours": [
        {
            "open": [
                {
                    "is_overnight": false,
                    "start": "0630",
                    "end": "1530",
                    "day": 0
                },
                {
                    "is_overnight": false,
                    "start": "0630",
                    "end": "1530",
                    "day": 1
                },
                {
                    "is_overnight": false,
                    "start": "1600",
                    "end": "2100",
                    "day": 1
                },
                {
                    "is_overnight": false,
                    "start": "0630",
                    "end": "1530",
                    "day": 2
                },
                {
                    "is_overnight": false,
                    "start": "1600",
                    "end": "2100",
                    "day": 2
                },
                {
                    "is_overnight": false,
                    "start": "0630",
                    "end": "1530",
                    "day": 3
                },
                {
                    "is_overnight": false,
                    "start": "1600",
                    "end": "2330",
                    "day": 3
                },
                {
                    "is_overnight": false,
                    "start": "0630",
                    "end": "1530",
                    "day": 4
                },
                {
                    "is_overnight": false,
                    "start": "1600",
                    "end": "2330",
                    "day": 4
                },
                {
                    "is_overnight": false,
                    "start": "1600",
                    "end": "2330",
                    "day": 5
                }
            ],
            "hours_type": "REGULAR",
            "is_open_now": true
        }
    ]
}

As you can see, BusinessDetail has exactly same info as Business at the top and I'd like to change BusinessDetail like below.

struct BusinessDetail {
    let business: Business

    let isClaimed: Bool
    let isClosed: Bool
    let url: URL
    let phone: String?

    let photoURLs: [URL]

    var hours: [Hours]?
}

However, I am not sure if it is technically possible.

Here's a conceptual example, but you can apply the same principle to your case, with the following JSON:

{ "a": 1, "b": 2, "c": "three" }

and these models:

struct Smaller: Decodable {
  var a, b: Int
}

struct Larger: Decodable {
  var smaller: Smaller
  var c: String
}

Decoding Smaller doesn't need any extra work (ie no need to manually implement the init(from: Decoder) initializer):

let decoder = JSONDecoder()
let small = try! decoder.decode(Smaller.self, from: jsonData)

But with Larger , you need to manually do this:

struct Larger: Decodable {
  var smaller: Smaller
  var c: String

  enum CodingKeys: CodingKey {
     case c
  }

  init(from decoder: Decoder) throws {
     let container = try decoder.container(keyedBy: CodingKeys.self)

     self.smaller = try Smaller.init(from: decoder)
     self.c = try container.decode(String.self, forKey: .c)
  }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM