How to add parameters to existing model when objects already saved? - ios

I have a Bill class which contains a few instances of bills that are saved in a plist file to the documents directory using NSKeyedArchiver.
class Bill: NSObject, NSCoding {
required init?(coder aDecoder: NSCoder) {
name = aDecoder.decodeObject(forKey: "Name") as! String
moPayment = aDecoder.decodeDouble(forKey: "Payment")
super.init()
}
override init() {
super.init()
}
var name = "Bill Name"
var moPayment = 0.0
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: "Name")
aCoder.encode(moPayment, forKey: "Payment")
}
}
func saveBillItems(_ bills: [Bill]) {
let data = NSMutableData()
let archiver = NSKeyedArchiver(forWritingWith: data)
archiver.encode(bills, forKey: "BillItems")
archiver.finishEncoding()
data.write(to: dataFilePath(), atomically: true)
}
func loadBillItems() {
let path = dataFilePath()
if let data = try? Data(contentsOf: path) {
let unarchiver = NSKeyedUnarchiver(forReadingWith: data)
bills = unarchiver.decodeObject(forKey: "BillItems") as! [Bill]
unarchiver.finishDecoding()
}
}
All of this works as expected but now I am trying to add an additional parameter to record paidStatus.
class Bill: NSObject, NSCoding {
required init?(coder aDecoder: NSCoder) {
...
status = aDecoder.decodeObject(forKey: "Status") as! PaidStatus
super.init()
}
...
var status = PaidStatus.unpaid
enum PaidStatus {
case overdue
case upcoming
case unpaid
case paid
}
...
func encode(with aCoder: NSCoder) {
...
aCoder.encode(status, forKey: "Status")
}
}
func saveBillItems(_ bills: [Bill]) {
...
}
func loadBillItems() {
...
}
When I try to run the app now, I get an error: "Unexpectedly found nil..."
status = aDecoder.decodeObject(forKey: "Status") as! PaidStatus
due to trying to load existing bill objects that don't have this parameter.
Is there a way to add this parameter to my existing objects without having to delete them and recreate them again from scratch?

Since your object may not have a value, you need to use an optional downcast, not a forced downcast. Since status is not an optional, you can use a nil coalescing operator to provide a default value
status = aDecoder.decodeObject(forKey: "Status") as? PaidStatus ?? .unpaid

Related

How to write set of classes to document directory using NSSecureCoding and NSKeyedArchiver?

I'm having trouble archiving and/or unarchiving (not sure where the problem is, exactly) a set of custom classes from the iOS documents directory. The set is saved to disk (or at least it appears to be saved) because I can pull it from disk but I cannot unarchive it.
The model
final class BlockedUser: NSObject, NSSecureCoding {
static var supportsSecureCoding = true
let userId: String
let name: String
let date: Int
var timeIntervalFormatted: String?
init(userId: String, name: String, date: Int) {
self.userId = userId
self.name = name
self.date = date
}
required convenience init?(coder: NSCoder) {
guard let userId = coder.decodeObject(forKey: "userId") as? String,
let name = coder.decodeObject(forKey: "name") as? String,
let date = coder.decodeObject(forKey: "date") as? Int else {
return nil
}
self.init(userId: userId, name: name, date: date)
}
func encode(with coder: NSCoder) {
coder.encode(userId, forKey: "userId")
coder.encode(name, forKey: "name")
coder.encode(date, forKey: "date")
}
}
Writing to disk
let fm = FileManager.default
let dox = fm.urls(for: .documentDirectory, in: .userDomainMask)[0]
let dir = dox.appendingPathComponent("master.properties", isDirectory: true)
do {
let userData: [URL: Any] = [
/* Everything else in this dictionary is a primitive type (string, bool, etc.)
and reads and writes without problem from disk. The only thing I cannot
get to work is the entry below (the set of custom classes). */
dir.appendingPathComponent("blockedUsers", isDirectory: false): blockedUsers // of type Set<BlockedUser>
]
for entry in userData {
let data = try NSKeyedArchiver.archivedData(withRootObject: entry.value, requiringSecureCoding: true)
try data.write(to: entry.key, options: [.atomic])
}
} catch {
print(error)
}
Reading from disk
if let onDisk = try? Data(contentsOf: dir.appendingPathComponent("blockedUsers", isDirectory: false)) {
if let blockedUsers = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(onDisk) as? Set<BlockedUser> {
print("success")
} else {
print("file found but cannot unarchive") // where I'm currently at
}
} else {
print("file not found")
}
The problem is that you are trying to decode an object instead of decoding an integer. Check this post. Try like this:
class BlockedUser: NSObject, NSSecureCoding {
static var supportsSecureCoding = true
let userId, name: String
let date: Int
var timeIntervalFormatted: String?
init(userId: String, name: String, date: Int) {
self.userId = userId
self.name = name
self.date = date
}
func encode(with coder: NSCoder) {
coder.encode(userId, forKey: "userId")
coder.encode(name, forKey: "name")
coder.encode(date, forKey: "date")
coder.encode(timeIntervalFormatted, forKey: "timeIntervalFormatted")
}
required init?(coder: NSCoder) {
userId = coder.decodeObject(forKey: "userId") as? String ?? ""
name = coder.decodeObject(forKey: "name") as? String ?? ""
date = coder.decodeInteger(forKey: "date")
timeIntervalFormatted = coder.decodeObject(forKey: "timeIntervalFormatted") as? String
}
}

