I have the following struct definition:
struct ThreadManager: Equatable {
let fid: Int
let date: NSDate
let forumName: String
let typeid: Int
var page: Int
var threadList: [Thread]
var totalPageNumber: Int?
}
and the thread is :
struct Thread: Equatable {
let author: Author
let replyCount: Int
let readCount: Int
let title: String
let tid: Int
let isTopThread: Bool
var attributedStringDictionary: [String: NSAttributedString]
var postDescripiontTimeString: String
var hasRead: Bool
}
How can I encode a ThreadManager variable to NSData? I tried to used the following functions, but it does not worK.
func encode<T>(var value: T) -> NSData {
return withUnsafePointer(&value) { p in
NSData(bytes: p, length: sizeofValue(value))
}
}
func decode<T>(data: NSData) -> T {
let pointer = UnsafeMutablePointer<T>.alloc(sizeof(T))
data.getBytes(pointer, length: sizeof(T))
return pointer.move()
}
I have ThreadManager items, and I want to store them into sqlite. So I need to convert them to NSData. I have a variable called threadManager, the number of items in its threadList is about 70. I run the code and set a breakpoint, and input encode(threadManager) in xcode console, it is only 73bytes. It is wrong. How can I encode and decode those struct to NSData.
If your database is to be read on any other platform (Android, the web, wherever), you'd better choosing a cross-platform format such as JSON, or spread your struct members in their dedicated columns in a database table.
If you only target iOS/OSX/tvOS/etc, I recommend NSCoder. It is efficient, and most importantly:
NSCoder is platform-independant, which means that your NSData coding and decoding is not dependent on the particular memory layout currently used by the platform. For example, you don't have to fear 32 / 64 bits compatibility.
NSCoder lets you change your type over time, while keeping the ability to import old versions of your struct.
The code below adds a asData() function to your struct, and an init(data:) initializer. Those two let you go back and forth from your struct to NSData.
import Foundation
struct MyStruct {
let name: String
let date: NSDate
}
extension MyStruct {
init(data: NSData) {
let coding = NSKeyedUnarchiver.unarchiveObjectWithData(data) as! Coding
name = coding.name as String
date = coding.date
}
func asData() -> NSData {
return NSKeyedArchiver.archivedDataWithRootObject(Coding(self))
}
class Coding: NSObject, NSCoding {
let name: NSString
let date: NSDate
init(_ myStruct: MyStruct) {
name = myStruct.name
date = myStruct.date
}
required init?(coder aDecoder: NSCoder) {
self.name = aDecoder.decodeObjectForKey("name") as! NSString
self.date = aDecoder.decodeObjectForKey("date") as! NSDate
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(name, forKey: "name")
aCoder.encodeObject(date, forKey: "date")
}
}
}
let encodedS = MyStruct(name: "foo", date: NSDate())
let data = encodedS.asData()
let decodedS = MyStruct(data: data)
print(decodedS.name)
print(decodedS.date)
#Gwendal Roué : you are right, but I have to build another class according to each struct. I used the following method, it is ugly, but it works. Can you help me to improve it?
init(data: NSData) {
let dictionary = NSKeyedUnarchiver.unarchiveObjectWithData(data) as! NSDictionary
fid = (dictionary["fid"] as! NSNumber).integerValue
date = dictionary["date"] as! NSDate
forumName = dictionary["forumName"] as! String
typeid = (dictionary["typeid"] as! NSNumber).integerValue
page = (dictionary["page"] as! NSNumber).integerValue
totalPageNumber = (dictionary["totalPageNumber"] as? NSNumber)?.integerValue
let threadDataList = dictionary["threadDataList"] as! [NSData]
threadList = threadDataList.map { Thread(data: $0) }
}
extension ThreadManager {
func encode() -> NSData {
let dictionary = NSMutableDictionary()
dictionary.setObject(NSNumber(integer: fid), forKey: "fid")
dictionary.setObject(date, forKey: "date")
dictionary.setObject(forumName, forKey: "forumName")
dictionary.setObject(NSNumber(integer: typeid), forKey: "typeid")
dictionary.setObject(NSNumber(integer: page), forKey: "page")
if totalPageNumber != nil {
dictionary.setObject(NSNumber(integer: totalPageNumber!), forKey: "totalPageNumber")
}
let threadDataList: [NSData] = threadList.map { $0.encode() }
dictionary.setObject(threadDataList, forKey: "threadDataList")
return NSKeyedArchiver.archivedDataWithRootObject(dictionary)
}
}
Related
This is pretty simple but can't seem to find the correct information to solve saving an array like this in User Defaults.
It says it's not a property that NSUser Defaults Excepts.
Code:
var notificationList: [(type: String,imageName: String, text: String, date: String, seen: Bool)] = [(type: "Default",imageName: "ClearPartioned", text: "", date: "", seen: true)]
if (UserDefaults.standard.object(forKey: "notificationList")) == nil { // first time launching
print("making notification list")
UserDefaults.standard.set(notificationList, forKey: "notificationList")
UserDefaults.standard.synchronize()
print("\(notificationList)")
} else {
print("getting saved array")
notificationList = (UserDefaults.standard.object(forKey: "notificationList") as! [(type: String, imageName: String, text: String, date: String, seen: Bool)])
print("\(notificationList)")
}
Update:
This is closer but gives error found in this question here. These are the closet answers I have been able to find and there either out dated or crash the system.
Code:
if (UserDefaults.standard.object(forKey: "notificationList")) == nil { // first time launching
print("making notification list")
let encodedData = NSKeyedArchiver.archivedData(withRootObject: notificationList)
UserDefaults.standard.set(encodedData, forKey: "notificationList")
UserDefaults.standard.synchronize()
} else {
print("getting saved array")
notificationList = (UserDefaults.standard.object(forKey: "notificationList") as! [(type: String, imageName: String, text: String, date: String, seen: Bool)])
print("\(notificationList)")
}
Update 2: This is best answer implementation From Dhiru
Code:
if (UserDefaults.standard.object(forKey: "notificationList")) == nil { // first time launching
print("making notification list")
let notificationData = NSKeyedArchiver.archivedData(withRootObject: notificationList)
UserDefaults.standard.set(notificationData, forKey: "notificationList")
UserDefaults.standard.synchronize()
} else {
print("getting saved array")
let decodedData = UserDefaults.standard.object(forKey: "notificationList") as! Data
let notificationList = NSKeyedUnarchiver.unarchiveObject(with: decodedData) as AnyObject
print("\(notificationList)")
}
Its giving me an error that crashes system
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[_SwiftValue encodeWithCoder:]: unrecognized selector sent to instance 0x1c011f380'
libc++abi.dylib: terminating with uncaught exception of type NSException
Im sure this code would fix it but this is horribly implemented with multiple errors below because I have no clue how to use this code.
Code:
func (coder aDecoder: NSCoder) {
if let notificationList = aDecoder.decodeObjectForKey("notificationList") {
self.notificationList = notificationList
}
}
func encodeWithCoder(aCoder: NSCoder) {
if let notificationList = notificationList {
aCoder.encodeObject(notificationList, forKey: "notificationList")
}
}
You have to store your Object in form of Data
Convert into data using
NSKeyedArchiver.archivedData(withRootObject:)
Convert back to Object using
NSKeyedUnarchiver.unarchiveObject(with:)
Saving Data for UserDefaults
let notificationData = NSKeyedArchiver.archivedData(withRootObject: notificationList)
UserDefaults.standard.set(notificationData, forKey: "notificationList")
Retrive Data from User UserDefaults
let decodedData = UserDefaults.standard.object(forKey: "notificationList") as! Data
let notificationList = NSKeyedUnarchiver.unarchiveObject(with: decodedData) as! AnyObject
This is how I actually save a Custom Object created in the app in Swift 4.
First, we create 3 protocols for our purpose of saving the custom object in UserDefaults. The logic behind is to convert the Custom Object into a normalized Dictionary/Array form.
This can be applied to any kind of Object which you have created.
The 3 protocols are:
Decoder (Used to decode the dictionary into custom object)
Encoder (Used to encode the custom object into dictionary)
UserDefaultsProtocol (Used to save, delete, update & retrieve the custom object from UserDefault)
Decoder Protocol
protocol Decoder {
associatedtype T
static func decode(dictionary: [String: Any]) -> T
}
Encoder Protocol
protocol Encoder {
func encode() -> [String: Any]
}
UserDefaultsProtocol
protocol UserDefaultsDelegate: class {
associatedtype T
func saveToUserDefaults()
static func removeFromUserDefaults()
static func retrieveFromUserDefaults() -> T?
}
As per your question, NotificationList Object would look like this
class NotificationList {
var type: String = ""
var imageName: String = ""
var text: String = ""
var date: String = ""
var seen: Bool = false
}
Now, you need to confirm all the 3 mentioned protocols to NotificationList. (Swift Best Practice: Use of Extensions & Protocols)
class NotificationList {
private struct Constants {
static let RootKey = "notification_list"
static let TypeKey = "type"
static let ImageNameKey = "image_name"
static let TextKey = "text"
static let DateKey = "date"
static let SeenKey = "seen"
}
var type: String = ""
var imageName: String = ""
var text: String = ""
var date: String = ""
var seen: Bool = false
typealias T = NotificationList
}
extension NotificationList: Encoder {
func encode() -> [String : Any] {
return [
Constants.TypeKey: type,
Constants.ImageNameKey: imageName,
Constants.TextKey: text,
Constants.DateKey: date,
Constants.SeenKey: seen
]
}
}
extension NotificationList: Decoder {
static func decode(dictionary: [String: Any]) -> NotificationList {
let type = dictionary[Constants.TypeKey] as! String
let imageName = dictionary[Constants.ImageNameKey] as! String
let text = dictionary[Constants.TextKey] as! String
let date = dictionary[Constants.DateKey] as! String
let seen = dictionary[Constants.SeenKey] as! Bool
let notificationList = NotificationList()
notificationList.type = type
notificationList.imageName = imageName
notificationList.text = text
notificationList.date = date
notificationList.seen = seen
return notificationList
}
}
extension NotificationList: UserDefaultsDelegate {
func saveToUserDefaults() {
UserDefaults.standard.setValue(encode(), forKey: Constants.RootKey)
}
static func retrieveFromUserDefaults() -> NotificationList? {
guard let encodedNotificationList = UserDefaults.standard.dictionary(forKey: Constants.RootKey) else {
return nil
}
return NotificationList.decode(dictionary: encodedNotificationList)
}
static func removeFromUserDefaults() {
UserDefaults.standard.removeObject(forKey: Constants.RootKey)
}
}
How to save NotificationList to UserDefaults?
var notificationList = NotificationList()
notificationList.type = "Default"
notificationList.imageName = "ClearPartioned"
notificationList.text = ""
notificationList.date = ""
notificationList.seen = true
Save to UserDefaults
notificationList.saveToUserDefaults()
Retrieve from UserDefaults
if let notificationList = NotificationList.retrieveFromUserDefaults() {
// You will get the instance of notification list saved in UserDefaults
}
HOW TO SAVE ARRAY OF NOTIFICATION LIST?
Say notificationLists contains the array of notificationList objects.
var notificationListsArray = [[String: Any]]()
notificationLists.forEach {
notificationListsArray.append($0.encode())
}
Save that array of dictionary to UserDefaults
UserDefaults.standard.setValue(notificationListsArray, forValue: "notificationLists")
I am trying to add a key and a value to a dictionary then add this dictionary the user defaults and read back into a dictionary object. I have two questions that I would really appreciate any help in,
1) why is the dictionary being read from user defaults empty? Since I added a key and a value to the dictionary shouldn't those be saved to the dictionary I retrieve from user defaults?
let defaults = UserDefaults.standard;
var myDict = [String: String]()
myDict["key"] = "value"
defaults.setValue(myDict, forKey: "myDict")
let mydict2 = defaults.object(forKey: "myDict") as? [String: String] ?? [String:String]()
print(mydict2)
2) What can I do to this code if the dictionary stores a custom class that I created as a value or a key so if the dictionary was like this:
class Car {
var engineSize: Int
var color: String
init() {
engineSize = 2000
color = "blue"
}
}
class Boat {
var surfaceArea: Int
var weight: Int
init() {
surfaceArea = 3500
weight = 4000
}
}
var myDict = [Car: Boat]()
how can I save that second dict to user defaults and read it from there?
Thank you
EDIT:
This is the answer suggested by ebby94:
var myDict = [String:String]()
myDict["key"] = "value";
let data = NSKeyedArchiver.archivedData(withRootObject: myDict)
UserDefaults.standard.set(data, forKey: "myDict")
func foo()
{
guard let archivedData = UserDefaults.standard.value(forKey: "myDict") as? Data
else
{
print("failed1")
return
}
guard var unarchivedDictionary = NSKeyedUnarchiver.unarchiveObject(with: archivedData) as? [String:String]
else
{
print("failed2")
return
}
print(unarchivedDictionary["key"]!)
}
foo()
However this prints failed1, I'm assuming the data wasn't archived correctly. Can this be because I'm running it in playground?
If you want to save custom object to userDefault first you need to encode & decode variable then save using archive & get data using unarchive.
class Car {
var engineSize: Int
var color: String
init() {
engineSize = 2000
color = "blue"
}
// Decode
required convenience public init(coder decoder: NSCoder)
{
self.init()
if let engineSize = decoder.decodeObject(forKey: "engineSize") as? Int
{
self.engineSize = engineSize
}
if let color = decoder.decodeObject(forKey: "color") as? String
{
self.color = color
}
}
// Encode
func encodeWithCoder(coder : NSCoder)
{
if let engineSize = self.engineSize
{
coder.encode(engineSize, forKey: "engineSize")
}
if let color = self.color
{
coder.encode(color, forKey: "weight")
}
}
}
class Boat {
var surfaceArea: Int
var weight: Int
init() {
surfaceArea = 3500
weight = 4000
}
// Decode
required convenience public init(coder decoder: NSCoder)
{
self.init()
if let surfaceArea = decoder.decodeObject(forKey: "surfaceArea") as? Int
{
self.surfaceArea = surfaceArea
}
if let weight = decoder.decodeObject(forKey: "weight") as? Int
{
self.weight = weight
}
}
// Encode
func encodeWithCoder(coder : NSCoder)
{
if let surfaceArea = self.surfaceArea
{
coder.encode(surfaceArea, forKey: "surfaceArea")
}
if let weight = self.weight
{
coder.encode(weight, forKey: "weight")
}
}
You can't save a dictionary directly in UserDefaults. You'll have to convert the dictionary into data and save it and then retrieve the data and unarchive it into dictionary.
Archive and save to UserDefaults
let myDict = [String:String]()
let data = NSKeyedArchiver.archivedData(withRootObject: myDict)
UserDefaults.standard.set(data, forKey: "myDict")
Retrieve and unarchive the data to dictionary
guard let archivedData = UserDefaults.standard.value(forKey: "myDict") as? Data
else{return}
guard let unarchivedDictionary = NSKeyedUnarchiver.unarchiveObject(with: archivedData) as? [String:String]
else{return}
Userdefaults not work in Playground. you need to implement and start it in an App in Simulator
I am making an app that tracks a user's workouts. I have two custom classes, the first being ExerciseModel, which holds the data for each exercise performed during the workout, including the name, sets, reps, etc. Here is my data model:
import UIKit
class ExerciseModel: NSObject, NSCoding
{
// MARK: Properties
var name: String
var sets: Int
var reps: Int
var heartrate: Int?
var type: String?
//MARK: Archiving Paths
static let DocumentsDirectory = NSFileManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first!
static let ArchiveURL = DocumentsDirectory.URLByAppendingPathComponent("exercises")
// MARK: Initialization
init?(name: String, sets: Int, reps: Int, heartrate: Int?, type: String)
{
// MARK: Initlaize stored properties
self.name = name
self.sets = sets
self.reps = reps
self.heartrate = heartrate
self.type = type
super.init()
// Initialization should fail if there is no name or sets is negative
if name.isEmpty || sets < 0
{
return nil
}
}
struct PropertyKey
{
static let nameKey = "name"
static let setKey = "sets"
static let repKey = "reps"
static let heartrateKey = "heartrate"
static let typekey = "type"
}
// MARK: NSCoding
func encodeWithCoder(aCoder: NSCoder)
{
aCoder.encodeObject(name, forKey: PropertyKey.nameKey)
aCoder.encodeInteger(sets, forKey: PropertyKey.setKey)
aCoder.encodeInteger(reps, forKey: PropertyKey.repKey)
aCoder.encodeObject(type, forKey: PropertyKey.typekey)
}
required convenience init?(coder aDecoder: NSCoder)
{
let name = aDecoder.decodeObjectForKey(PropertyKey.nameKey) as! String
let sets = aDecoder.decodeIntegerForKey(PropertyKey.setKey)
let reps = aDecoder.decodeIntegerForKey(PropertyKey.repKey)
let heartrate = aDecoder.decodeIntegerForKey(PropertyKey.heartrateKey)
let type = aDecoder.decodeObjectForKey(PropertyKey.typekey) as? String
// Must call designated initializer
self.init(name: name, sets: sets, reps: reps, heartrate: heartrate, type: type!)
}
init?(name: String, sets: Int, reps: Int, heartrate: Int, type: String)
{
// Initialize stored properties.
self.name = name
self.sets = sets
self.reps = reps
self.heartrate = heartrate
self.type = type
}
}
My second custom class is called WorkoutStorage, and this is meant to allow the user to save entire workouts and retrieve them later. The exercise property is an array of ExerciseModel objects, described above. Here is my data model for WorkoutStorage:
//
import UIKit
#objc(WorkoutStorage)
class WorkoutStorage: NSObject, NSCoding
{
// MARK: Properties
var name: String
var date: NSDate
var exercises: [ExerciseModel]
var maxHR: Int
var avgHR: Int
// MARK: Archiving Paths
static let DocumentsDirectory = NSFileManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first!
static let ArchiveURL = DocumentsDirectory.URLByAppendingPathComponent("storedWorkouts")
// MARK: Initialization
init?(name: String, date: NSDate, exercises: [ExerciseModel], maxHR: Int, avgHR: Int)
{
//MARK: Initialize Stored Properties
self.name = name
self.date = date
self.exercises = exercises
self.maxHR = maxHR
self.avgHR = avgHR
super.init()
}
struct PropertyKey
{
static let nameKey = "name"
static let dateKey = "date"
static let exercisesKey = "exercises"
static let maxHRKey = "maxHR"
static let avgHRKey = "avgHR"
}
// MARK: NSCoding
func encodeWithCoder(aCoder: NSCoder)
{
aCoder.encodeObject(name, forKey: PropertyKey.nameKey)
aCoder.encodeObject(date, forKey: PropertyKey.dateKey)
aCoder.encodeObject(exercises, forKey: PropertyKey.exercisesKey)
aCoder.encodeInteger(maxHR, forKey: PropertyKey.maxHRKey)
aCoder.encodeInteger(avgHR, forKey: PropertyKey.avgHRKey)
}
required convenience init?(coder aDecoder: NSCoder)
{
let name = aDecoder.decodeObjectForKey(PropertyKey.nameKey) as! String
let date = aDecoder.decodeObjectForKey(PropertyKey.dateKey) as! NSDate
let exercises = aDecoder.decodeObjectForKey(PropertyKey.exercisesKey) as! [ExerciseModel]
let maxHR = aDecoder.decodeIntegerForKey(PropertyKey.maxHRKey)
let avgHR = aDecoder.decodeIntegerForKey(PropertyKey.avgHRKey)
// Must call designated initializer
self.init(name: name, date: date, exercises: exercises, maxHR: maxHR, avgHR: avgHR)
}
}
I followed the Apple tutorial for Persist Data to set up NSKeyedArchiver and NSKeyedUnarchiver for this, but I am still having trouble retrieving my data. When I try to load the Workouts, I call the following function:
func loadStoredWorkouts() -> WorkoutStorage
{
NSKeyedUnarchiver.setClass(WorkoutStorage.self, forClassName: "WorkoutStorage")
NSKeyedArchiver.setClassName("WorkoutStorage", forClass: WorkoutStorage.self)
print("\(WorkoutStorage.ArchiveURL.path!)")
return NSKeyedUnarchiver.unarchiveObjectWithFile(WorkoutStorage.ArchiveURL.path!) as! WorkoutStorage
}
Currently I can only return a single WorkoutStorage object, but when I attempt to retrieve an array containing all the stored WorkoutStorage objects, I get an error saying: Could not cast value of type 'Workout_Tracker.WorkoutStorage' (0x1000fcc80) to 'NSArray' (0x19f6b2418). I have read a lot of documentation trying to figure out why this will only return a single object, as well as checked out questions with similar issues, but to no avail. I originally set up my app following the Apple Persist Data tutorial to store and load my ExerciseModel objects, and that seems to work flawlessly. I set up the WorkoutStorage class the same way, but there seems to be an issue here.
Any help would be greatly appreciated!!
**Edit*
Here is the code I use to archive the WorkoutStorage object:
func saveWorkoutStorageObject(currentWorkout: WorkoutStorage)
{
NSKeyedUnarchiver.setClass(WorkoutStorage.self, forClassName: "WorkoutStorage")
NSKeyedArchiver.setClassName("WorkoutStorage", forClass: WorkoutStorage.self)
let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(currentWorkout, toFile: WorkoutStorage.ArchiveURL.path!)
if !isSuccessfulSave
{
print("Failed to save exercises")
}
if isSuccessfulSave
{
print("Successful save of current workout: \(currentWorkout)")
}
}
Workouts are only created one at a time by the user, so each time one is completed, I pass the object to the above function to archive it.
To unarchive all the objects, I was trying to do something along the lines of:
var workouts = [WorkoutStorage]()
override func viewDidLoad()
{
super.viewDidLoad()
workouts = loadStoredWorkouts()
}
where the loadStoredWorkouts() function would be:
func loadStoredWorkouts() -> [WorkoutStorage]
{
NSKeyedUnarchiver.setClass(WorkoutStorage.self, forClassName: "WorkoutStorage")
NSKeyedArchiver.setClassName("WorkoutStorage", forClass: WorkoutStorage.self)
print("\(WorkoutStorage.ArchiveURL.path!)")
return NSKeyedUnarchiver.unarchiveObjectWithFile(WorkoutStorage.ArchiveURL.path!) as! [WorkoutStorage]
}
Your saveWorkoutStorageObject only archives a single workout. It doesn't archive the array, so of course you can't unarchive an array.
You need to archive the workouts array if you want to be able to unarchive an array.
Each time you archive something to a file you replace the contents of the file. It doesn't append to the end.
Since NSKeyedArchiver.archiveRootObject automatically archives child objects, all you need to do is archive the array and your WorkoutStorage objects will be archived automagically
func saveWorkouts(workouts:[WorkoutStorage])
{
let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(workouts, toFile: WorkoutStorage.ArchiveURL.path!)
if isSuccessfulSave
{
print("Successful save of workouts: \(workouts)")
} else {
print("Failed to save exercises")
}
}
I have a class to handle a simple note creator in my app. At the moment, notes are stored using an array of custom Note objects. How can I save the contents of this array when the app closes and load them again when the app is re-opened? I've tried NSUserDefaults, but I can't figure out how to save the array since it isn't just comprised of Strings.
Code:
Note.swift
class Note {
var contents: String
// an automatically generated note title, based on the first line of the note
var title: String {
// split into lines
let lines = contents.componentsSeparatedByCharactersInSet(NSCharacterSet.newlineCharacterSet()) as [String]
// return the first
return lines[0]
}
init(text: String) {
contents = text
}
}
var notes = [
Note(text: "Contents of note"),]
There are different approaches to this.
NSCoding
The easiest would be to adopt NSCoding, let Note inherit from NSObject and use NSKeyedArchiver and NSKeyedUnarchiver to write to/from files in the app's sandbox.
Here is a trivial example for this:
final class Feedback : NSObject, NSCoding {
private static let documentsPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0]
let content : String
let entry : EntryId
let positive : Bool
let date : NSDate
init(content: String, entry: EntryId, positive : Bool, date :NSDate = NSDate()) {
self.content = content
self.entry = entry
self.positive = positive
self.date = date
super.init()
}
#objc init?(coder: NSCoder) {
if let c = coder.decodeObjectForKey("content") as? String,
let d = coder.decodeObjectForKey("date") as? NSDate {
let e = coder.decodeInt32ForKey("entry")
let p = coder.decodeBoolForKey("positive")
self.content = c
self.entry = e
self.positive = p
self.date = d
}
else {
content = ""
entry = -1
positive = false
date = NSDate()
}
super.init()
if self.entry == -1 {
return nil
}
}
#objc func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeBool(self.positive, forKey: "positive")
aCoder.encodeInt32(self.entry, forKey: "entry")
aCoder.encodeObject(content, forKey: "content")
aCoder.encodeObject(date, forKey: "date")
}
static func feedbackForEntry(entry: EntryId) -> Feedback? {
let path = Feedback.documentsPath.stringByAppendingString("/\(entry).feedbackData")
if let success = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as? Feedback {
return success
}
else {
return nil
}
}
func save() {
let path = Feedback.documentsPath.stringByAppendingString("/\(entry).feedbackData")
let s = NSKeyedArchiver.archiveRootObject(self, toFile: path)
if !s {
debugPrint("Warning: did not save a Feedback for \(self.entry): \"\(self.content)\"")
}
}
}
Core Data
The more efficient but more complex solution is using Core Data, Apple's ORM-Framework - which's usage is way beyond the scope of a SO answer.
Further Reading
NSHipster article
Archiving programming guide
Core Data programming guide
Here is my code for a Game object. It has plenty of different properties, one of which is an array of Cards. When I try to use NSCoding to save my game objects, XCode throws this error
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Hearthstone_Tracker.Card encodeWithCoder:]: unrecognized selector sent to instance 0x7fbca9db9750'
From what I've read, these errors happen oftentimes when an UIButton calls a function and passes itself as an argument when the function doesn't expect it or vice versa. I don't see how it's applicable to my case though.
I've read NSCoding has some problem with structs, but as Cards is actually a class, I don't think the problem lies there either.
Here is my Game class:
import Foundation
import UIKit
class Game: NSObject, NSCoding {
// MARK: Properties
let playerHero: String
let playerDeck: String
let playerImage: UIImage?
let opponentHero: String
let opponentDeck: String
let opponentImage: UIImage?
let result: String
let coin: Bool
var date: String?
let mode: String
let rank: String?
let duration: Int?
let durationSec: Int?
let id: Int
let cardsPlayed: [Card]
var manaEfficiency: (Double, Double)?
var turnCount: Int?
var estimatedTurnLength: Int?
var finisher: String?
// MARK: Keys for the data
struct PropertyKey {
static let playerHero = "playerHero"
static let playerDeck = "playerDeck"
static let playerImage = "playerImage"
static let opponentHero = "opponentHero"
static let opponentDeck = "opponentDeck"
static let opponentImage = "opponntImage"
static let result = "result"
static let coin = "coin"
static let date = "date"
static let mode = "mode"
static let rank = "rank"
static let duration = "duration"
static let durationSec = "durationSec"
static let id = "id"
static let cardsPlayed = "cardsPlayed"
}
// MARK: Archiving Paths
static let DocumentsDirectory: AnyObject = NSFileManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first!
static let ArchiveURL = DocumentsDirectory.URLByAppendingPathComponent("games")
// MARK: Initializer
init(playerHero: String, playerDeck: String, opponentHero: String, opponentDeck: String, result: String, coin: Bool, date: String, mode: String, rank: String, duration: Int?, durationSec: Int?, id: Int, cardsPlayed: [Card]) {
// ...
// Some initializer code
// ...
self.cardsPlayed = cardsPlayed
super.init()
}
// MARK: NSCoding
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(playerHero, forKey: PropertyKey.playerHero)
aCoder.encodeObject(playerDeck, forKey: PropertyKey.playerDeck)
aCoder.encodeObject(opponentHero, forKey: PropertyKey.opponentHero)
aCoder.encodeObject(opponentDeck, forKey: PropertyKey.opponentDeck)
aCoder.encodeObject(result, forKey: PropertyKey.result)
aCoder.encodeBool(coin, forKey: PropertyKey.coin)
aCoder.encodeObject(date, forKey: PropertyKey.date)
aCoder.encodeObject(mode, forKey: PropertyKey.mode)
aCoder.encodeObject(rank, forKey: PropertyKey.rank)
aCoder.encodeObject(duration, forKey: PropertyKey.duration)
aCoder.encodeObject(durationSec, forKey: PropertyKey.durationSec)
aCoder.encodeObject(id, forKey: PropertyKey.id)
aCoder.encodeObject(cardsPlayed, forKey: PropertyKey.cardsPlayed)
}
required convenience init?(coder aDecoder: NSCoder) {
let playerHero = aDecoder.decodeObjectForKey(PropertyKey.playerHero) as! String
let playerDeck = aDecoder.decodeObjectForKey(PropertyKey.playerDeck) as! String
let opponentHero = aDecoder.decodeObjectForKey(PropertyKey.opponentHero) as! String
let opponentDeck = aDecoder.decodeObjectForKey(PropertyKey.opponentDeck) as! String
let result = aDecoder.decodeObjectForKey(PropertyKey.result) as! String
let coin = aDecoder.decodeBoolForKey(PropertyKey.coin)
let date = aDecoder.decodeObjectForKey(PropertyKey.date) as! String
let mode = aDecoder.decodeObjectForKey(PropertyKey.mode) as! String
let rank = aDecoder.decodeObjectForKey(PropertyKey.rank) as! String
let duration = aDecoder.decodeObjectForKey(PropertyKey.duration) as? Int
let durationSec = aDecoder.decodeObjectForKey(PropertyKey.durationSec) as? Int
let id = aDecoder.decodeObjectForKey(PropertyKey.id) as! Int
let cardsPlayed = aDecoder.decodeObjectForKey(PropertyKey.cardsPlayed) as! [Card]
self.init(playerHero: playerHero, playerDeck: playerDeck, opponentHero: opponentHero, opponentDeck: opponentDeck, result: result, coin: coin, date: date, mode: mode, rank: rank, duration: duration, durationSec: durationSec, id: id, cardsPlayed: cardsPlayed)
}
And here is Card class:
import Foundation
import UIKit
class Card: NSObject {
// MARK: Properties
let name: String
let manacost: Int
let turn: Int
let player: String
// MARK: Initialzer
init(name: String, manacost: Int, turn: Int, player: String) {
self.name = name
self.manacost = manacost
self.turn = turn
self.player = player
super.init()
}
}
Every object (and its internal hierarchy of subobjects) to be able to be stored in NSDefaults must be able to serialise itself ...that is.. must conform to NSCoding.