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
}
Related
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
}
}
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
}
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
}
}
I was going to use Core Data but thought this was easier but can't find anything online about how to store an array of Cell class as a variable in the Cell class itself.
class Cell: NSObject, NSCoding {
var name:String!
var tag:String?
var more:String?
var amount:String!
var balance:String!
var subCells:[Cell]?//THIS IS THE ONE I'M STUGGLING WITH
override init() {
super.init()
}
init(name: String, tag: String, more: String, amount: String, balance: String, subCells: [Cell] = [Cell]()) {
super.init()
self.name = name
self.tag = tag
self.more = more
self.amount = amount
self.balance = balance
self.subCells = subCells//THIS
}
required init(coder decoder: NSCoder) {
self.name = decoder.decodeObject(forKey: "name") as! String
self.tag = decoder.decodeObject(forKey: "tag") as? String
self.more = decoder.decodeObject(forKey: "more") as? String
self.amount = decoder.decodeObject(forKey: "amount") as! String
self.balance = decoder.decodeObject(forKey: "balance") as! String
self.subCells = decoder.decodeObject(forKey: "subCells") as? [Cell]//THIS
}
func initWithCoder(decoder: NSCoder) -> Cell{
self.name = decoder.decodeObject(forKey: "name") as! String
self.tag = decoder.decodeObject(forKey: "tag") as? String
self.more = decoder.decodeObject(forKey: "more") as? String
self.amount = decoder.decodeObject(forKey: "amount") as! String
self.balance = decoder.decodeObject(forKey: "balance") as! String
self.subCells = decoder.decodeObject(forKey: "subCells") as? [Cell]//THIS
return self
}
func encode(with aCoder: NSCoder) {
aCoder.encode(self.name, forKey: "name")
aCoder.encode(self.tag, forKey: "tag")
aCoder.encode(self.more, forKey: "more")
aCoder.encode(self.amount, forKey: "amount")
aCoder.encode(self.balance, forKey: "balance")
aCoder.encode(self.subCells, forKey: "subCell")//THIS
}
}
Am I forced to use core data to solve this or can I encode the array before encoding the whole class in the viewdidload()
ok i figured it out
required init(coder decoder: NSCoder) {
self.name = decoder.decodeObject(forKey: "name") as! String
self.tag = decoder.decodeObject(forKey: "tag") as? String
self.more = decoder.decodeObject(forKey: "more") as? String
self.amount = decoder.decodeObject(forKey: "amount") as! String
self.balance = decoder.decodeObject(forKey: "balance") as! String
self.subCells = NSKeyedUnarchiver.unarchiveObject(with: (decoder.decodeObject(forKey: "subCells") as! NSData) as Data) as? [Cell]
}
func encode(with aCoder: NSCoder) {
aCoder.encode(self.name, forKey: "name")
aCoder.encode(self.tag, forKey: "tag")
aCoder.encode(self.more, forKey: "more")
aCoder.encode(self.amount, forKey: "amount")
aCoder.encode(self.balance, forKey: "balance")
aCoder.encode(NSKeyedArchiver.archivedData(withRootObject: self.subCells), forKey: "subCells")
}
I have a model class User that I want to save in UserDefaults
import UIKit
class User: NSObject {
var name:String!
var email:String!
var userId:String!
var phone:String!
var admin_status:String!
var social_code:String!
var token:String!
var otp:String!
var forget_otp:String!
var p_img:String!
var created:String!
var status:String!
static var currentUser:User = User()
override init() {
super.init()
}
required init(coder aDecoder: NSCoder) {
self.name = aDecoder.decodeObject(forKey: "name") as! String
self.email = aDecoder.decodeObject(forKey: "email") as! String
self.userId = aDecoder.decodeObject(forKey: "userId") as! String
self.phone = aDecoder.decodeObject(forKey: "phone") as! String
self.admin_status = aDecoder.decodeObject(forKey: "admin_status") as! String
self.social_code = aDecoder.decodeObject(forKey: "social_code") as! String
self.token = aDecoder.decodeObject(forKey: "token") as! String
self.otp = aDecoder.decodeObject(forKey: "otp") as! String
self.forget_otp = aDecoder.decodeObject(forKey: "forget_otp") as! String
self.p_img = aDecoder.decodeObject(forKey: "p_img") as! String
self.created = aDecoder.decodeObject(forKey: "created") as! String
self.status = aDecoder.decodeObject(forKey: "status") as! String
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encode(name, forKey: "name")
aCoder.encode(email, forKey: "email")
aCoder.encode(userId, forKey: "userId")
aCoder.encode(phone, forKey: "phone")
aCoder.encode(admin_status, forKey: "admin_status")
aCoder.encode(social_code, forKey: "social_code")
aCoder.encode(token, forKey: "token")
aCoder.encode(otp, forKey: "otp")
aCoder.encode(forget_otp, forKey: "forget_otp")
aCoder.encode(p_img, forKey: "p_img")
aCoder.encode(created, forKey: "created")
aCoder.encode(status, forKey: "status")
}
}
Code to save and get from UserDefaults
class func setUserDefault(ObjectToSave : AnyObject? , KeyToSave : String)
{
let defaults = UserDefaults.standard
if (ObjectToSave != nil)
{
defaults.set(ObjectToSave, forKey: KeyToSave)
}
UserDefaults.standard.synchronize()
}
class func getUserDefault(KeyToReturnValye : String) -> AnyObject?
{
let defaults = UserDefaults.standard
if let name = defaults.value(forKey: KeyToReturnValye)
{
return name as AnyObject
}
return nil
}
Saving
let user:User = User()
user.name = json["data"]["first_name"].string
user.email = json["data"]["email"].string
user.phone = json["data"]["phone"].string
user.social_code = json["data"]["social_code"].string
user.admin_status = json["data"]["admin_status"].string
user.otp = json["data"]["otp"].string
user.forget_otp = json["data"]["forget_otp"].string
user.p_img = json["data"]["p_img"].string
user.status = json["data"]["status"].string
user.userId = json["data"]["id"].string
user.created = json["data"]["created"].string
Utilities.setUserDefault(ObjectToSave: user, KeyToSave: "user")
I also tried this
let encodedData = NSKeyedArchiver.archivedData(withRootObject: user)
UserDefaults.standard.set(encodedData, forKey: "User")
but it crashes because of static var currentUser:User = User()
how to fix this ?
As for NSCoding: you have wrong method signature. Change
func encodeWithCoder(aCoder: NSCoder)
to
func encode(with aCoder: NSCoder)
Also, your object should explicitly conform to the NSCoding protocol:
class User: NSObject, NSCoding {...}
As for UserDefaults, you can not store custom objects in the UserDefaults, only NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary (or similar types in Swift), see documentation
Have you tried by replacing your this line,
static var currentUser:User = User()
with this line,
static var currentUser:User!