Migrate to archivedData from archiveRootObject

I have two model swift files under below.
// Item.swift
import UIKit
class Item: NSObject, NSCoding {
var name: String
var valueInDollars: Int
var serialNumber: String?
let dateCreated: Date
let itemKey: String
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: "name")
aCoder.encode(dateCreated, forKey: "dateCreated")
aCoder.encode(itemKey, forKey: "itemKey")
aCoder.encode(serialNumber, forKey: "serialNumber")
aCoder.encode(valueInDollars, forKey: "valueInDollars")
}
required init(coder aDecoder: NSCoder) {
name = aDecoder.decodeObject(forKey: "name") as! String
dateCreated = aDecoder.decodeObject(forKey: "dateCreated") as! Date
itemKey = aDecoder.decodeObject(forKey: "itemKey") as! String
serialNumber = aDecoder.decodeObject(forKey: "serialNumber") as! String?
valueInDollars = aDecoder.decodeInteger(forKey: "valueInDollars")
super.init()
}
}
// ItemStore.swift
import UIKit
class ItemStore {
var allItems = [Item]()
let itemArchiveURL: URL = {
let documentsDirectories =
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let documentDirectory = documentsDirectories.first!
return documentDirectory.appendingPathComponent("items.archive")
}()
func saveChanges() -> Bool {
print("Saving items to: \(itemArchiveURL.path)")
return NSKeyedArchiver.archiveRootObject(allItems, toFile: itemArchiveURL.path)
}
}
These two model files confirming to NSCoding protocol and using archiveRootObject to archive the data.
But the archiveRootObject is deprecated, and the NSCoding is not as safe as the NSSecureCoding, how can I tweak the code to adjust all of these?
You can rewrite you saveChanges function to something like this:
func saveChanges() -> Bool {
print("Saving items to: \(itemArchiveURL.path)")
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: allItems, requiringSecureCoding: false)
try data.write(to: itemArchiveURL)
}
catch {
print("Error archiving data: \(error)")
return false
}
return true
}

Why does my data not unarchive when I call for it when opening my app

