Error while decoding array of custom objects from NSUserDefaults? - ios

I have an array of the custom object TemplateIndex, which I am trying to save and unsave to NSUserDefaults. But when I decode it, I get the following error:
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value
Here is my custom object:
class TemplateIndex: NSObject, NSCoding {
var identifier: String
var sectionNumber: Int
var indexNumber: Int
init(identifier: String, sectionNumber: Int, indexNumber: Int) {
self.identifier = identifier
self.sectionNumber = sectionNumber
self.indexNumber = indexNumber
}
required init?(coder aDecoder: NSCoder) {
self.identifier = aDecoder.decodeObject(forKey: "identifier") as! String
self.sectionNumber = aDecoder.decodeObject(forKey: "sectionNumber") as! Int
self.indexNumber = aDecoder.decodeObject(forKey: "indexNumber") as! Int
}
func encode(with aCoder: NSCoder) {
aCoder.encode(self.identifier, forKey: "identifier")
aCoder.encode(self.sectionNumber, forKey: "sectionNumber")
aCoder.encode(self.indexNumber, forKey: "indexNumber")
}
}
var favouriteTemplateIdentifiersArray: [TemplateIndex] = []
And here are my save and unsave functions:
func unarchiveFaveTemplates() {
guard let unarchivedObject = UserDefaults.standard.data(forKey: "faveTemplates") else {
return
}
guard let unarchivedFaveTemplates = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(unarchivedObject) else {
return
}
favouriteTemplateIdentifiersArray = unarchivedFaveTemplates as! [TemplateIndex]
print("array opened")
}
func saveFaveTemplates() {
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: favouriteTemplateIdentifiersArray, requiringSecureCoding: false)
UserDefaults.standard.set(data, forKey: "faveTemplates")
UserDefaults.standard.synchronize()
print("array saved")
} catch {
fatalError("can't encode data.")
}
}
Any help is appreciated, thankyou!
EDIT: Working Code
class TemplateIndex: Codable {
var identifier: String
var sectionNumber: Int
var indexNumber: Int
init(identifier: String, sectionNumber: Int, indexNumber: Int) {
self.identifier = identifier
self.sectionNumber = sectionNumber
self.indexNumber = indexNumber
}
}
func unarchiveFaveTemplates() {
if let data = UserDefaults.standard.value(forKey: "faveTemplates") as? Data,
let newArray = try? JSONDecoder().decode(Array<TemplateIndex>.self, from: data) {
print("opened")
favouriteTemplateIdentifiersArray = newArray
}
}
func saveFaveTemplates() {
if let data = try? JSONEncoder().encode(favouriteTemplateIdentifiersArray) {
UserDefaults.standard.set(data, forKey: "faveTemplates")
}
print("changes saved")
}

Forget about NSCoding and NSKeyedArchiver , you need to use Codable
struct TemplateIndex:Codable {
var identifier: String
var sectionNumber,indexNumber: Int
}
guard let data = UserDefaults.standard.data(forKey: "faveTemplates") else {
return
}
do {
let arr = try JSONDecoder().decode([TemplateIndex].self,from:data)
let data = try JSONEncoder().encode(arr)
UserDefaults.standard.set(data, forKey: "faveTemplates")
} catch {
print(error)
}

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

Facing issue while unarchivedObject data

I have created a generic function for NSKeyedArchiver and NSKeyedUnarchiver. I am able to archive the array data but while doing unarchive facing an issue. Below is my code:
NSKeyedArchiver code:
func cacheData<T>(data: T) {
do {
let codedData = try NSKeyedArchiver.archivedData(withRootObject: data, requiringSecureCoding: false)
} catch {
print("Exception while caching data \(error)")
}
}
NSKeyedUnarchiver code:
func getCacheData<T>(encodedData: Data, ofClass: T.Type) -> [T]? {
do{
if let decodedData = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSArray.self, T.self as! AnyClass], from: encodedData){
return decodedData as? [T]
}
} catch {
print("Exception while decode array cache data \(error)")
}
return nil
}
Above code works fine for having only strings, integers variables but it failed if having custom classes variables. How to allow these custom classes in NSKeyedUnarchiver.
I am getting below error:
Exception while decode array cache data Error
Domain=NSCocoaErrorDomain Code=4864 "value for key 'customclass1' was of
unexpected class 'CustomClass1'. Allowed classes are '{(
NSArray,
MainClass )}'." UserInfo={NSDebugDescription=value for key 'customclass2' was of unexpected class 'CustomClass2'. Allowed classes are
'{(
NSArray,
MainClass )}'.}
Any idea how to solve this?
Make sure all your class are confirming to NSCoding. Something like this:
func archiveAndUnarchive() {
let class2 = Class2(value: "Value")
let class1 = Class1(name: "Name", class2: class2)
do {
// ARCHIVING
let data = try NSKeyedArchiver.archivedData(withRootObject: class1, requiringSecureCoding: false)
// UNARCHIVING
if let decodedData = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? Class1 {
print(decodedData)
}
} catch {
print(error)
}
}
class Class1: NSObject, NSCoding {
var name: String?
var class2: Class2?
func encode(with coder: NSCoder) {
coder.encode(name, forKey: "name")
coder.encode(class2, forKey: "class2")
}
required init?(coder: NSCoder) {
super.init()
self.name = coder.decodeObject(forKey: "name") as? String ?? ""
self.class2 = coder.decodeObject(forKey: "class2") as? Class2
}
init(name: String, class2: Class2) {
super.init()
self.name = name
self.class2 = class2
}
}
class Class2: NSObject, NSCoding {
var value: String?
func encode(with coder: NSCoder) {
coder.encode(value, forKey: "value")
}
required init?(coder: NSCoder) {
super.init()
self.value = coder.decodeObject(forKey: "value") as? String
}
init(value: String) {
super.init()
self.value = value
}
}

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
}

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
}

