How to unarchive custom array with NSUserDefaults? - xcode

I'm saving an array of type ClassA to NSUserDefaults. ClassA look like this:
class ClassA :NSObject, NSCoding{
init (descriptionParam: String) {
self.description = descriptionParam
}
var description: String?
required init(coder aDecoder: NSCoder) {
if let description = aDecoder.decodeObjectForKey("description") as? String {
self.description = description
}
}
func encodeWithCoder(aCoder: NSCoder) {
if let description = self.description {
aCoder.encodeObject(description, forKey: "description")
}
}
}
This is how I'm saving the array to NSUserDefaults:
let myData = NSKeyedArchiver.archivedDataWithRootObject(ClassAManager.classa_array)
userDefaults.setObject(myData, forKey: "classAarray");
I'm doing the following in my initial viewDidLoad():
var classA: AnyObject? = NSUserDefaultsManager.userDefaults.objectForKey("classAarray") as AnyObject?
let classAunpacked = NSKeyedUnarchiver.unarchiveObjectWithData(classA) as [ClassA]
I get the following compile-time error on the second line above (the one with let):
Cannot invoke 'unarchiveObjectWithData' with an argument list of type '(AnyObject?)'
However, if I try to retrieve the array with anything other than AnyObject?, I get other compile time errors. It also seems I can't cast from AnyObject? to [ClassA]. Any ideas how this should be done?

unarchiveObjectWithData takes an NSData as it's sole argument, not an optional AnyObject. Since the result of unarchive... is also an optional, I'd suggest using:
if let classA = NSUserDefaultsManager.userDefaults.dataForKey("classAarray") {
if let classAunpacked = NSKeyedUnarchiver.unarchiveObjectWithData(classA) as? [ClassA] {
// Use classAunpacked here
}
}

Related

Storing/retriving a Codable Dictionary of structs in UserDefaults doesn't work for me