My data will not unarchive but it properly saves when the user is using the application. I have checked and I have used the same code before in other applications I have written.
func loadSurveys(){
guard let surveyData = UserDefaults.standard.object(forKey: "addedSurveys") as? NSData else {
print("No surveys found")
initSurveysArray()
return
}
guard let allSurveys = NSKeyedUnarchiver.unarchiveObject(with: surveyData as Data) as? addedSurveys else {
print("Could not unarchive")
return
}
surveys = allSurveys.surveys
}
Here is also where I save it,
func saveSurveys(){
let surveyData = NSKeyedArchiver.archivedData(withRootObject: surveys)
UserDefaults.standard.set(surveyData, forKey: "addedSurveys")
}
here is my addedSurveys class, and I have implemented NSCoding for all my classes that are referenced in there
class addedSurveys: NSObject, NSCoding{
var surveys = [survey]()
override init(){}
convenience init(survey : [survey])
{
self.init()
self.surveys = survey
}
required init?(coder aDecoder: NSCoder) {
self.surveys = aDecoder.decodeObject(forKey: "addedSurveys") as! [survey]
}
func encode(with aCoder: NSCoder) {
aCoder.encode(surveys, forKey: "addedSurveys")
}
}
Here is the Survey Object:
class survey: NSObject, NSCoding{
func encode(with aCoder: NSCoder) {
aCoder.encode(areas, forKey: "areas")
aCoder.encode(settings, forKey: "setting")
aCoder.encode(overview, forKey: "overview")
aCoder.encode(name, forKey: "name")
}
required init?(coder aDecoder: NSCoder) {
self.areas = aDecoder.decodeObject(forKey: "area") as? [areaProps] ?? [areaProps()]
self.settings = aDecoder.decodeObject(forKey: "settings") as? companySettings ?? companySettings()
self.overview = aDecoder.decodeObject(forKey: "overview") as? overviewProps ?? overviewProps()
self.name = aDecoder.decodeObject(forKey: "name") as? String ?? "ok"
}
var areas = [areaProps]()
var settings = companySettings(manager: "")
var overview = overviewProps(x: "", y: "", a: "", b: "", c: "", d: "", e: "", f: "", st: "", ct: "", stt: "")
var name = ""
override init() {
}
convenience init(areas: [areaProps], settings: companySettings, overview: overviewProps, name : String)
{
self.init()
self.areas = areas
self.settings = settings
self.overview = overview
self.name = name
}
}

iOS UI Test Does Not Recognize Items in NSUserDefaults

I am writing a demo app where I store items in NSUserDefaults. The iOS UI Test looks like this:
func test_should_create_account_successfully() {
app.textFields["usernameTextField"].tapAndType(text: "johndoe")
app.textFields["passwordTextField"].tapAndType(text: "password")
app.buttons["registerButton"].tap()
let users = dataAccess.getUsers()
XCTAssertTrue(users.count > 0)
}
The registerButton tap fired the following code:
#IBAction func saveButtonClicked() {
let user = User(username: self.usernameTextField.text!, password: self.passwordTextField.text!)
self.dataAccess.saveUser(user)
}
DataAccess class is defined below:
func saveUser(_ user:User) {
user.userId = UUID().uuidString
var users = getUsers()
users.append(user)
let usersData = NSKeyedArchiver.archivedData(withRootObject: users)
// save the user
let userDefaults = UserDefaults.standard
userDefaults.setValue(usersData, forKey: "users")
userDefaults.synchronize()
}
func getUsers() -> [User] {
let userDefaults = UserDefaults.standard
let usersData = userDefaults.value(forKey: "users") as? Data
if usersData == nil {
return [User]()
}
let users = NSKeyedUnarchiver.unarchiveObject(with: usersData!) as! [User]
return users
}
The problem is that the following line always return 0 users:
let users = dataAccess.getUsers()
This only happens in iOS UI Test and not in normal Unit Test target.
UPDATE: User class is NSCoding Protocol compatible
public class User : NSObject, NSCoding {
var username :String!
var password :String!
var userId :String!
init(username :String, password :String) {
self.username = username
self.password = password
}
public func encode(with aCoder: NSCoder) {
aCoder.encode(self.userId,forKey: "userId")
aCoder.encode(self.username, forKey: "username")
aCoder.encode(self.password, forKey: "password")
}
public required init?(coder aDecoder: NSCoder) {
self.userId = aDecoder.decodeObject(forKey: "userId") as! String
self.username = aDecoder.decodeObject(forKey: "username") as! String
self.password = aDecoder.decodeObject(forKey: "password") as! String
}
}
It is because your class User is not properly encoded do it like this sample class (in swift 3):
class User: NSObject, NSCoding {
let name : String
let url : String
let desc : String
init(tuple : (String,String,String)){
self.name = tuple.0
self.url = tuple.1
self.desc = tuple.2
}
func getName() -> String {
return name
}
func getURL() -> String{
return url
}
func getDescription() -> String {
return desc
}
func getTuple() -> (String, String, String) {
return (self.name,self.url,self.desc)
}
required init(coder aDecoder: NSCoder) {
self.name = aDecoder.decodeObject(forKey: "name") as? String ?? ""
self.url = aDecoder.decodeObject(forKey: "url") as? String ?? ""
self.desc = aDecoder.decodeObject(forKey: "desc") as? String ?? ""
}
func encode(with aCoder: NSCoder) {
aCoder.encode(self.name, forKey: "name")
aCoder.encode(self.url, forKey: "url")
aCoder.encode(self.desc, forKey: "desc")
}
}
then save and get it from UserDefaults like this:
func save() {
let data = NSKeyedArchiver.archivedData(withRootObject: object)
UserDefaults.standard.set(data, forKey:"userData" )
}
func get() -> MyObject? {
guard let data = UserDefaults.standard.object(forKey: "userData") as? Data else { return nil }
return NSKeyedUnarchiver.unarchiveObject(with: data) as? MyObject
}

