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")
}
}
Related
I was unable to find a suitable community to post in, so I am sorry if this is off-topic. I am having trouble saving a custom class in swift to userdefaults. Every other answer I have seen requires initializing the class with arguments but I am looking for a way around that when encoding. I also wonder if userdefaults is the best choice? It is a large amount of data but I am trying to avoid using a relational database because I am just trying to save this data structure directly without creating a schema. It produces an error when adding mediations to the mediation object array and then trying to encode the data.
My code:
import Foundation
class SavedData: NSObject, NSCoding {
func encode(with aCoder: NSCoder) {
aCoder.encode(mediations, forKey: "mediations")
aCoder.encode(name, forKey: "name")
}
required convenience init?(coder aDecoder: NSCoder) {
let name = aDecoder.decodeObject(forKey: "name") as! String
let mediations = aDecoder.decodeObject(forKey: "mediations") as! [Mediation]
self.init(name: name, mediations: mediations)
}
init(name: String, mediations: [Mediation]) {
// Get Saved Mediations from memory
self.mediations = mediations
self.name = name
}
public var mediations: [Mediation]
var name: String = "foo"
class Mediation {
init(name: String, role: String, data: [[String]]) {
self.name = name
self.data = Data(defendant: data[0], plaintiff: data[1])
}
var role: String = ""
var name: String = ""
var data: Data
class Data {
init(defendant: [String], plaintiff: [String]) {
self.defendant = defendant
self.plaintiff = plaintiff
}
var plaintiff: [String] = []
var defendant: [String] = []
}
}
func new_mediation (name: String, role: String, data: [[String]]) {
let mediation = Mediation(name: name, role: role, data: data)
self.mediations.append(mediation)
}
}
My favourite way to save array of custom class to UserDefaults is to use JSONEncoder.
So make sure your Mediation is Codable by:
Class Mediation: Codable
Then to save the array:
let encodedMediations = try! JSONEncoder().encode(mediations)
UserDefaults.standard.set(encodedMediations, forKey: "SavedMediations")
Finally to fetch the array:
if let savedMediations = UserDefaults.standard.data(forKey: "SavedMediations") {
let decodedMediations = try! JSONDecoder().decode([Mediation].self, from: savedMediations)
}
this Struct is work in swift 2
I have a Swift 3 struct like this.
let tempContacts = NSMutableArray()
let arrayOfArray = NSMutableArray()
I have encode The Person Object in this for loop
for person in tempContacts as! [Person] {
let encodedObject: Data = NSKeyedArchiver.archivedData(withRootObject: person) as Data
arrayOfArray.add(encodedObject)
}
I have decode the data in this for loop
let tempContacts2 = NSMutableArray()
for data in arrayOfArray {
let person: Person = NSKeyedUnarchiver.unarchiveObject(with: data as! Data) as! Person
tempContacts2.add(person)
}
but unarchiveObject is always return nil value
First your model class should conform to the NSCoder protocol. The rest is really simple, there's no need to store the archived results for each object in an array, you can pass the initial array directly to NSKeyedArchiver like this :
class Person: NSObject, NSCoding {
var name = ""
init(name: String) {
self.name = name
}
// NSCoder
required convenience init?(coder decoder: NSCoder) {
guard let name = decoder.decodeObject(forKey: "name") as? String else { return nil }
self.init(name: name)
}
func encode(with coder: NSCoder) {
coder.encode(self.name, forKey: "name")
}
}
let tempContacts = [Person(name: "John"), Person(name: "Mary")]
let encodedObjects = NSKeyedArchiver.archivedData(withRootObject: tempContacts)
let decodedObjects = NSKeyedUnarchiver.unarchiveObject(with: encodedObjects)
As a side note : if NSCoder compliance is correctly implemented in your model class, you can of course use your way of archiving/unarchiving individual objects too. So your original code works too, with some minor adjustments:
for person in tempContacts {
let encodedObject = NSKeyedArchiver.archivedData(withRootObject: person)
arrayOfArray.add(encodedObject)
}
var tempContacts2 = [Person]()
for data in arrayOfArray {
let person: Person = NSKeyedUnarchiver.unarchiveObject(with: data as! Data) as! Person
tempContacts2.append(person)
}
Note 2: if you absolutely wants to use NSMutableArrays that's possible too, just define tempContacts like this:
let tempContacts = NSMutableArray(array: [Person(name: "John"), Person(name: "Mary")])
The rest is working without changes.
Note 3: The reason it used to work in Swift 2 and it's not working anymore in Swift 3 is that the signature for the NSCoder method func encode(with coder:) changed in Swift 3.
Whenever I open my app, it doesn't load my array values because the != nil function isn't called. Is there anything I can do about this?
Code:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
var toDoData = NSUserDefaults.standardUserDefaults()
if (toDoData.valueForKey("TDDATA") != nil){
todos = toDoData.valueForKey("TDDATA") as! NSArray as! [TodoModel]
}
if todos.count != 0{
toDoData.setValue(todos, forKeyPath: "TDDATA")
toDoData.synchronize()
}
}
Don't worry about the table. It populates perfectly. I just need the loading data issue fixed.
Code included in your answer helps a lot!
Thanks.
UPDATE:
Here is the TodoModel:
import Foundation
import UIKit
class TodoModel : NSObject, NSCoding {
var id: String
var image: String
var title: String
var desc: String
var scores: String
init (id: String, image: String, title: String, desc: String, scores: String) {
self.id = id
self.image = image
self.title = title
self.desc = desc
self.scores = scores
}
}
valueForKey and setValue:forKeyPath are KVC (Key Value Coding) methods (read here and here). It will not help you read/write to the user defaults database.
Looking in the NSUserDefaults documentation, there are a number of methods available for getting and setting values in the defaults database. Since you are using arrays, we will use:
arrayForKey to get.
setObject:forKey to set. (There is no array-specific setter)
EDIT: Try this in your viewDidAppear. Here we check if we have data, and if we do, we store it. If we don't have data, then check if the defaults database has some saved. If it does, use it instead. It would be advantageous to only load data from the defaults database in viewDidLoad, and then save in viewDidAppear or even better, a function which is called when a todo is added.
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
let defaults = NSUserDefaults.standardUserDefaults()
if todos.count > 0 {
// Save what we have
let data = NSKeyedArchiver.archivedDataWithRootObject(todos)
defaults.setObject(data, forKey: "TDDATA")
defaults.synchronize()
print("saved \(todos.count)")
} else if let storedTodoData = defaults.dataForKey("TDDATA"),
storedTodos = NSKeyedUnarchiver.unarchiveObjectWithData(storedTodoData) as? [TodoModel] {
// There was stored data! Use it!
todos = storedTodos
print("Used \(todos.count) stored todos")
}
}
In addition, we must implement the NSCoding protocol in your model. This should be something like this:
class TodoModel: NSObject, NSCoding {
var myInt: Int = 0
var myString: String?
var myArray: [String]?
required init?(coder aDecoder: NSCoder) {
myInt = aDecoder.decodeIntegerForKey("myInt")
myString = aDecoder.decodeObjectForKey("myString") as? String
myArray = aDecoder.decodeObjectForKey("myArray") as? [String]
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeInteger(myInt, forKey: "myInt")
aCoder.encodeObject(myString, forKey: "myString")
aCoder.encodeObject(myArray, forKey: "myArray")
}
}
(Of course, replace myInt, myString, myArray, etc, with whatever properties your model might have.)
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
In my scenario, I am trying to maintain cache data for UICollectionView. Here, VC2 to VC1 I am passing array data and VC1 I am loading passed data into UICollectionView. Now, If I close and reopen app then I can’t able to see UICollectionView data Its all removed but I have to maintain cache. How to do it?
Collection View Data Load From VC2 passed array
func pass(data: [TeamListData]) {
print("ARRAY DATA RECEIVED:\(data)")
participantsData = data
self.collectionView.reloadData()
}
My Array Data
ARRAY DATA RECEIVED:[TeamListData(userid: Optional("1"), firstname: Optional(“abc”), designation: Optional("Analyst"), profileimage: Optional(“url.jpg"), isSelected: true), TeamListData(userid: Optional(“2”), firstname: Optional(“def”), designation: Optional("Executive"), profileimage: Optional(“url.jpg"), isSelected: true)]
Saved your data after getting callback in VC1
Code
func pass(data: [TeamListData]) {
print("ARRAY DATA RECEIVED:\(data)")
participantsData = data
self.collectionView.reloadData()
UserDefaults.standard.setValue( try? PropertyListEncoder().encode(data), forKey: "sessiondata")
}
Inside viewDidLoad in VC1
func storeValidaion(){
// Retrive Array Values
if participantsData == nil {
if let data = UserDefaults.standard.value(forKey:"sessiondata") as? Data {
guard let sessionData = try? PropertyListDecoder().decode(Array<TeamListData>.self, from: data) else {
return
}
print("ARRAY VALUES: \(sessionData)")
self.participantsData = sessionData
self.collectionView.reloadData()
}
}
}
You're not looking for a cache, but for a persistent storage. Depending on where your data is coming from and how good solution you need, you can either use the disk, UserDefaults or a database approach such as CoreData, Realm or others.
There's a handy tutorial with a lot of code here for storing custom objects in UserDefaults with NSCoding: https://developer.apple.com/library/archive/referencelibrary/GettingStarted/DevelopiOSAppsSwift/PersistData.html
E.g.
Conforming to the NSCoding:
struct PropertyKey {
static let name = "name"
static let photo = "photo"
static let rating = "rating"
}
class Meal: NSObject, NSCoding {
let name: String
let photo: UIImage
let rating: Int
required convenience init?(coder aDecoder: NSCoder) {
// The name is required. If we cannot decode a name string, the initializer should fail.
guard let name = aDecoder.decodeObject(forKey: PropertyKey.name) as? String else {
return nil
}
// Because photo is an optional property of Meal, just use conditional cast.
let photo = aDecoder.decodeObject(forKey: PropertyKey.photo) as? UIImage
let rating = aDecoder.decodeInteger(forKey: PropertyKey.rating)
// Must call designated initializer.
self.init(name: name, photo: photo, rating: rating)
}
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: PropertyKey.name)
aCoder.encode(photo, forKey: PropertyKey.photo)
aCoder.encode(rating, forKey: PropertyKey.rating)
}
}
Saving data:
private func saveMeals() {
let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(meals, toFile: Meal.ArchiveURL.path)
if isSuccessfulSave {
os_log("Meals successfully saved.", log: OSLog.default, type: .debug)
} else {
os_log("Failed to save meals...", log: OSLog.default, type: .error)
}
}
Loading data:
private func loadMeals() -> [Meal]? {
return NSKeyedUnarchiver.unarchiveObject(withFile: Meal.ArchiveURL.path) as? [Meal]
}
Realm on the other hand offers a lot of flexibility for a bit more time investment in learning a third party lib: https://realm.io/docs/swift/latest/#models