How can I store an array of objects of type Goal which I have created in NSUserDefaults? (in swift)
Here is the code:
func saveGoalList ( newGoalList : [Goal] ){
let updatedGoalList = newGoalList;
NSUserDefaults.standardUserDefaults().setObject(updatedGoalList, forKey: "GoalList")
NSUserDefaults.standardUserDefaults().synchronize()
}
class GoalsViewController: MainPageContentViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet var tableView: GoalsTableView!
var cell = GoalTableViewCell()
var goalsArray : Array<Goal> = [] //
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.delegate = self
self.tableView.dataSource = self
if var storedGoalList: [Goal] = NSUserDefaults.standardUserDefaults().objectForKey("GoalList") as? [Goal]{
goalsArray = storedGoalList;
}
var goal = Goal(title: "Walk the Dog")
goalsArray.append(goal)
saveGoalList(goalsArray)
self.tableView?.reloadData()
tableView.estimatedRowHeight = 44.0
tableView.rowHeight = UITableViewAutomaticDimension
self.xpnotificationView.alpha = 0.0
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return goalsArray.count //to ensure there is always an extra cell to fill in.
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { //recreate the cell and try using it.
cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as GoalTableViewCell
cell.goalTextField.text = goalsArray[indexPath.row].title as String!
cell.checkmarkImageView.visible = goalsArray[indexPath.row].checkmarked as Bool!
if (cell.checkmarkImageView.visible == true) {
cell.blackLineView.alpha = 1.0
} else {
cell.blackLineView.alpha = 0.0
}
return cell
}
}
I understand that there are only certain data types that work with NSUserDefaults. Could anyone help me understand how I could do that?
Edit: Right now Goal inherits from NSObject.
I am posting code from a learning project I did to store objects using NSCoding. Fully functional and ready to use. A math game that was storing game variables, etc.
//********This class creates the object and properties to store********
import Foundation
class ButtonStates: NSObject {
var sign: String = "+"
var level: Int = 1
var problems: Int = 10
var time: Int = 30
var skipWrongAnswers = true
func encodeWithCoder(aCoder: NSCoder!) {
aCoder.encodeObject(sign, forKey: "sign")
aCoder.encodeInteger(level, forKey: "level")
aCoder.encodeInteger(problems, forKey: "problems")
aCoder.encodeInteger(time, forKey: "time")
aCoder.encodeBool(skipWrongAnswers, forKey: "skipWrongAnswers")
}
init(coder aDecoder: NSCoder!) {
sign = aDecoder.decodeObjectForKey("sign") as String
level = aDecoder.decodeIntegerForKey("level")
problems = aDecoder.decodeIntegerForKey("problems")
time = aDecoder.decodeIntegerForKey("time")
skipWrongAnswers = aDecoder.decodeBoolForKey("skipWrongAnswers")
}
override init() {
}
}
//********Here is the data archiving and retrieving class********
class ArchiveButtonStates:NSObject {
var documentDirectories:NSArray = []
var documentDirectory:String = ""
var path:String = ""
func ArchiveButtons(#buttonStates: ButtonStates) {
documentDirectories = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
documentDirectory = documentDirectories.objectAtIndex(0) as String
path = documentDirectory.stringByAppendingPathComponent("buttonStates.archive")
if NSKeyedArchiver.archiveRootObject(buttonStates, toFile: path) {
//println("Success writing to file!")
} else {
println("Unable to write to file!")
}
}
func RetrieveButtons() -> NSObject {
var dataToRetrieve = ButtonStates()
documentDirectories = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
documentDirectory = documentDirectories.objectAtIndex(0) as String
path = documentDirectory.stringByAppendingPathComponent("buttonStates.archive")
if let dataToRetrieve2 = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as? ButtonStates {
dataToRetrieve = dataToRetrieve2 as ButtonStates
}
return(dataToRetrieve)
}
}
the following is in my ViewController where the game is played. Only showing the relevant code for retrieving and storing objects
class mathGame: UIViewController {
var buttonStates = ButtonStates()
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
//set inital view
//retrieving a stored object & placing property into local class variables
buttonStates = ArchiveButtonStates().RetrieveButtons() as ButtonStates
gameData.sign = buttonStates.sign
gameData.level = buttonStates.level
gameData.problems = buttonStates.problems
gameData.time = buttonStates.time
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
//storing the object
ArchiveButtonStates().ArchiveButtons(buttonStates: buttonStates)
}
}
You need your class to adopt the NSCoding protocol and encode and decode itself, like this:
https://github.com/mattneub/Programming-iOS-Book-Examples/blob/master/bk2ch23p798basicFileOperations/ch36p1053basicFileOperations/Person.swift
Now you can transform an instance of your class into an NSData by calling NSKeyedArchiver.archivedDataWithRootObject: - and an NSData can go into NSUserDefaults.
This also means that an NSArray of instances of your class can be transformed into an NSData by the same means.
For Swift 2.1, your Goal class should look like :
import Foundation
class Goal : NSObject, NSCoding {
var title: String
// designated initializer
init(title: String) {
self.title = title
super.init() // call NSObject's init method
}
// MARK: - comply wiht NSCoding protocol
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(title, forKey: "GoalTitle")
}
required convenience init?(coder aDecoder: NSCoder) {
// decoding could fail, for example when no Blog was saved before calling decode
guard let unarchivedGoalTitle = aDecoder.decodeObjectForKey("GoalTitle") as? String
else {
// option 1 : return an default Blog
self.init(title: "unknown")
return
// option 2 : return nil, and handle the error at higher level
}
// convenience init must call the designated init
self.init(title: unarchivedGoalTitle)
}
}
and you should use it in your view controller like I did in this test code :
// create an array with test data
let goal1 = Goal(title: "first goal")
let goal2 = Goal(title: "second goal")
let goalArray = [goal1, goal2]
// first convert the array of custom Goal objects to a NSData blob, as NSUserDefaults cannot handle arrays of custom objects directly
let dataBlob = NSKeyedArchiver.archivedDataWithRootObject(goalArray)
// this NSData object can now be stored in the user defaults
NSUserDefaults.standardUserDefaults().setObject(dataBlob, forKey: "myGoals")
// sync to make sure they are saved before we retreive anytying
NSUserDefaults.standardUserDefaults().synchronize()
// now read back
if let decodedNSDataBlob = NSUserDefaults.standardUserDefaults().objectForKey("myGoals") as? NSData {
if let loadedGoalsArray = NSKeyedUnarchiver.unarchiveObjectWithData(decodedNSDataBlob) as? [Goal] {
for goal in loadedGoalsArray {
print("goal : \(goal.title)")
}
}
}
As a final remark : it would be easier to use NSKeyedArchiver instead of NSUserDefaults, and store your array of custom objects directly to a file. You can read more about the difference between both methods in another answer I posted here.
Related
I am try to save and retrieve notes data with custom object called Sheet.
But I am having crashes when it runs. Is this the correct way to do it or is there any other ways to solve this?
The Sheet Class
class Sheet {
var title = ""
var content = ""
}
Here is the class for UITableViewController
class NotesListTableVC: UITableViewController {
var notes = [Sheet]()
override func viewDidLoad() {
super.viewDidLoad()
if let newNotes = UserDefaults.standard.object(forKey: "notes") as? [Sheet] {
//set the instance variable to the newNotes variable
notes = newNotes
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Return the number of rows in the section.
return notes.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell(withIdentifier: "notesCELL", for: indexPath)
cell.textLabel!.text = notes[indexPath.row].title
return cell
}
// Add new note or opening existing note
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "editNote" {
var noteContentVC = segue.destination as! NoteContentVC
var selectedIndexPath = tableView.indexPathForSelectedRow
noteContentVC.note = notes[selectedIndexPath!.row]
}
else if segue.identifier == "newNote" {
var newEntry = Sheet()
notes.append(newEntry)
var noteContentVC = segue.destination as! NoteContentVC
noteContentVC.note = newEntry
}
saveNotesArray()
}
// Reload the table view
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.tableView.reloadData()
}
// Deleting notes
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
notes.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: UITableViewRowAnimation.automatic)
}
// Save the notes
func saveNotesArray() {
// Save the newly updated array
UserDefaults.standard.set(notes, forKey: "notes")
UserDefaults.standard.synchronize()
}
}
And where should I call the saveNotesArray function?
You are trying to save an array of custom objects to UserDefaults. Your custom object isn't a property list object You should use Codable to save non-property list object in UserDefaults like this.
Swift 4
Custom Class
class Sheet: Codable {
var title = ""
var content = ""
}
ViewController.swift
class ViewController: UIViewController {
var notes = [Sheet]()
override func viewDidLoad() {
super.viewDidLoad()
getSheets()
addSheets()
getSheets()
}
func getSheets()
{
if let storedObject: Data = UserDefaults.standard.data(forKey: "notes")
{
do
{
notes = try PropertyListDecoder().decode([Sheet].self, from: storedObject)
for note in notes
{
print(note.title)
print(note.content)
}
}
catch
{
print(error.localizedDescription)
}
}
}
func addSheets()
{
let sheet1 = Sheet()
sheet1.title = "title1"
sheet1.content = "content1"
let sheet2 = Sheet()
sheet2.title = "title1"
sheet2.content = "content1"
notes = [sheet1,sheet2]
do
{
UserDefaults.standard.set(try PropertyListEncoder().encode(notes), forKey: "notes")
UserDefaults.standard.synchronize()
}
catch
{
print(error.localizedDescription)
}
}
}
You give answer to the question that you ask.
App crash log.
[User Defaults] Attempt to set a non-property-list object ( "Sheet.Sheet" )
Official Apple info.
A default object must be a property list—that is, an instance of (or
for collections, a combination of instances of): NSData, NSString,
NSNumber, NSDate, NSArray, or NSDictionary.
If you want to store any other type of object, you should typically
archive it to create an instance of NSData. For more details, see
Preferences and Settings Programming Guide.
One of the possible solution:
class Sheet : NSObject, NSCoding{
var title:String?
var content:String?
func encode(with aCoder: NSCoder) {
aCoder.encodeObject(self.title, forKey: "title")
aCoder.encodeObject(self.content, forKey: "content")
}
required init?(coder aDecoder: NSCoder) {
self.title = aDecoder.decodeObject(forKey: "title") as? String
self.content = aDecoder.decodeObject(forKey: "content") as? String
}
}
Save
userDefaults.setValue(NSKeyedArchiver.archivedDataWithRootObject(sheets), forKey: "sheets")
Load
sheets = NSKeyedUnarchiver.unarchiveObjectWithData(userDefaults.objectForKey("sheets") as! NSData) as! [Sheet]
The code you posted tries to save an array of custom objects to NSUserDefaults. You can't do that. Implementing the NSCoding methods doesn't help. You can only store things like Array, Dictionary, String, Data, Number, and Date in UserDefaults.
You need to convert the object to Data (like you have in some of the code) and store that Data in UserDefaults. You can even store an Array of Data if you need to.
When you read back the array you need to unarchive the Data to get back your Sheet objects.
Change your Sheet object to :
class Sheet: NSObject, NSCoding {
var title: String
var content: String
init(title: String, content: String) {
self.title = title
self.content = content
}
required convenience init(coder aDecoder: NSCoder) {
let title = aDecoder.decodeObject(forKey: "title") as! String
let content = aDecoder.decodeObject(forKey: "content") as! String
self.init(title: title, content: content)
}
func encode(with aCoder: NSCoder) {
aCoder.encode(title, forKey: "title")
aCoder.encode(content, forKey: "content")
}
}
into a function like :
func loadData() {
if let decoded = userDefaults.object(forKey: "notes") as? Data, let notes = NSKeyedUnarchiver.unarchiveObject(with: decoded) as? [Sheet] {
self.notes = notes
self.tableView.reloadData()
}
}
and then call :
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.loadData()
}
saveNotesArray can be called after new Notes added with :
func saveNotesArray() {
// Save the newly updated array
var userDefaults = UserDefaults.standard
let encodedData: Data = NSKeyedArchiver.archivedData(withRootObject: notes)
userDefaults.set(encodedData, forKey: "notes")
userDefaults.synchronize()
}
I've set some variables as an Object
import UIKit
class SpeedTestResult: NSObject {
var testTime: NSDate?
}
Then in the controller I set this object and pass it to a class to store it:
testResult.testTime = NSDate()
SpeedTestManager().addTestResult(testResult)
I need to store this object and then access the elements within in a view later, This is what I have:
import Foundation
class SpeedTestManager : NSObject {
var testResultArray = [NSObject]()
func addTestResult(testResult: NSObject) {
testResultArray.append(testResult)
print("Printing testResultArray: \(testResultArray)")
}
}
But when I try to print the the object I just get
Printing testResultArray: [<ProjectName.SpeedTestResult: 0x127b85e50>]
How do I access elements within the object and store this object and retrieve it for later use in a view?
class TestResult : NSObject, NSSecureCoding {
var testTime: NSDate?
override init() {
super.init()
}
#objc required init?(coder decoder: NSCoder) {
self.testTime = decoder.decodeObjectForKey("testTime") as? NSDate
}
#objc func encodeWithCoder(encoder: NSCoder) {
encoder.encodeObject(self.testTime, forKey: "testTime")
}
#objc static func supportsSecureCoding() -> Bool {
return true
}
override var description: String {
return String.init(format: "TestResult: %#", self.testTime ?? "null")
}
}
class SpeedTestManager : NSObject, NSSecureCoding {
var testResultArray = [NSObject]()
func addTestResult(testResult: NSObject) {
testResultArray.append(testResult)
print("Printing testResultArray: \(testResultArray)")
}
override init() {
super.init()
}
#objc func encodeWithCoder(encoder: NSCoder) {
encoder.encodeObject(self.testResultArray, forKey: "testResultArray")
}
#objc required init?(coder decoder: NSCoder) {
self.testResultArray = decoder.decodeObjectForKey("testResultArray") as! [NSObject]
}
#objc static func supportsSecureCoding() -> Bool {
return true
}
override var description: String {
return String.init(format: "SpeedManager: [%#]", self.testResultArray.map({"\($0)"}).joinWithSeparator(","))
}
}
class TestViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let testResult = TestResult()
testResult.testTime = NSDate()
let speedManager = SpeedTestManager()
speedManager.addTestResult(testResult)
NSUserDefaults.standardUserDefaults().setObject(NSKeyedArchiver.archivedDataWithRootObject(speedManager), forKey: "speedManager")
NSUserDefaults.standardUserDefaults().synchronize()
if let archivedSpeedManager = NSUserDefaults.standardUserDefaults().objectForKey("speedManager") as? NSData {
let unarchivedSpeedManager = NSKeyedUnarchiver.unarchiveObjectWithData(archivedSpeedManager)
print("SpeedManager: \(unarchivedSpeedManager ?? "null")")
}
else {
print("Failed to unarchive speed manager")
}
}
}
Here is one way you can do it:
import Foundation
class SpeedTestResult: NSObject {
var testTime: NSDate?
}
class SpeedTestManager : NSObject {
var testResultArray = [NSObject]()
func addTestResult(testResult: NSObject) {
testResultArray.append(testResult)
for result in testResultArray {
// This will crash if result is not a SpeedTestResult.
// print((result as! SpeedTestResult).testTime)
// This is better:
if let timeResult = result as? SpeedTestResult
{
print(timeResult.testTime)
}
else
{
print("Not time type...")
}
}
}
}
var testResult = SpeedTestResult()
testResult.testTime = NSDate()
SpeedTestManager().addTestResult(testResult)
This addresses your specific question, but there are some other problems here:
If you are going to store only SpeedTestResult instances in
testResultArray, then why not make it of type
[SpeedTestResutl]()?
If you will store different types of tests in the array, then how do
you find out which type of test an NSObject element represents?
There are ways... In the above code we at least make sure we are not treating a wrong type of object as a SpeedTestResult.
When you do SpeedTestManager().addTestResult(testResult), you don't
keep a reference to the SpeedTestManager instance. The next time
you make the same call, you will be creating a different
SpeedTestManager instance.
This is not really a problem, but SpeedTestManager does not have to
be a sub-class of NSObject, unless you want to use it in
Objective-C.
You probably don't want to print the content of testResultArray in
the addTestResult() method. You could have other methods for
accessing the array.
To add your test results to the same test manager, you could do:
let myTestManager = SpeedTestManager()
myTestManager.addTestResult(testResult)
// create other test results and add them ...
I'm new in Swift and iOS and I came across this issue while making an app. I want to basically store user data using NSCoding onto local storage. However, my code below doesn't do it. Anyone can tell me what's wrong with it? Much appreciated!
Also, both saveChecklist and LoadChecklist are called in appDelegate, under applicationDidEnterBackground and applicationWillTerminate.
I have a feeling my issue lies in encodeWithCoder and init(aDecoder), as I used GET to append my Checklist item into lists.
My code in DataModel.class:
import Foundation
class DataModel: NSObject, NSCoding {
var checklist = Checklist()
var lists: [Checklist] {
get {
return [checklist]
}
set {
}
}
override init() {
super.init()
loadChecklist()
}
// MARK: - All the saving stuff
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(checklist, forKey: "Checklist")
}
required init?(coder aDecoder: NSCoder) {
checklist = aDecoder.decodeObjectForKey("Checklist") as! Checklist
}
func documentsDirectory() -> String {
let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
return paths[0]
}
func dataFilePath() -> String {
return (documentsDirectory() as NSString).stringByAppendingPathComponent("Checklist.plist") // create file if no checklist.plist is present
}
func saveChecklist() {
let data = NSMutableData()
let archiver = NSKeyedArchiver(forWritingWithMutableData: data)
archiver.encodeObject(lists, forKey: "Checklists")
archiver.finishEncoding()
data.writeToFile(dataFilePath(), atomically: true)
}
func loadChecklist() {
let path = dataFilePath()
if NSFileManager.defaultManager().fileExistsAtPath(path) {
if let data = NSData(contentsOfFile: path) {
let unarchiver = NSKeyedUnarchiver(forReadingWithData: data)
lists = unarchiver.decodeObjectForKey("Checklists") as! [Checklist]
unarchiver.finishDecoding()
}
}
}
My code in Checklist.class:
import Foundation
class Checklist: NSObject, NSCoding {
var item = [items]()
var rituals = [items]()
var doneButtonVisible: Bool
var streak: Int
var itemDoneCount: Int
var startDate: NSDate
var dayHasStarted: Bool
override init() {
doneButtonVisible = false
itemDoneCount = 0
streak = 0
startDate = NSDate()
dayHasStarted = false
super.init()
}
// saves
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(item, forKey: "Items")
aCoder.encodeObject(rituals, forKey: "Rituals")
aCoder.encodeObject(itemDoneCount, forKey: "ItemDoneCount")
aCoder.encodeObject(doneButtonVisible, forKey: "DoneButtonVisible")
aCoder.encodeObject(streak, forKey: "Streak")
aCoder.encodeObject(startDate, forKey: "StartDate")
aCoder.encodeObject(dayHasStarted, forKey: "DayHasStarted")
}
// loads
required init?(coder aDecoder: NSCoder) {
item = aDecoder.decodeObjectForKey("Items") as! [items]
rituals = aDecoder.decodeObjectForKey("Rituals") as! [items]
itemDoneCount = aDecoder.decodeObjectForKey("ItemDoneCount") as! Int
doneButtonVisible = aDecoder.decodeObjectForKey("DoneButtonVisible") as! Bool
streak = aDecoder.decodeObjectForKey("Streak") as! Int
startDate = aDecoder.decodeObjectForKey("StartDate") as! NSDate
dayHasStarted = aDecoder.decodeObjectForKey("DayHasStarted") as! Bool
super.init()
}
}
UPDATE
Solved the saving and loading issue by revamping my data model. Using Core Data instead to save and load data.
However, I still have no idea as to why the above code does not load. Apparently, it does not save the contents of checklist to begin with.
Your Checklist class needs to inherit from NSCoding too, and implement the encodeWithCoder(coder: NSCoder) method. This is a good tutorial on NSCoding.
This question already has an answer here:
How to store value generated from a ViewController using NSCoding in Swift
(1 answer)
Closed 7 years ago.
I need to save data from a segued ViewController (“ScoreView.swift”) to “ScoreHistory.swift” using NSCoding. I tried but the data isn't showing up in "ScoreTableViewController.swift". What am I missing?
I have this ScoreView.swift which has the following code: (Pls note that this is a "segued" view where data has been passed from another ViewController)
class ScoreView: UIViewController {
var dateToday = NSDate()
var score: ScoreHistory?
var numberofquestions:String = ""
var scorepassed:String = ""
var scorepercentpassed:String = ""
var scoreremarkspassed:String = ""
var totalduration:String!
var incorrectanswerspassed:String = ""
var skippedquestionspassed:String = ""
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
datePlayedLabel.text = dateToday.description
totalScoreLabel.text = scorepassed
scorePercentage.text = scorepercentpassed
totalAnsweredLabel.text = numberofquestions
totalDurationLabel.text = totalduration
gameStatusLabel.text = "Exam Finished"
// NSCoding
if let score = score {
datePlayedLabel.text = score.datePlayed
totalScoreLabel.text = score.totalScore
totalAnsweredLabel.text = score.totalAnswered
totalDurationLabel.text = score.totalDuration
gameStatusLabel.text = score.gameStatus
}
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if backMenu === sender {
let datePlayed = datePlayedLabel.text ?? ""
let totalScore = totalScoreLabel.text ?? ""
let totalAnswered = totalAnsweredLabel.text ?? ""
let totalDuration = totalDurationLabel.text ?? ""
let gameStatus = gameStatusLabel.text ?? ""
// Set the score to be passed to ScoreTableViewController after the unwind segue.
score = ScoreHistory(datePlayed: datePlayed, totalScore: totalScore, totalAnswered: totalAnswered, totalDuration: totalDuration, gameStatus: gameStatus)
}
NSKeyedArchiver.archiveRootObject(score!, toFile: ScoreHistory.ArchiveURL.path!)
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(true)
datePlayedLabel.text = dateToday.description
totalScoreLabel.text = scorepassed
scorePercentage.text = scorepercentpassed
totalAnsweredLabel.text = numberofquestions
totalDurationLabel.text = totalduration
gameStatusLabel.text = "Exam Finished"
}
// Labels
}
}
I have ScoreHistory.swift, which has the following code:
class ScoreHistory: NSObject, NSCoding {
// MARK: Properties
var datePlayed: String
var totalScore: String
var totalAnswered: String
var totalDuration: String
var gameStatus: String
// MARK: Archiving Paths
static let DocumentsDirectory = NSFileManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first!
static let ArchiveURL = DocumentsDirectory.URLByAppendingPathComponent("scores")
// MARK: Types
struct PropertyKey {
static let datePlayedKey = "datePlayed"
static let totalScoreKey = "totalScore"
static let totalAnsweredKey = "totalAnswered"
static let totalDurationKey = "totalDuration"
static let gameStatusKey = "gameStatus"
}
// MARK: Initialization
init?(datePlayed: String, totalScore: String, totalAnswered: String, totalDuration: String, gameStatus: String) {
// Initialize stored properties.
self.datePlayed = datePlayed
self.totalScore = totalScore
self.totalAnswered = totalAnswered
self.totalDuration = totalDuration
self.gameStatus = gameStatus
super.init()
}
// MARK: NSCoding
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(datePlayed, forKey: PropertyKey.datePlayedKey)
aCoder.encodeObject(totalScore, forKey: PropertyKey.totalScoreKey)
aCoder.encodeObject(totalAnswered, forKey: PropertyKey.totalAnsweredKey)
aCoder.encodeObject(totalDuration, forKey: PropertyKey.totalDurationKey)
aCoder.encodeObject(gameStatus, forKey: PropertyKey.gameStatusKey)
}
required convenience init?(coder aDecoder: NSCoder) {
let datePlayed = aDecoder.decodeObjectForKey(PropertyKey.datePlayedKey) as! String
let totalScore = aDecoder.decodeObjectForKey(PropertyKey.totalScoreKey) as! String
let totalAnswered = aDecoder.decodeObjectForKey(PropertyKey.totalAnsweredKey) as! String
let totalDuration = aDecoder.decodeObjectForKey(PropertyKey.totalDurationKey) as! String
let gameStatus = aDecoder.decodeObjectForKey(PropertyKey.gameStatusKey) as! String
// Must call designated initializer.
self.init(datePlayed: datePlayed, totalScore: totalScore, totalAnswered: totalAnswered, totalDuration: totalDuration, gameStatus: gameStatus)
}
}
Here is the full code of ScoreTableViewController.swift:
class ScoreTableViewController: UITableViewController {
// MARK: Properties
var scores = [ScoreHistory]()
var dateToday = NSDate()
override func viewDidLoad() {
super.viewDidLoad()
// Load any saved scores, otherwise load sample data.
if let savedScores = loadScores() {
scores += savedScores
} else {
// Load the sample data.
loadSampleScores()
}
}
func loadSampleScores() {
let score1 = ScoreHistory(datePlayed: dateToday.description, totalScore: "0", totalAnswered: "0", totalDuration: "0", gameStatus: "started")!
scores += [score1]
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return scores.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Table view cells are reused and should be dequeued using a cell identifier.
let cellIdentifier = "ScoreHistoryTableViewCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! ScoreHistoryTableViewCell
// Fetches the appropriate note for the data source layout.
let score = scores[indexPath.row]
cell.datePlayedLabel.text = score.datePlayed
cell.totalScoreLabel.text = score.datePlayed
cell.totalScoreLabel.text = score.totalScore
cell.totalAnsweredLabel.text = score.totalAnswered
cell.totalDurationLabel.text = score.totalDuration
cell.gameStatusLabel.text = score.gameStatus
return cell
}
// Override to support conditional editing of the table view.
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
// Override to support editing the table view.
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
// Delete the row from the data source
scores.removeAtIndex(indexPath.row)
saveScores()
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}
}
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "ShowDetail" {
let scoreDetailViewController = segue.destinationViewController as! ScoreViewController
// Get the cell that generated this segue.
if let selectedScoreCell = sender as? ScoreHistoryTableViewCell {
let indexPath = tableView.indexPathForCell(selectedScoreCell)!
let selectedScore = scores[indexPath.row]
scoreDetailViewController.score = selectedScore
}
}
}
// MARK: NSCoding
func saveScores() {
let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(scores, toFile: ScoreHistory.ArchiveURL.path!)
if !isSuccessfulSave {
print("Failed to save scores...")
}
}
func loadScores() -> [ScoreHistory]? {
return NSKeyedUnarchiver.unarchiveObjectWithFile(ScoreHistory.ArchiveURL.path!) as? [ScoreHistory]
}
#IBAction func unwindToScoreList(sender: UIStoryboardSegue) {
if let sourceViewController = sender.sourceViewController as? ScoreViewController, score = sourceViewController.score {
if let selectedIndexPath = tableView.indexPathForSelectedRow {
// Update an existing note.
scores[selectedIndexPath.row] = score
tableView.reloadRowsAtIndexPaths([selectedIndexPath], withRowAnimation: .None)
// Add a new score.
let newIndexPath = NSIndexPath(forRow: scores.count, inSection: 0)
scores.append(score)
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Bottom)
saveScores()
}
}
}
}
GOAL: My goal is to record/store all session data from “ScoreView.swift” whenever a user finishes a quiz game.
The "ScoreView" is shown after each quiz game, I plan to record each quiz results in "ScoreHistory.swift." How do I do it?
The easiest solution is to save the changed values from the UITextField instances back to the score instance in ScoreView (why is score optional at all since you always pass a non-optional score instance ??) and unwind the segue.
Then the array is saved in the method unwindToScoreList of ScoreTableViewController
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if backMenu === sender {
score?.datePlayed = datePlayedLabel.text ?? ""
score?.totalScore = totalScoreLabel.text ?? ""
score?.totalAnswered = totalAnsweredLabel.text ?? ""
score?.totalDuration = totalDurationLabel.text ?? ""
score?.gameStatus = gameStatusLabel.text ?? ""
}
}
No archiving in ScoreView !
Your loadScores function is loading an archived array of scores:
func loadScores() -> [ScoreHistory]? {
return NSKeyedUnarchiver.unarchiveObjectWithFile(ScoreHistory.ArchiveURL.path!) as? [ScoreHistory]
}
In your segue, you are only archiving a single score. You can't archive a ScoreHistory instance and expect to unarchive a ScoreHistory array. Where you currently have:
score = ScoreHistory(datePlayed: datePlayed, totalScore: totalScore, totalAnswered: totalAnswered, totalDuration: totalDuration, gameStatus: gameStatus)
You need to change this to:
var scores = loadScores() ?? []
score = ScoreHistory(datePlayed: datePlayed, totalScore: totalScore, totalAnswered: totalAnswered, totalDuration: totalDuration, gameStatus: gameStatus)
scores.append(score)
saveScores(scores)
Where loadScores and saveScores are the same as the code in ScoreTableViewController, although I've added the scores to save as a parameter given this code creates a local var.
UPDATE: It's late and I wasn't paying enough attention. You need to handle loadScores returning nil, and of course scores should be var not let or you won't be able to add to it. With these changes, scores should no longer be optional, so you won't need to unwrap it.
On the line that contains return goal //**// my program crashes and gives me the error: "fatal: array cannot be bridged from Objective-C". Could anyone know what this is from?
func saveGoals (goals : [Goal]) {
var updatedGoals = NSKeyedArchiver.archivedDataWithRootObject(goals)
NSUserDefaults.standardUserDefaults().setObject(updatedGoals, forKey: "Goals")
NSUserDefaults.standardUserDefaults().synchronize()
}
func loadCustomObjectWithKey() -> [Goal?] {
if let encodedObject : NSData = NSUserDefaults.standardUserDefaults().objectForKey("Goals") as? NSData {
var encodedObject : NSData? = NSUserDefaults.standardUserDefaults().objectForKey("Goals") as? NSData
var goal : [Goal] = NSKeyedUnarchiver.unarchiveObjectWithData(encodedObject!) as [Goal]
return goal //**//
} else {
return [Goal]()
}
}
class GoalsViewController: MainPageContentViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet var tableView: GoalsTableView!
var goalsArray : Array<Goal> = [] //
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.delegate = self
self.tableView.dataSource = self
if var storedGoals: [Goal] = loadCustomObjectWithKey() as? [Goal] {
goalsArray = storedGoals
}
//retrieve data.
if var storedGoalList: [Goal] = NSUserDefaults.standardUserDefaults().objectForKey("GoalList") as? [Goal]{
goalsArray = storedGoalList;
}
var goal = Goal(title: "Walk the Dog")
goalsArray.append(goal)
saveGoals(goalsArray)
self.tableView?.reloadData()
tableView.estimatedRowHeight = 44.0
tableView.rowHeight = UITableViewAutomaticDimension
}
}
You're trying to return a [Goal] as [Goal?]. Since the array content types don't match (and can't match) you get a runtime exception. Changed the return type to [Goal], particularly since you always return something anyway.