Swift (v 5/5.1) newbie here, having a hard time with Codables...hoping to get some advise from the experts here.
Okay, I have a simple dictionary from struct where the key is a string. I want to store the dictionary in UserDefaults (and later retrieve). There are some quite similar questions here, but these are mainly addressing nested struct's.
First attempt (error handling removed for simplicity):
public struct PriceStruct:Codable {
var myPrice: Double
var myTime: TimeInterval
var selected: Bool
var direction: Int
var myHigh, myLow: Double
enum CodingKeys: String, CodingKey {
case myPrice = "myPrice"
case myTime = "myTime"
case selected = "selected"
case direction = "direction"
case myHigh = "myHigh"
case myLow = "myLow"
}
}
var myPrices: [String: PriceStruct] = [:]
// [fill myPrices with some data...]
func savePrices() {
// error: Attempt to set a non-property-list object
UserDefaults.standard.set(myPrices, forKey: "prices")
}
func loadPrices() {
// obviously this doesn't work either
let myPrices = UserDefaults.standard.data(forKey: "prices")
}
While I assumed from the documentation, that UserDefaults is capable of storing dictionaries, it doesn't - at least for me.
Next thing I tried was using JSONEncoder like this:
// this time with prior JSON encoding
func savePrices() {
// this works
let json = try! JSONEncoder().encode(myPrices)
UserDefaults.standard.set(json as Data, forKey: "prices")
}
func loadPrices() {
// this doesn't work
let json = UserDefaults.standard.data(forKey: "prices")
let decoder = JSONDecoder()
let decoded = try! decoder.decode(PriceStruct.self, from json!)
}
Unfortunately I'm getting an error when trying to load data back from UserDefaults:
Swift.DecodingError.keyNotFound(CodingKeys(stringValue: "myPrice", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"myPrice\", intValue: nil) (\"myPrice\").", underlyingError: nil))
Other variants I tried is converting the encoded JSON to an UTF8 encoded string and storing/retrieving this one:
func savePrices() {
// this works too
let json = try! JSONEncoder().encode(myPrices)
UserDefaults.standard.set(String(data: json, encoding: .utf8), forKey: "prices")
}
func loadPrices() {
// and this doesn't work either
let json = UserDefaults.standard.string(forKey: "prices")!.data(using: .utf8)
}
So, from the error raised, CodingKeys seems to be the root of the problem. I tried to switch over using NSKeyedArchiver and NSKeyedUnarchiver` with no success.
I'm really wondering if there is a simple/universal solution to save/load a Dictionary in UserDefaults?
All your comments and suggestions are appreciated. Thanks!
I tried with the below code in my project that will work for me.
User Model
public protocol UserModel: Codable, PrimaryKey {
var id: String { get }
var firstName: String? { get }
var lastName: String? { get }
var userName: String? { get }
var emails: [String] { get }
}
public struct User: UserModel {
public let id: String
public let firstName: String?
public let lastName: String?
public let userName: String?
public let emails: [String]
public enum CodingKeys: String, CodingKey {
case id = "Id"
case firstName = "FirstName"
case lastName = "LastName"
case userName = "UserName"
case emails = "Emails"
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
self.id = try container.decode(String.self, forKey: .id)
self.firstName = try container.decodeIfPresent(String.self, forKey: .firstName)
self.lastName = try container.decodeIfPresent(String.self, forKey: .lastName)
self.userName = try container.decodeIfPresent(String.self, forKey: .userName)
self.emails = try container.decodeIfPresent([String].self, forKey: .emails) ?? []
}
catch let error {
debugPrint(error)
throw error
}
}
}
I have stored in userDefault using below way
User Data Class
class UserData: NSObject
{
let userDefaultKey = "user_information"
var userData: User?
func getDictionary() -> [String: Data]
{
var dicInfo = [String: Data]()
do
{
let _userData = try JSONEncoder().encode(userData)
dicInfo["userData_default"] = _userData
}catch let error{
print("error while save data in User Default: \(error.localizedDescription)")
}
return dicInfo
}
func saveToDefault()
{
let userDefault = UserDefaults.standard
userDefault.set(getDictionary(), forKey: userDefaultKey)
userDefault.synchronize()
}
func loadFromDefault()
{
let userDefault = UserDefaults.standard
if let dicInfo = userDefault.object(forKey: userDefaultKey) as? [String: Data]
{
update(dicInfo)
}
}
func update(_ dictionaryInfo: [String: Data])
{
do
{
if let _userData_data = dictionaryInfo["userData_default"]
{
if let _userData = try? JSONDecoder().decode(User.self, from: _userData_data) {
userData = _userData
}
}
saveToDefault()
}catch let error{
print("error while load From Default data in User Default: \(error.localizedDescription)")
}
}
}
Hope this will help you.

NSCoder crash on decodeBool forKey (Xcode 8, Swift 3)

I have this simple class
import UIKit
class SimpleModel: NSObject, NSCoding {
var name : String!
var done : Bool!
init(name:String) {
self.name = name
self.done = false
}
internal required init?(coder aDecoder: NSCoder) {
self.name = aDecoder.decodeObject(forKey: "name") as! String
self.done = aDecoder.decodeBool(forKey: "done") // BUG HERE
}
func encode(with encoder: NSCoder) {
encoder.encode(self.name, forKey: "name")
encoder.encode(self.done, forKey: "done")
}
}
the save code:
let data = NSKeyedArchiver.archivedData(withRootObject: storageArray)
UserDefaults.standard.set(data, forKey: "storage")
UserDefaults.standard.synchronize()
the read code:
if let data = UserDefaults.standard.data(forKey: "storage") {
storageArray = NSKeyedUnarchiver.unarchiveObject(with: data) as! [SimpleModel]
}
the problem occurs when the NSKeyedUnarchiver does it's job. I can not understand where the problem comes from.
Thanks!
the trick is remove ! form the primitive types.
If you put ! you are saying "make an implicit-unwrapped optional" so the encoder will archive as NSNumber instead of Bool (or Int, Double).
If you remove ! the encoder will archive as Bool and things works as expected (I spent an "incident" and this solution is provided by Apple)
Bool and Int have new methods:
self.x = Int(decoder.decodeCInt(forKey: "Speed"))
self.y = decoder.decodeObject(forKey: "Serial") as! String
self.z = Bool(decoder.decodeBool(forKey: "Direction") )
I had the same Problem.
Try this:
self.done = aDecoder.decodeObject(forKey: "done") as? Bool ?? aDecoder.decodeBool(forKey: "done")

Swift 3.0 NSFetchRequest error [duplicate]

In Swift 2 the following code was working:
let request = NSFetchRequest(entityName: String)
but in Swift 3 it gives error:
Generic parameter "ResultType" could not be inferred
because NSFetchRequest is now a generic type. In their documents they wrote this:
let request: NSFetchRequest<Animal> = Animal.fetchRequest
so if my result class is for example Level how should I request correctly?
Because this not working:
let request: NSFetchRequest<Level> = Level.fetchRequest
let request: NSFetchRequest<NSFetchRequestResult> = Level.fetchRequest()
or
let request: NSFetchRequest<Level> = Level.fetchRequest()
depending which version you want.
You have to specify the generic type because otherwise the method call is ambiguous.
The first version is defined for NSManagedObject, the second version is generated automatically for every object using an extension, e.g:
extension Level {
#nonobjc class func fetchRequest() -> NSFetchRequest<Level> {
return NSFetchRequest<Level>(entityName: "Level");
}
#NSManaged var timeStamp: NSDate?
}
The whole point is to remove the usage of String constants.
I think i got it working by doing this:
let request:NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Level")
at least it saves and loads data from DataBase.
But it feels like it is not a proper solution, but it works for now.
The simplest structure I found that works in 3.0 is as follows:
let request = NSFetchRequest<Country>(entityName: "Country")
where the data entity Type is Country.
When trying to create a Core Data BatchDeleteRequest, however, I found that this definition does not work and it seems that you'll need to go with the form:
let request: NSFetchRequest<NSFetchRequestResult> = Country.fetchRequest()
even though the ManagedObject and FetchRequestResult formats are supposed to be equivalent.
Here are some generic CoreData methods that might answer your question:
import Foundation
import Cocoa
func addRecord<T: NSManagedObject>(_ type : T.Type) -> T
{
let entityName = T.description()
let context = app.managedObjectContext
let entity = NSEntityDescription.entity(forEntityName: entityName, in: context)
let record = T(entity: entity!, insertInto: context)
return record
}
func recordsInTable<T: NSManagedObject>(_ type : T.Type) -> Int
{
let recs = allRecords(T.self)
return recs.count
}
func allRecords<T: NSManagedObject>(_ type : T.Type, sort: NSSortDescriptor? = nil) -> [T]
{
let context = app.managedObjectContext
let request = T.fetchRequest()
do
{
let results = try context.fetch(request)
return results as! [T]
}
catch
{
print("Error with request: \(error)")
return []
}
}
func query<T: NSManagedObject>(_ type : T.Type, search: NSPredicate?, sort: NSSortDescriptor? = nil, multiSort: [NSSortDescriptor]? = nil) -> [T]
{
let context = app.managedObjectContext
let request = T.fetchRequest()
if let predicate = search
{
request.predicate = predicate
}
if let sortDescriptors = multiSort
{
request.sortDescriptors = sortDescriptors
}
else if let sortDescriptor = sort
{
request.sortDescriptors = [sortDescriptor]
}
do
{
let results = try context.fetch(request)
return results as! [T]
}
catch
{
print("Error with request: \(error)")
return []
}
}
func deleteRecord(_ object: NSManagedObject)
{
let context = app.managedObjectContext
context.delete(object)
}
func deleteRecords<T: NSManagedObject>(_ type : T.Type, search: NSPredicate? = nil)
{
let context = app.managedObjectContext
let results = query(T.self, search: search)
for record in results
{
context.delete(record)
}
}
func saveDatabase()
{
let context = app.managedObjectContext
do
{
try context.save()
}
catch
{
print("Error saving database: \(error)")
}
}
Assuming that there is a NSManagedObject setup for Contact like this:
class Contact: NSManagedObject
{
#NSManaged var contactNo: Int
#NSManaged var contactName: String
}
These methods can be used in the following way:
let name = "John Appleseed"
let newContact = addRecord(Contact.self)
newContact.contactNo = 1
newContact.contactName = name
let contacts = query(Contact.self, search: NSPredicate(format: "contactName == %#", name))
for contact in contacts
{
print ("Contact name = \(contact.contactName), no = \(contact.contactNo)")
}
deleteRecords(Contact.self, search: NSPredicate(format: "contactName == %#", name))
recs = recordsInTable(Contact.self)
print ("Contacts table has \(recs) records")
saveDatabase()
This is the simplest way to migrate to Swift 3.0, just add <Country>
(tested and worked)
let request = NSFetchRequest<Country>(entityName: "Country")
Swift 3.0 This should work.
let request: NSFetchRequest<NSFetchRequestResult> = NSManagedObject.fetchRequest()
request.entity = entityDescription(context)
request.predicate = predicate
I also had "ResultType" could not be inferred errors. They cleared once I rebuilt the data model setting each entity's Codegen to "Class Definition". I did a brief writeup with step by step instructions here:
Looking for a clear tutorial on the revised NSPersistentContainer in Xcode 8 with Swift 3
By "rebuilt" I mean that I created a new model file with new entries and attributes. A little tedious, but it worked!
What worked best for me so far was:
let request = Level.fetchRequest() as! NSFetchRequest<Level>
I had the same issue and I solved it with the following steps:
Select your xcdatamodeld file and go to the Data Model Inspector
Select your first Entity and go to Section class
Make sure that Codegen "Class Definition" is selected.
Remove all your generated Entity files. You don't need them anymore.
After doing that I had to remove/rewrite all occurences of fetchRequest as XCode seem to somehow mix up with the codegenerated version.
HTH
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
func loadItemsCategory() {
let request: NSFetchRequest<Category> = Category.fetchRequest()
do {
categoryArray = try context.fetch(request)
} catch {
print(error)
}
tableView.reloadData()
}

Accessing PFObject Subclassed Properties from a Query

I am using Parse and have created a subclass of PFObject. When creating objects it makes things much easier. Once objects are created, I am experimenting with querying the database and accessing the custom properties I created. What I am finding is that I cannot use dot notation to access the properties when I am working the the PFObjects returned from the query. Is this normal?
Here is subclass I created.
import Foundation
import UIKit
import Parse
class MessagePFObject: PFObject
{
#NSManaged var messageSender : String
#NSManaged var messageReceiver : String
#NSManaged var messageMessage : String
#NSManaged var messageSeen : Bool
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// Custom Query method.
override class func query() -> PFQuery?
{
let query = PFQuery(className: MessagePFObject.parseClassName())
query.includeKey("user")
query.orderByDescending("createdAt")
return query
}
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
init(messageSenderInput: String?, messageReceiverInput: String?, messageMessageInput: String?)
{
super.init()
self.messageSender = messageSenderInput!
self.messageReceiver = messageReceiverInput!
self.messageMessage = messageMessageInput!
self.messageSeen = false
}
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
override init()
{
super.init()
}
}
//++++++++++++++++++++++++++++++++++++ EXTENSION +++++++++++++++++++++++++++++++++++++++++++++++++++
extension MessagePFObject : PFSubclassing
{
class func parseClassName() -> String
{
return "MessagePFObject"
}
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
override class func initialize()
{
var onceToken: dispatch_once_t = 0
dispatch_once(&onceToken) {
self.registerSubclass()
}
}
}
Here is my query and what I am required to do to access the properties. createdAt, updatedAt, etc are all available with dot notation but none of my custom properties are. You can see I access messageSeen with element.objectForKey("messageSeen").
let predicate = NSPredicate(format: "messageSender == %# OR messageReceiver == %#", self.currentUser!.username!, self.currentUser!.username!)
let query = messagePFObject.queryWithPredicate(predicate)
query!.findObjectsInBackgroundWithBlock ({ (objects, error) -> Void in
if error == nil && objects!.count > 0
{
for element in objects!
{
print(element)
print(element.parseClassName)
print(element.objectId)
print(element.createdAt)
print(element.updatedAt)
print(element.objectForKey("messageSeen"))
}
}
else if error != nil
{
print(error)
}
})
If this is normal then that is fine. I just want to make sure I am not missing something.
Take care,
Jon
Your object subclass has to implement the PFSubclassing protocol and you need to call MessagePFObject.registerSubclass() in your app delegate.
The parse documentation is very good : https://parse.com/docs/ios/guide#objects-subclasses

transfer array of custom objects with WatchConnectivity / encoding in Swift

Am trying to transfer an array of custom objects from iOS to watchkitextension.
Understood that in order to do so, data needs to be encoded. Am though getting error when decoding.
Here we go:
The custom object:
final class Person: NSObject {
var PersonName:String = ""
var PersonAge:Int = 0
var joined:NSDate = NSDate()
init(PersonName: String, PersonAge:Int, joined:NSDate){
self.PersonName = PersonName
self.PersonAge = PersonAge
self.joined = joined
super.init()
}
}
extension Person: NSCoding {
private struct CodingKeys {
static let PersonName = "PersonName"
static let PersonAge = "PersonAge"
static let joined = "joined"
}
convenience init(coder aDecoder: NSCoder) {
let PersonName = aDecoder.decodeObjectForKey(CodingKeys.PersonName) as! String
let PersonAge = aDecoder.decodeIntForKey(CodingKeys.PersonAge) as! Int
let joined = aDecoder.decodeDoubleForKey(CodingKeys.joined) as! NSDate
self.init(PersonName: PersonName, PersonAge: PersonAge, joined: joined)
}
func encodeWithCoder(encoder: NSCoder) {
encoder.encodeObject(PersonName, forKey: CodingKeys.PersonName)
encoder.encodeObject(PersonAge, forKey: CodingKeys.PersonAge)
encoder.encodeObject(joined, forKey: CodingKeys.joined)
}
}
The class with the array:
#objc(Group)
final class Group: NSObject {
static let sharedInstance = Group()
var Persons:[Person] = []
required override init() {
super.init()
}
init (Persons:[Person]){
self.Persons = Persons
super.init()
}
}
extension Group: NSCoding {
private struct CodingKeys {
static let Persons = "Persons"
}
convenience init(coder aDecoder: NSCoder) {
let Persons = aDecoder.decodeObjectForKey(CodingKeys.Persons) as! [Person]
self.init(Persons: Persons)
self.Persons = aDecoder.decodeObjectForKey(CodingKeys.Persons) as! [Person]
}
func encodeWithCoder(encoder: NSCoder) {
encoder.encodeObject(Persons, forKey: CodingKeys.Persons)
}
}
Creating example object, append to array, then encode:
let aPerson:Person? = Person(PersonName: "Martin", PersonAge: 50, joined: NSDate())
Group.sharedInstance.Persons.append(aPerson!)
let encodedData = NSKeyedArchiver.archivedDataWithRootObject(Group.sharedInstance)
And here I get the error "execution was interrupted - reason signal SIGABRT"
let decodedData = NSKeyedUnarchiver.unarchiveObjectWithData(encodedData) as? Group
Try changing these sections to:
convenience init(coder aDecoder: NSCoder) {
let PersonName = aDecoder.decodeObjectForKey(CodingKeys.PersonName) as! String
let PersonAge = aDecoder.decodeIntForKey(CodingKeys.PersonAge) as! Int
let joined = aDecoder.decodeObjectForKey(CodingKeys.joined) as! NSDate
self.init(PersonName: PersonName, PersonAge: PersonAge, joined: joined)
}
func encodeWithCoder(encoder: NSCoder) {
encoder.encodeObject(PersonName, forKey: CodingKeys.PersonName)
encoder.encodeInt(PersonAge, forKey: CodingKeys.PersonAge)
encoder.encodeObject(joined, forKey: CodingKeys.joined)
}
So that the serialization matches the deserialization, and the types desired by the Person class. That said, the WWDC talk on WatchConnectivity specifically recommended not to use NSKeyedArchiver as it is not a very space efficient serialization method.

Resources