NEW: My new book Pro SwiftUI is out now – level up your SwiftUI skills today! >>

Loading JSON ignores different subclasses

Forums > Swift

Hi,

This is a two part question, the first being around best practice and the second about solving an actual problem - they are related, I promise.

The app I'm writing (of which the code below is representative of) allows for the creation of objects with many subclasses which essentially contain the functionality that drive the program. I'm currently trying to save that data as JSON and am running into problems with the decoding of the saved data.

My first question is - should I be using JSON? At present there is no need for sharing of the data outside of the app, so is using JSON the best approach. I know that, ultimately, there is little point in trying to hide data from determined users but using JSON seems to be helping the inquisitive a little too much. Should I be looking at alternatives and, if so, where would you suggest I look?

My second question is - how the hell do I decode subclasses of data? :D

I'll provide the code below and the output of the code which shows the problem. With this example code I can create multiple shops and hold multiple vehicles (superclass) of different sub-classes (cars and bikes) within that shop. Saving the data works and loading the data works up to a point - the correct data is returned but the vehicles do not get decoded into their correct sub-classes, which can be seen in the annotated output below where the decoder used is always 'Vehicle'. Just FYI, I've seen it mentioned that heterogenous arrays are not best practice but they are a necessity within my app.

*** Listing vehicles within shop, shows overridden methods being used ***
Test: ID(1) - Bicycle Id: 0, Manufacture: Bike Maker, Wheels: 2, Has Basket: false
Test: ID(1) - Bicycle Id: 1, Manufacture: Bike Maker, Wheels: 2, Has Basket: false
Test: ID(1) - Car Id: 2, Manufacture: Car Maker, Wheels: 4, Left-Hand Drive: false
Test: ID(1) - Car Id: 3, Manufacture: Car Maker, Wheels: 4, Left-Hand Drive: false

*** Saving Data shows correct data being saved *** 
Test: ID(1) - JSON = {"vehiclesStocked":[{"id":0,"manufacturer":"Bike Maker","wheels":2,"vehicleType":1,"hasBasket":false},{"id":1,"manufacturer":"Bike Maker","wheels":2,"vehicleType":1,"hasBasket":false},{"isLeftHandDrive":false,"id":2,"manufacturer":"Car Maker","wheels":4,"vehicleType":2},{"isLeftHandDrive":false,"id":3,"manufacturer":"Car Maker","wheels":4,"vehicleType":2}],"shopName":"Shop Name - 000"}

*** Decoding shows that all vehicles are decoded as Super Class Vehicle ***
Test: ID(0) - Decoding Vehicle
Test: ID(0) - Decoding Vehicle
Test: ID(0) - Decoding Vehicle
Test: ID(0) - Decoding Vehicle

*** Output shows that the data has been saved and retrieved correctly with Sub-Class data intact ***
Test: ID(77) - JSON: {"vehiclesStocked":[{"id":0,"manufacturer":"Bike Maker","wheels":2,"vehicleType":1,"hasBasket":false},{"id":1,"manufacturer":"Bike Maker","wheels":2,"vehicleType":1,"hasBasket":false},{"isLeftHandDrive":false,"id":2,"manufacturer":"Car Maker","wheels":4,"vehicleType":2},{"isLeftHandDrive":false,"id":3,"manufacturer":"Car Maker","wheels":4,"vehicleType":2}],"shopName":"Shop Name - 000"}

*** Verification of Vehicle Type (ie, subtype) identifies correct vehicle type ***
Test: ID(0) - Vehicle = vtBicycle
Test: ID(0) - Vehicle = vtBicycle
Test: ID(0) - Vehicle = vtCar
Test: ID(0) - Vehicle = vtCar

*** Listing vehicles shows that Superclass function is being called, not specific Sub-Class function ***
Test: ID(1) - Id: 0, Vehicle Type: vtBicycle, Manufacture: Bike Maker, Wheels: 2
Test: ID(1) - Id: 1, Vehicle Type: vtBicycle, Manufacture: Bike Maker, Wheels: 2
Test: ID(1) - Id: 2, Vehicle Type: vtCar, Manufacture: Car Maker, Wheels: 4
Test: ID(1) - Id: 3, Vehicle Type: vtCar, Manufacture: Car Maker, Wheels: 4

