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
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'm having a modal entity file as below,
import UIKit
class MyProfile: NSObject {
var userName : String = ""
func initWithDict(dict: NSMutableDictionary) {
self.userName = dict.objectForKey("username") as! String
}
}
Saving that entity by encoding as below,
let myDict: NSMutableDictionary = ["username": "abc"]
let myEntity:MyProfile = MyProfile()
myEntity.initWithDict(myDict)
let userDefaults = NSUserDefaults.standardUserDefaults()
let encodedData = NSKeyedArchiver.archivedDataWithRootObject(myEntity)
userDefaults.setObject(encodedData, forKey: "MyProfileEntity")
userDefaults.synchronize()
Getting that saved entity as below,
let myEntity:MyProfile = MyProfile()
let userDefaults = NSUserDefaults.standardUserDefaults()
guard let decodedNSData = userDefaults.objectForKey("MyProfileEntity") as? NSData,
myEntity = NSKeyedUnarchiver.unarchiveObjectWithData(decodedNSData) as? MyProfile!
else {
print("Failed")
return
}
print(myEntity.userName)
It's not working, having crashes and lot of syntax errors, I'm new to swift,
It's showing some syntax errors like definition conflicts with previous value in the unarchiveObjectWithData line. If I fix that error, then at the time of getting the entity from userdefaults it's crashing.
can anyone suggest how can I resolve it?
To save custom object into user default, you must implement NSCoding protocol. Please replace your custom data model like this:
class MyProfile: NSObject,NSCoding {
var userName : String = ""
#objc required init(coder aDecoder:NSCoder){
self.userName = aDecoder.decodeObjectForKey("USER_NAME") as! String
}
#objc func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(self.userName, forKey: "USER_NAME")
}
init(dict: [String: String]) {
self.userName = dict["username"]!
}
override init() {
super.init()
}
}
Here is the code for saving and retrieving MyProfile object:
// Save profile
func saveProfile(profile: MyProfile){
let filename = NSHomeDirectory().stringByAppendingString("/Documents/profile.bin")
let data = NSKeyedArchiver.archivedDataWithRootObject(profile)
data.writeToFile(filename, atomically: true)
}
// Get profile
func getProfile() -> MyProfile?{
if let data = NSData(contentsOfFile: NSHomeDirectory().stringByAppendingString("/Documents/profile.bin")){
let unarchiveProfile = NSKeyedUnarchiver.unarchiveObjectWithData(data) as! MyProfile
return unarchiveProfile
} else{
return nil
}
}
Now here is the code snippet how to use those method:
// Create profile object
let profile = MyProfile(dict: ["username" : "MOHAMMAD"])
// save profile
saveProfile(profile)
// retrieve profile
if let myProfile = getProfile(){
print(myProfile.userName)
}else{
print("Profile not found")
}
You can't do this:
let myEntity:MyProfile = MyProfile()
Then later on, do this:
myEntity = ...
When something is defined with 'let', you cannot change it.
Change to
var myEntity: MyProfile?
It is possible that
NSKeyedUnarchiver.unarchiveObjectWithData(decodedNSData)
is returning nil. You then proceed to force unwrapping by adding
as? MyProfile!
try changing this to
as? MyProfile
Then later, see if you got something back
if let myEntity = myEntity {
print(myEntity.userName)
}
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)
}
}