Swift - How to Encode & Decode Array of Struct Objects

My model is built on a struct instead of a class. Because of that, I ran into some trouble of figuring how to encode/decode an array of these model objects. I tried to add a class to the struct extension as suggested.


import UIKit
import Foundation 


enum Path: String { case Stock = "Stock" } 


struct Stock { 
     var companyName: String? 
     var symbol: String? 
     var price: Double?


     func documentsDirectory() -> NSString { 
          let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true) 
          let documentDirectory = paths[0] as NSString return documentDirectory 
     } 


     func saveStocksArray() -> Bool { 
          let stockObjects = StockClass(stock: self) 
          let file = documentsDirectory().appendingPathComponent(Path.Stock.rawValue) 
          return NSKeyedArchiver.archiveRootObject(stockObjects, toFile: file) 
     } 


      func loadStocksArray() -> [Stock]? {
          let file = documentsDirectory().appendingPathComponent(Path.Stock.rawValue) 
          let result = NSKeyedUnarchiver.unarchiveObject(withFile: file) return result as? [Stock] 
     } 
} 
extension Stock { 
     class StockClass: NSObject, NSCoding { 
          var stock: Stock? init(stock: Stock) { 
               self.stock = stock super.init() } 
          
          required init?(coder aDecoder: NSCoder) { 
               guard let companyName = aDecoder.decodeObject(forKey: "companyName") as? String else { stock = nil; super.init(); return nil } 
               guard let symbol = aDecoder.decodeObject(forKey: "symbol") as? String else { stock = nil; super.init(); return nil } 
               guard let price = aDecoder.decodeObject(forKey: "price") as? Double else { stock = nil; super.init(); return nil } 


               stock = Stock(companyName: companyName, symbol: symbol, price: price) super.init() } 


          func encode(with aCoder: NSCoder) { 
               if let stock = stock { 
                    aCoder.encode(stock.companyName, forKey: "companyName") 
                    aCoder.encode(stock.symbol, forKey: "symbol") 
                    aCoder.encode(stock.price, forKey: "price") 
               } 
          } 
     }
}


let stocksArray = [Stock(companyName: "Tesla", symbol: "TSLA", price: 200.01), Stock(companyName: "Apple", symbol: "AAPL", price: 120.12)]


Playground gave me an error of "instance vs value type" when I tried to save & load stocksArray. Any suggestion for this problem?

a) you should also post the code snippets and messages how you tried to save & load stocksArray and the exact error message

b) you can not save an single item in `func saveStocksArray() -> Bool` and retrieve it as an array in `func loadStocksArray() -> [Stock]?`

have you edited the code in the post ?


In fact,

  1. var stock: Stock? init(stock: Stock) {
  2. self.stock = stock super.init() }

seems erroneous, lacking a ; : self.stock = stock; super.init()


You need to define encoding in the struct itself as well, with your code :

func encode(with aCoder: NSCoder) {

aCoder.encode(companyName, forKey: "companyName")

aCoder.encode(symbol, forKey: "symbol")

aCoder.encode(price, forKey: "price")

}

}


Maybe the simplest would be to replace directly struct by a class

First of all, your `saveStocksArray` needs to pass an `NSArray` containing `StockClass` to `NSKeyedArchiver.archiveRootObject(_:toFile:)`, not a single object of `StockClass`. You may need to take an Array as a method parameter.


So, any of your 3 methods in `Stock` does not depend on a single instance, which means they should be type methods rather than instance methods.


Second, you get an `NSArray` containing `StockClass` from `NSKeyedUnarchiver.unarchiveObject(withFile:)` in your `loadStocksArray`, you need to explicitly convert it to an Array of `Stock`, which `as?`-casting won't do.


Third, I do not prefer using `NSString` just for using `appendingPathComponent(_:)`. (Sorry, this is just a problem of preference, not a critical issue.)


All three things fixed as well as fixing missing line breaks (you'd better fix your post), you can write something like this:

import UIKit
import Foundation

enum Path: String { case Stock = "Stock" }

struct Stock {
    var companyName: String?
    var symbol: String?
    var price: Double?
}
extension Stock {
    class StockClass: NSObject, NSCoding {
        var stock: Stock
        init(stock: Stock) {
            self.stock = stock
            super.init()
        }
       
        required init?(coder aDecoder: NSCoder) {
            guard let companyName = aDecoder.decodeObject(forKey: "companyName") as? String else { return nil }
            guard let symbol = aDecoder.decodeObject(forKey: "symbol") as? String else { return nil }
            guard let price = aDecoder.decodeObject(forKey: "price") as? Double else { return nil }
            
            stock = Stock(companyName: companyName, symbol: symbol, price: price)
            super.init()
        }
       
        func encode(with aCoder: NSCoder) {
            aCoder.encode(stock.companyName, forKey: "companyName")
            aCoder.encode(stock.symbol, forKey: "symbol")
            aCoder.encode(stock.price, forKey: "price")
        }
    }
   
    static func documentsDirectoryURL() -> URL {
        let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        return urls[0]
    }
   
    static func saveStocksArray(stocksArray: [Stock]) -> Bool {
        let stockObjects = stocksArray.map{StockClass(stock: $0)}
        let file = documentsDirectoryURL().appendingPathComponent(Path.Stock.rawValue).path
        return NSKeyedArchiver.archiveRootObject(stockObjects, toFile: file)
    }
   
    static func loadStocksArray() -> [Stock]? {
        let file = documentsDirectoryURL().appendingPathComponent(Path.Stock.rawValue).path
        let result = NSKeyedUnarchiver.unarchiveObject(withFile: file)
        return (result as? [StockClass])?.map{$0.stock}
    }
}

let stocksArray = [
    Stock(companyName: "Tesla", symbol: "TSLA", price: 200.01),
    Stock(companyName: "Apple", symbol: "AAPL", price: 120.12)
]

if
    Stock.saveStocksArray(stocksArray: stocksArray),
    let unarchivedStocks = Stock.loadStocksArray()
{
    print(unarchivedStocks)
}
Swift - How to Encode & Decode Array of Struct Objects
 
 
Q