So what I want to know is how do I resolve this? Any help would be greatly appreciated. Thanks.

Here is the code. MyClass.Swift

//
//  MyClass.swift
//  TestJsonEncodeDecode
//
//  Created by Jes Hynes on 28/07/2022.
//

import Foundation
import SwiftUI

func debugLog (_ id: Int, _ output : String ) {
    print("Test: ID(\(id)) - \(output)")
}

enum eVehicleType : Int, Codable {
    case vtVehicle = 0, vtBicycle, vtCar
}

class Manager : ObservableObject {
    @Published var shop = [VehicleShop]()
    var shopData = Data()
    @Published var selectedShopId = 0

    static var shopId = 0

    static let shared = Manager()

    func addShop(){
        let newShop = VehicleShop(shopName: "Unnamed Shop")
        newShop.id = Manager.shopId
        newShop.shopName = "Shop Name - " + String(format: "%03d", newShop.id)
        Manager.shopId += 1
        shop.append(newShop)
    }

    func listShops(){
        for thisShop in shop {
            debugLog(thisShop.id, thisShop.shopName)
        }
    }
}

class VehicleShop : ObservableObject, Codable, Hashable {
    static func == (lhs: VehicleShop, rhs: VehicleShop) -> Bool {
        lhs.id == rhs.id
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }

    var id = 0
    @Published var shopName : String = "Unknown"
    static var vehicleId : Int = 0
    var vehiclesStocked = [Vehicle]()
    var selectedVehicle : Vehicle?

    enum CodingKeys: String, CodingKey {
        case shopName
        case vehiclesStocked
    }

    required init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        shopName = try values.decode(String.self, forKey: .shopName)
        vehiclesStocked = try values.decode([Vehicle].self, forKey: .vehiclesStocked)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(shopName, forKey: .shopName)
        try container.encode(vehiclesStocked, forKey: .vehiclesStocked)
    }

    init(shopName: String){
        self.shopName = shopName
    }

    func saveData()->Data{
        var shopData = Data()
        do{
            let encoder = JSONEncoder()

            let data = try encoder.encode(self)
            encoder.outputFormatting = .prettyPrinted

            if let jsonString = String(data: data, encoding: .utf8){
                debugLog(1,"JSON = \(jsonString)")
            }
            shopData = data
        } catch let err {
            debugLog(999,"Unable to save data.  Error \(err)")
        }
        return shopData
    }

    func loadData(shopData: Data){
        vehiclesStocked = [Vehicle]()
        var thisShopStock : VehicleShop?
        do {
            thisShopStock = try JSONDecoder().decode(VehicleShop.self, from: shopData)
            let json = NSString(data: shopData, encoding: String.Encoding.utf8.rawValue)
            debugLog(77, "JSON: \(json! as String)")

            for vehiclesStock in thisShopStock!.vehiclesStocked {
                debugLog(0,"Vehicle = \(vehiclesStock.vehicleType)")
                switch vehiclesStock.vehicleType {

                case .vtVehicle:
                    vehiclesStocked.append(vehiclesStock)
                case .vtBicycle:
                    vehiclesStocked.append(vehiclesStock)   //  <-- This just adds item as a Vehicle, NOT a bicycle
                    //  print(vehiclesStock.hasBasket) // <-- Value of type 'Vehicle' has no member 'hasBasket'
                    //  vehiclesStocked.append(vehiclesStock as! Bicycle)   // <-- Downcasting causes an error
                case .vtCar:
                    vehiclesStocked.append(vehiclesStock)
                }
            }
        } catch let err {
            debugLog(999, "Failed to decode data.  Error \(err)")
        }
    }

    func addBicycle(){
        let bicycle = Bicycle()
        vehiclesStocked.append(bicycle)
    }

    func addCar(){
        let car = Car()
        vehiclesStocked.append(car)
    }

    func listVehicles(){
        for vehiclesStock in vehiclesStocked {
            vehiclesStock.displayVehicle()
        }
    }

    func getAllCars()->[Car]{
        let cars =  vehiclesStocked.compactMap { $0 as? Car }
        return cars
    }

    func getAllBicycles()->[Bicycle]{
        let bicycles =  vehiclesStocked.compactMap { $0 as? Bicycle }
        return bicycles
    }

    func displayVehicle(){
        switch selectedVehicle?.vehicleType {
        case .vtBicycle:
            let bicycle : Bicycle = selectedVehicle as! Bicycle
            Bicycle().displayVehicle(bicycle: bicycle)
        case .vtCar:
            let car : Car = selectedVehicle as! Car
            Car().displayVehicle(car: car)
        default:
            break
        }
    }
}

