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