NEW: Learn to build the incredible iOS 15 Weather app today! >>

Help: inheritance, protocol, and structs

Forums > Swift

I feel like I've seen the answer to this somewhere but I can't type the right keywords to get to it. Because of this, I suspect that I'm also doing it wrong, and There's A Better Way.

I'm writing an application to build passes for Apple Wallet. There are four kinds of passes: boarding passes, payment methods, coupons, and "generic". They all have some common fields, and then each one has special rules. I'm trying to make some helper structs that will make generating valid JSON for the pass.json file simpler.

Deep inside, there are fields (things like title, heading, etc.) that can take one of three data types: a plain String, an ISO 8601 date that's formatted as a String, or a number (could be Int, could be Double...). So, I want to have a PassField that can render itself to JSON, and then have three concrete structs: StringField, DateField, and NumberField. But it seems that Numeric isn't necessarily Encodable, so I guess I get four, with NumberField being split into IntegerField and DoubleField.

Then, a pass can have collections of fields: the stuff on the back, the header, the front, auxiliary. Each of these is a [PassField]. I've declared: public protocol PassField: Encodable, and then on each of the structs that implement PassField they do the encoding. But in the pass' encode method when I try to encode the fields: try container.encode(primaryFields, forKey: .primaryFields) I get this error: Protocol 'PassField' as a type cannot conform to 'Encodable'.

So, what does that mean? Do I have to do this with classes and inheritance rather than protocols?

   

You can't supply a protocol as the type to decode into, it has to be a concrete type. Which, as you've encountered, can be a problem when there are multiple types involved.

If you give us some sample code (structs, JSON data, etc.) we can work on a solution.

Also, maybe this article will be some help: Bringing Polymorphism to Codable

1      

Yeah, I guess the answer is to use classes rather than structs.

   

I mean, it's possible with structs, it just depends on how your JSON is structured and what your structs look like.

Here's an example:

import Foundation

enum PassType: String, Decodable {
    case boardingPass
    case paymentMethod
    case coupon
    case genericPass

    var metatype: Pass.Type {
        switch self {
        case .boardingPass: return BoardingPass.self
        case .paymentMethod: return PaymentMethod.self
        case .coupon: return Coupon.self
        case .genericPass: return GenericPass.self
        }
    }
}

protocol Pass: Decodable {
    var type: PassType { get }
    var title: String { get set }
    var heading: String { get set }
    var date: Date { get set }
}

struct BoardingPass: Pass {
    let type = PassType.boardingPass
    var title: String
    var heading: String
    var date: Date
    var roundTrip: Bool
}

struct PaymentMethod: Pass {
    let type = PassType.paymentMethod
    var title: String
    var heading: String
    var date: Date
    var signatureRequired: Bool
}

struct Coupon: Pass {
    let type = PassType.coupon
    var title: String
    var heading: String
    var date: Date
    var item: String
    var percentOff: Double
}

struct GenericPass: Pass {
    let type = PassType.genericPass
    var title: String
    var heading: String
    var date: Date
    var num: Double
}

struct PassWrapper: Decodable {
    let pass: Pass

    enum CodingKeys: CodingKey {
        case type, pass
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let type = try container.decode(PassType.self, forKey: .type)
        self.pass = try type.metatype.init(from: container.superDecoder(forKey: .pass))
    }
}

let passJSON = """
[
    {
        "type": "paymentMethod",
        "pass": {
            "title": "PaymentPass",
            "heading": "This is a payment pass",
            "date": "2021-07-18T22:01:37Z",
            "signatureRequired": false
        }
    },
    {
        "type": "genericPass",
        "pass": {
            "title": "A Pass",
            "heading": "This is generic",
            "date": "2022-01-01T00:00:00Z",
            "num": 4
        }
    },
    {
        "type": "boardingPass",
        "pass": {
            "title": "All Aboard!",
            "heading": "Board a flight to your dream vacation",
            "date": "2021-10-01T12:00:00Z",
            "roundTrip": true
        }
    },
    {
        "type": "coupon",
        "pass": {
            "title": "Coupon: The Movie",
            "heading": "The biggest failure in movie history",
            "date": "1996-12-20T22:00:00Z",
            "item": "tube socks",
            "percentOff": 50.0
        }
    }
]
""".data(using: .utf8)!

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601

var passes: [Pass] = []

do {
    let result = try decoder.decode([PassWrapper].self, from: passJSON)
    for p in result {
        passes.append(p.pass)
    }
} catch {
    print(error)
}

(example inspired by Encoding And Decoding Polymorphic Objects In Swift)

You could also use an enum-based solution.

   

Hacking with Swift is sponsored by Sentry

SPONSORED With Sentry’s error and performance monitoring for iOS, you see mobile vitals that actually matter, can solve any latency issues quickly, and learn how each release is performing over time.

Learn More

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.