class Vehicle : ObservableObject, Codable {

    var id : Int
    var vehicleType : eVehicleType = .vtVehicle
    @Published var wheels : Int
    @Published var manufacturer : String

    enum CodingKeys: String, CodingKey {
        case id
        case vehicleType
        case wheels
        case manufacturer
    }

    init(){
        self.id = VehicleShop.vehicleId
        VehicleShop.vehicleId += 1
        self.wheels = 0
        self.manufacturer = "Unknown"
    }

    required init(from decoder: Decoder) throws {
        debugLog(0, "Decoding Vehicle")
        let values = try decoder.container(keyedBy: CodingKeys.self)
        id = try values.decode(Int.self, forKey: .id)
        wheels = try values.decode(Int.self, forKey: .wheels)
        manufacturer = try values.decode(String.self, forKey: .manufacturer)
        vehicleType = try values.decode(eVehicleType.self, forKey: .vehicleType)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(id, forKey: .id)
        try container.encode(vehicleType, forKey: .vehicleType)
        try container.encode(wheels, forKey: .wheels)
        try container.encode(manufacturer, forKey: .manufacturer)
    }

    func displayVehicle(){
        debugLog(1, "Id: \(id), Vehicle Type: \(vehicleType), Manufacture: \(manufacturer), Wheels: \(wheels)")
    }

}

class Bicycle : Vehicle {
    @Published var hasBasket : Bool

    enum CodingKeys: String, CodingKey {
        case hasBasket
    }

    override init(){
        self.hasBasket = false
        super.init()
        vehicleType = .vtBicycle
        wheels = 2
        manufacturer = "Bike Maker"
    }

    required init(from decoder: Decoder) throws {
        debugLog(0, "Decoding Bicycle")

        let values = try decoder.container(keyedBy: CodingKeys.self)
        hasBasket = try values.decode(Bool.self, forKey: .hasBasket)
        try super.init(from: decoder)
        vehicleType = .vtBicycle

    }

    override func encode(to encoder: Encoder) throws {
        try super.encode(to: encoder)
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(hasBasket, forKey: .hasBasket)
    }

    func displayVehicle(bicycle: Bicycle){
        debugLog(1, "Bicycle Id: \(bicycle.id), Manufacture: \(bicycle.manufacturer), Wheels: \(bicycle.wheels), Has Basket: \(bicycle.hasBasket)")
    }

    override func displayVehicle(){
        debugLog(1, "Bicycle Id: \(id), Manufacture: \(manufacturer), Wheels: \(wheels), Has Basket: \(hasBasket)")
    }
}

class Car : Vehicle {
    @Published var isLeftHandDrive : Bool

    enum CodingKeys: String, CodingKey {
        case isLeftHandDrive
    }

    override init(){
        isLeftHandDrive = false
        super.init()
        vehicleType = .vtCar
        wheels = 4
        manufacturer = "Car Maker"
    }

    required init(from decoder: Decoder) throws {
        debugLog(0, "Decoding Car")

        let values = try decoder.container(keyedBy: CodingKeys.self)
        isLeftHandDrive = try values.decode(Bool.self, forKey: .isLeftHandDrive)

        try super.init(from: decoder)
        vehicleType = .vtCar

    }

    override func encode(to encoder: Encoder) throws {
        try super.encode(to: encoder)
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(isLeftHandDrive, forKey: .isLeftHandDrive)
    }

