Migrate to archivedData from archiveRootObject - ios

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
}

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

Error while decoding array of custom objects from NSUserDefaults?

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

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
}

In swift how can I store value of NSUserdefaults which need to store in local database or text file

I am using userdefaults to save data in defaults and now I want to save/transfer that userdefaults data in one text file. Is it possible and if "yes" then how?
Thank you for help and appreciation.
It would work like this example below. You can test it in the Playground:
import UIKit
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
let PersonKey = "PersonKey"
let textFileName = "textFile.txt"
UserDefaults.standard.removeObject(forKey: PersonKey)
class Person: NSObject, NSCoding {
let name: String
let age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
required init(coder decoder: NSCoder) {
self.name = decoder.decodeObject(forKey: "name") as? String ?? ""
self.age = decoder.decodeInteger(forKey: "age")
}
func encode(with coder: NSCoder) {
coder.encode(name, forKey: "name")
coder.encode(age, forKey: "age")
}
func saveToFile() {
let content = "\(name) \(age)"
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
let documentsDirectory = paths[0]
let fileName = "\(documentsDirectory)/" + textFileName
do {
try content.write(toFile: fileName, atomically: true, encoding: .utf8)
} catch {
print(error)
}
}
static func loadDataFromFile() -> String {
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
let documentsDirectory = paths[0]
let fileName = "\(documentsDirectory)/" + textFileName
let content: String
do{
content = try String(contentsOfFile: fileName, encoding: .utf8)
}catch _{
content = ""
}
return content;
}
}
let person = Person(name: "Bob", age: 33)
let ud = UserDefaults.standard
if ud.object(forKey: PersonKey) == nil {
print("Missing person in UD")
}
let encodedData = NSKeyedArchiver.archivedData(withRootObject: person)
ud.set(encodedData, forKey: PersonKey)
if let data = ud.data(forKey: PersonKey) {
print("Person data exist")
let unarchivedPerson = NSKeyedUnarchiver.unarchiveObject(with: data) as? Person
unarchivedPerson?.saveToFile()
}
let t = DispatchQueue.main.asyncAfter(deadline: .now() + 2.0, execute: {
print("Data from file: ", Person.loadDataFromFile())
})

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