Attempt to insert non-property list object when trying to save a custom object in Swift 3

I have a simple object which conforms to the NSCoding protocol.
import Foundation
class JobCategory: NSObject, NSCoding {
var id: Int
var name: String
var URLString: String
init(id: Int, name: String, URLString: String) {
self.id = id
self.name = name
self.URLString = URLString
}
// MARK: - NSCoding
required init(coder aDecoder: NSCoder) {
id = aDecoder.decodeObject(forKey: "id") as? Int ?? aDecoder.decodeInteger(forKey: "id")
name = aDecoder.decodeObject(forKey: "name") as! String
URLString = aDecoder.decodeObject(forKey: "URLString") as! String
}
func encode(with aCoder: NSCoder) {
aCoder.encode(id, forKey: "id")
aCoder.encode(name, forKey: "name")
aCoder.encode(URLString, forKey: "URLString")
}
}
I'm trying to save an instance of it in UserDefaults but it keeps failing with the following error.
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to insert non-property list object for key jobCategory'
This is the code where I'm saving in UserDefaults.
enum UserDefaultsKeys: String {
case jobCategory
}
class ViewController: UIViewController {
#IBAction func didTapSaveButton(_ sender: UIButton) {
let category = JobCategory(id: 1, name: "Test Category", URLString: "http://www.example-job.com")
let userDefaults = UserDefaults.standard
userDefaults.set(category, forKey: UserDefaultsKeys.jobCategory.rawValue)
userDefaults.synchronize()
}
}
I replaced the enum value to key with a normal string but the same error still occurs. Any idea what's causing this?
You need to create Data instance from your JobCategory model using JSONEncoder and store that Data instance in UserDefaults and later decode using JSONDecoder.
struct JobCategory: Codable {
let id: Int
let name: String
}
// To store in UserDefaults
if let encoded = try? JSONEncoder().encode(category) {
UserDefaults.standard.set(encoded, forKey: UserDefaultsKeys.jobCategory.rawValue)
}
// Retrieve from UserDefaults
if let data = UserDefaults.standard.object(forKey: UserDefaultsKeys.jobCategory.rawValue) as? Data,
let category = try? JSONDecoder().decode(JobCategory.self, from: data) {
print(category.name)
}
Old Answer
You need to create Data instance from your JobCategory instance using archivedData(withRootObject:) and store that Data instance in UserDefaults and later unarchive using unarchiveTopLevelObjectWithData(_:), So try like this.
For Storing data in UserDefaults
let category = JobCategory(id: 1, name: "Test Category", URLString: "http://www.example-job.com")
let encodedData = NSKeyedArchiver.archivedData(withRootObject: category, requiringSecureCoding: false)
let userDefaults = UserDefaults.standard
userDefaults.set(encodedData, forKey: UserDefaultsKeys.jobCategory.rawValue)
For retrieving data from UserDefaults
let decoded = UserDefaults.standard.object(forKey: UserDefaultsKeys.jobCategory.rawValue) as! Data
let decodedTeams = NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(decoded) as! JobCategory
print(decodedTeams.name)
Update Swift 4, Xcode 10
I have written a struct around it for easy access.
//set, get & remove User own profile in cache
struct UserProfileCache {
static let key = "userProfileCache"
static func save(_ value: Profile!) {
UserDefaults.standard.set(try? PropertyListEncoder().encode(value), forKey: key)
}
static func get() -> Profile! {
var userData: Profile!
if let data = UserDefaults.standard.value(forKey: key) as? Data {
userData = try? PropertyListDecoder().decode(Profile.self, from: data)
return userData!
} else {
return userData
}
}
static func remove() {
UserDefaults.standard.removeObject(forKey: key)
}
}
Profile is a Json encoded object.
struct Profile: Codable {
let id: Int!
let firstName: String
let dob: String!
}
Usage:
//save details in user defaults...
UserProfileCache.save(profileDetails)
Hope that helps!!!
Thanks
Swift save Codable object to UserDefault with #propertyWrapper
#propertyWrapper
struct UserDefault<T: Codable> {
let key: String
let defaultValue: T
init(_ key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
if let data = UserDefaults.standard.object(forKey: key) as? Data,
let user = try? JSONDecoder().decode(T.self, from: data) {
return user
}
return defaultValue
}
set {
if let encoded = try? JSONEncoder().encode(newValue) {
UserDefaults.standard.set(encoded, forKey: key)
}
}
}
}
enum GlobalSettings {
#UserDefault("user", defaultValue: User(name:"",pass:"")) static var user: User
}
Example User model confirm Codable
struct User:Codable {
let name:String
let pass:String
}
How to use it
//Set value
GlobalSettings.user = User(name: "Ahmed", pass: "Ahmed")
//GetValue
print(GlobalSettings.user)
Save dictionary Into userdefault
let data = NSKeyedArchiver.archivedData(withRootObject: DictionaryData)
UserDefaults.standard.set(data, forKey: kUserData)
Retrieving the dictionary
let outData = UserDefaults.standard.data(forKey: kUserData)
let dict = NSKeyedUnarchiver.unarchiveObject(with: outData!) as! NSDictionary
Based on Harjot Singh answer. I've used like this:
struct AppData {
static var myObject: MyObject? {
get {
if UserDefaults.standard.object(forKey: "UserLocationKey") != nil {
if let data = UserDefaults.standard.value(forKey: "UserLocationKey") as? Data {
let myObject = try? PropertyListDecoder().decode(MyObject.self, from: data)
return myObject!
}
}
return nil
}
set {
UserDefaults.standard.set(try? PropertyListEncoder().encode(newValue), forKey: "UserLocationKey")
}
}
}
Here's a UserDefaults extension to set and get a Codable object, and keep it human-readable in the plist (User Defaults) if you open it as a plain text file:
extension Encodable {
var asDictionary: [String: Any]? {
guard let data = try? JSONEncoder().encode(self) else { return nil }
return try? JSONSerialization.jsonObject(with: data) as? [String : Any]
}
}
extension Decodable {
init?(dictionary: [String: Any]) {
guard let data = try? JSONSerialization.data(withJSONObject: dictionary) else { return nil }
guard let object = try? JSONDecoder().decode(Self.self, from: data) else { return nil }
self = object
}
}
extension UserDefaults {
func setEncodableAsDictionary<T: Encodable>(_ encodable: T, for key: String) {
self.set(encodable.asDictionary, forKey: key)
}
func getDecodableFromDictionary<T: Decodable>(for key: String) -> T? {
guard let dictionary = self.dictionary(forKey: key) else {
return nil
}
return T(dictionary: dictionary)
}
}
If you want to also support array (of codables) to and from plist array, add the following to the extension:
extension UserDefaults {
func setEncodablesAsArrayOfDictionaries<T: Encodable>(_ encodables: Array<T>, for key: String) {
let arrayOfDictionaries = encodables.map({ $0.asDictionary })
self.set(arrayOfDictionaries, forKey: key)
}
func getDecodablesFromArrayOfDictionaries<T: Decodable>(for key: String) -> [T]? {
guard let arrayOfDictionaries = self.array(forKey: key) as? [[String: Any]] else {
return nil
}
return arrayOfDictionaries.compactMap({ T(dictionary: $0) })
}
}
If you don't care about plist being human-readable, it can be simply saved as Data (will look like random string if opened as plain text):
extension UserDefaults {
func setEncodable<T: Encodable>(_ encodable: T, for key: String) throws {
let data = try PropertyListEncoder().encode(encodable)
self.set(data, forKey: key)
}
func getDecodable<T: Decodable>(for key: String) -> T? {
guard
self.object(forKey: key) != nil,
let data = self.value(forKey: key) as? Data
else {
return nil
}
let obj = try? PropertyListDecoder().decode(T.self, from: data)
return obj
}
}
(With this second approach, you don't need the Encodable and Decodable extensions from the top)

Resources