    func displayVehicle(car: Car){
        debugLog(1, "Car Id: \(car.id), Manufacture: \(car.manufacturer), Wheels: \(car.wheels), Left Hand Drive: \(car.isLeftHandDrive)")
    }

    override func displayVehicle(){
        debugLog(1, "Car Id: \(id), Manufacture: \(manufacturer), Wheels: \(wheels), Left-Hand Drive: \(isLeftHandDrive)")
    }
}

func setUp(){
    //let milfords = VehicleShop(shopName: "Milfords")
    //Manager.shared.shop.append(milfords)
}

Content View

//
//  ContentView.swift
//  TestJsonEncodeDecode
//
//  Created by Jes Hynes on 28/07/2022.
//

import SwiftUI

struct ManagerView: View {
    @ObservedObject var manager : Manager

    func getShopName()->String {
        var shopName = "None Selected"
        if (manager.selectedShopId != 0) {
            shopName = manager.shop[manager.selectedShopId].shopName
        }
        return shopName
    }

    var body: some View {

        Button("Add Shop (Shop Count = \(manager.shop.count))", action:
            manager.addShop
        )

        VStack{
            Picker(selection: $manager.selectedShopId, label: Text("Selected Shop")){
                ForEach(manager.shop, id: \.id){
                    Text($0.shopName).tag($0.id)
                }
            }.id(UUID())
        }
    }
}

struct ShopView: View {
    @ObservedObject var shop : VehicleShop

    func saveData(){
        Manager.shared.shopData = shop.saveData()
    }

    func loadData(){
        shop.loadData(shopData: Manager.shared.shopData)
    }

   var body: some View {
        Button("Add Bike", action:
            shop.addBicycle
        )

        Button("Add Car", action:
            shop.addCar
        )

        Button("List Vehicles", action:
            shop.listVehicles
        )

        Button("Save Data", action:
            saveData
        )

        Button("Load Data", action:
            loadData
        )
    }
}
struct ContentView: View {
    @EnvironmentObject var manager : Manager

    var body: some View {

        ManagerView(manager: manager)
        if manager.shop.count > 0 {
            let shop = manager.shop[manager.selectedShopId]
            ShopView(shop: shop)
        }
    }

}

App:

//
//  TestJsonEncodeDecodeApp.swift
//  TestJsonEncodeDecode
//
//  Created by Jes Hynes on 28/07/2022.
//

import SwiftUI

@main
struct TestJsonEncodeDecodeApp: App {
    @StateObject var manager = Manager.shared
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(manager)
        }
    }
}

   

Hi,

Just replying to myself as there's been no response in four months - any response would be fine, honestly :)

Am I going about things completely the wrong way, has no-one else been able to resolve this kind of problem, should I be looking at implementing in a different way?

I've looked at the code in another question along similar lines that uses structs and a meta-wrapper (https://www.hackingwithswift.com/forums/swift/help-inheritance-protocol-and-structs/9840) but this doesn't seem to want to work with classes, again I'm guessing I'm missing something fundamental.

Just for a bit of further information (in my actual application) the main class is divided into about ten subclasses with these ranging from three to thirty subclasses...and, yes, this is appropriate for my application.

I was wondering whether it would be worthwhile iterating through the individual subclasses and saving them as separate collections and then, on loading, do some jiggery-pokery to get them correctly set up again but that seems counter intuitive. Back in the day I would have just written a filing routine for each object but even that seems problematic and complex in Swift.

Should I be using CoreData instead? How flexible to change is it, ie, can I just design tables as I want, as I go, or do I need to have my data structures established before I start?

I would be really grateful for any advice or guidance as this is proving to be a major stumbling block.

Thanks in advance,

Jes

   

I get the impression that you want to associate multiple models of vehicle with each shop, and a given model of vehicle could be associated with multiple shops. That is a "many-to-many relationship", in which case you definitely want to use Core Data.

   

Hacking with Swift is sponsored by Play

SPONSORED Play is the first native iOS design tool created for designers and engineers. You can install Play for iOS and iPad today and sign up to check out the Beta of our macOS app with SwiftUI code export. We're also hiring engineers!

Click to learn more about Play!

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.