Conforming Class to NSCoding

I conformed my following Person class to NSCoding protocol.
class Person: NSObject, NSCoding{
var age:Int
var height: Double
var name: String
init(age:Int, height: Double, name:String){
self.age = age
self.height = height
self.name = name
}
func encode(with aCoder: NSCoder){
aCoder.encode(age, forKey: "age")
aCoder.encode(height, forKey: "height")
aCoder.encode(name, forKey: "name")
}
required init?(coder aDecoder: NSCoder){
age = aDecoder.decodeObject(forKey: "age") as! Int **//error**
height = aDecoder.decodeObject(forKey: "height") as! Double
name = aDecoder.decodeObject(forKey: "name") as! String
super.init()
}
}
Then, I created an array of this class. I archived it to a plist using NSKeyedArchiver and everything was fine. However, when I tried to unarchive it I got an error involving unwrapping a optional which was nil. The error showed up in the Person class where marked. This is the code I used:
if let people = unarchive(){
print(people)
}
Here's the function for unarchiving:
func unarchive()->[Person]?{
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
let documentDirectory = paths[0]
let path = documentDirectory.appending("ObjectData.plist")
let fileManager = FileManager.default
if(!fileManager.fileExists(atPath: path)){
if let bundlePath = Bundle.main.path(forResource: "ObjectData", ofType: "plist"){
do{
try fileManager.copyItem(atPath: bundlePath, toPath: path)
}catch{
print("problem copying")
}
}
}
if let objects = NSKeyedUnarchiver.unarchiveObject(withFile: path){
if let people = objects as? [Person]{
return people
}else{
return nil
}
}else{
return nil
}
}
Int and Double are not archived as objects. aDecoder.decodeObject(forKey: <key>) for them will always return nil, and using as! with nil will crash your app.
So, instead use this:
aDecoder.decodeInteger(forKey: "age")
aDecoder.decodeDouble(forKey: "height")
For name field you may keep your code.
You should use the proper decoding methods for Int and Double. You could paste the following code in a playground and test it :
import Foundation
class Person: NSObject, NSCoding{
var age:Int
var height: Double
var name: String
init(age:Int, height: Double, name:String){
self.age = age
self.height = height
self.name = name
}
func encode(with aCoder: NSCoder) {
aCoder.encode(age, forKey: "age")
aCoder.encode(height, forKey: "height")
aCoder.encode(name, forKey: "name")
}
required init?(coder aDecoder: NSCoder) {
age = aDecoder.decodeInteger(forKey: "age")
height = aDecoder.decodeDouble(forKey: "height")
name = aDecoder.decodeObject(forKey: "name") as! String
super.init()
}
}
let john = Person(age: 30, height: 170, name: "John")
let mary = Person(age: 25, height: 140, name: "Mary")
let guys = [john, mary]
let data = NSKeyedArchiver.archivedData(withRootObject: guys)
let people = NSKeyedUnarchiver.unarchiveObject(with: data)
dump (people)

Resources