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()
}
Related
I am trying to show an information which is in Core Data, on UITableViewCell.
I could get the information, but the information wasn't shown on UITableViewCell.
When I set the information on CoreData, I use Modal View then.
I tried to use UITableView.reload() but I couldn't show the information on UITableViewCell.
Please let me know how to show the information when I back from modal view.
This class is about showing the information on UItableView.
import UIKit
import CoreData
protocol FriendListTableViewDelegate {
func reloadTable()
}
class FriendListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, FriendListTableViewDelegate{
#IBOutlet weak var friendListTableView: UITableView!
var friends:[FriendBasicInfo] = []
override func viewDidLoad() {
super.viewDidLoad()
getData()
}
// Disable to effect the reload
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
reloadTable()
}
// fetch the information from CoreData
func getData() {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
do {
friends = try context.fetch(FriendBasicInfo.fetchRequest())
} catch {
print("error")
}
}
func reloadTable() {
friendListTableView.reloadData()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return friends.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let friendCell = tableView.dequeueReusableCell(withIdentifier: "FriendListCell") as! FriendListTableViewCell
let friendName = friendCell.viewWithTag(1) as? UILabel
let friendImage = friendCell.viewWithTag(2) as? UIImageView
friendName?.text = friends[indexPath.row].name
friendImage?.image = friends[indexPath.row].photo?.toImage()
return friendCell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 80
}
}
This class is to get the information from CoreData
import UIKit
import XLPagerTabStrip
import Eureka
import CoreData
import ImageRow
class InputFriendInforViewController: FormViewController, IndicatorInfoProvider {
var itemInfo: IndicatorInfo = "Info"
var friendPhoto: UIImage?
var friendName: String = ""
var friendBirthday: Date?
var friendGender: String = ""
var friendListTableViewDelegate: FriendListTableViewDelegate!
override func viewDidLoad() {
super.viewDidLoad()
form +++
Section("Friend Information")
<<< ImageRow(){
$0.title = "Image"
$0.sourceTypes = [.PhotoLibrary, .SavedPhotosAlbum, .Camera]
$0.value = UIImage(named: "noImage")
$0.onChange { [unowned self] row in
self.friendPhoto = row.value!
}
}
<<< TextRow(){ row in
row.title = "Name"
row.placeholder = "Enter Name here"
}.onChange { name in
self.friendName = name.value!
}
<<< DateRow(){ row in
row.title = "Birthday"
row.value = Date(timeIntervalSinceReferenceDate: 0)
}.onChange {date in
self.friendBirthday = date.value!
}
<<< PushRow<String>(){row in
row.title = "Gender"
row.options = ["Male","Female","Other"]
}.onChange {gender in
self.friendGender = gender.value!
}
+++ Section()
<<< ButtonRow() {
$0.title = "SAVE"
}.onCellSelection {_, _ in
self.saveInfo()
}
}
// MARK: - IndicatorInfoProvider
func indicatorInfo(for pagerTabStripController: PagerTabStripViewController) -> IndicatorInfo {
return itemInfo
}
// save friend Info for Core Data
func saveInfo (){
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
let managedContext = appDelegate.persistentContainer.viewContext
let friendEntity = NSEntityDescription.entity(forEntityName: "FriendBasicInfo", in: managedContext)!
let friendInfo = NSManagedObject(entity: friendEntity, insertInto: managedContext)
// make unique user ID
let friendUid = NSUUID().uuidString
// Image Data UIImage to png Data
let pngImage = self.friendPhoto?.toPNGData()
friendInfo.setValue(friendUid, forKey: "userID")
friendInfo.setValue(pngImage, forKey: "photo")
friendInfo.setValue(self.friendName, forKey: "name")
friendInfo.setValue(self.friendBirthday, forKey: "birthday")
friendInfo.setValue(self.friendGender, forKey: "gender")
do {
try managedContext.save()
self.dismiss(animated: true, completion:nil)
} catch let error as NSError {
print("Could not save. \(error), \(error.userInfo)")
}
}
}
This class is about UITableViewCell
import UIKit
class FriendListTableViewCell: UITableViewCell {
#IBOutlet weak var sampleImageView: UIImageView!
#IBOutlet weak var sampleLabel:UILabel!
}
V/r,
As you are using an extra data source array just reloading the table view doesn't consider the new inserted item.
There are a few options
Use NSFetchedResultsController. It updates the UI automatically when the context was saved.
On dismiss insert the new item into the data source array and a new row into the table view.
Observe NSManagedObjectContextDidSaveNotification and insert the item as described in 2.
Refetch the entire data and reload the table view.
The options are in order of efficiency. Version 1 is the most efficient one.
Side note:
viewWithTag is horribly old-fashioned. You got outlets, use them for example
cell.sampleLabel!.text = friends[indexPath.row].name
Your FriendListViewController TableView will reflect any updates to any FriendBasicInfo Entity which was fetched within getData() method. To present a new inserted FriendBasicInfo Entities to the database you have to execute a new fetch with getData() method.
Solution:
func reloadTable() {
getData()
friendListTableView.reloadData()
}
Alternative solution
Advanced monitoring of a fetched entities can be done with NSFetchedResultsController Delegate, this controller will automatically update the FriendListViewController tableview for any updated, inserted or deleted entities.
I'm trying to learn iOS programming so I thought it would be a good idea to emulate instagrams feed. Everyone uses this basic feed and I would like to know how to do it.
The basic idea is to have one image/text post show up in a single column. Right now I have a a single image to be shown.
I'm currently extracting the image url correctly from firebase. The only issue is that my CollectionView still is showing up empty. I started this project months ago and I forget where the tutorial is at. Please help me fill in the blanks. Here is the code:
import UIKit
import SwiftUI
import Firebase
import FirebaseUI
import SwiftKeychainWrapper
class FeedViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource{
#IBOutlet weak var collectionview: UICollectionView!
//var posts = [Post]()
var posts = [String](){
didSet{
collectionview.reloadData()
}
}
var following = [String]()
var posts1 = [String]()
var userStorage: StorageReference!
var ref : DatabaseReference!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
posts1 = fetchPosts()
//let myIndexPath = IndexPath(row: 0, section: 0)
//collectionView(collectionview, cellForItemAt: myIndexPath)
//print(self.posts1.count)
}
func fetchPosts() -> [String]{
let uid = Auth.auth().currentUser!.uid
let ref = Database.database().reference().child("posts")
let uids = Database.database().reference().child("users")
uids.observe(DataEventType.value, with: { (snapshot) in
let dict = snapshot.value as! [String:NSDictionary]
for (_,value) in dict {
if let uid = value["uid"] as? String{
self.following.append(uid)
}
}
ref.observe(DataEventType.value, with: { (snapshot2) in
let dict2 = snapshot2.value as! [String:NSDictionary]
for(key, value) in dict{
for uid2 in self.following{
if (uid2 == key){
for (key2,value2) in value as! [String:String]{
//print(key2 + "this is key2")
if(key2 == "urlToImage"){
let urlimage = value2
//print(urlimage)
self.posts1.append(urlimage)
self.collectionview.reloadData()
print(self.posts1.count)
}
}
}
}
}
})
})
//ref.removeAllObservers()
//uids.removeAllObservers()
print("before return")
print(self.posts1.count)
return self.posts1
override func viewDidLayoutSubviews() {
collectionview.reloadData()
}
func numberOfSections(in collectionView: UICollectionView) ->Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return posts1.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PostCell", for: indexPath) as! PostCell
cell.postImage.sd_setImage(with: URL(string: posts1[indexPath.row]))
//creating the cell
//cell.postImage.downloadImage(from: self.posts[indexPath.row])
// let storageRef = Storage.storage().reference(forURL: self.posts[indexPath.row].pathToImage)
//
//
print("im trying")
//let stickitinme = URL(fileURLWithPath: posts1[0])
//cell.postImage.sd_setImage(with: stickitinme)
//cell.authorLabel.text = self.posts[indexPath.row].author
//cell.likeLabel.text = "\(self.posts[indexPath.row].likes) Likes"
return cell
}
#IBAction func signOutPressed(_sender: Any){
signOut()
self.performSegue(withIdentifier: "toSignIn", sender: nil)
}
#objc func signOut(){
KeychainWrapper.standard.removeObject(forKey:"uid")
do{
try Auth.auth().signOut()
} catch let signOutError as NSError{
print("Error signing out: %#", signOutError)
}
dismiss(animated: true, completion: nil)
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
}
*/
}
UPDATE
The observe call is not updating the value of posts (the dictionary). Once the observe call exits, the value of posts is set back to empty.
PostCell class as asked:
import UIKit
class PostCell: UICollectionViewCell {
#IBOutlet weak var postImage: UIImageView!
#IBOutlet weak var authorLabel: UILabel!
#IBOutlet weak var likeLabel:UILabel!
#IBOutlet weak var likeBtn:UIButton!
#IBOutlet weak var unlikeBtn:UIButton!
#IBAction func likePressed (_ sender: Any){
}
#IBAction func unlikePressed(_sender: Any){
}
}
I think the problem is:
Your collectionView dataSource is called only once. Since the image url loading is asynchronous, you will need to refresh your collectionview every time new data is appended to your datasource array like this:
self.posts.append(urlimage)
collectionView.reloadData()
or:
var posts = [UIImage](){
didSet{
collectionView.reloadData()
}
}
Hope this helps.
Edit update:
Regarding the asynchronous calls, i think you should use escaping closure that runs the code block once the network request receives a response.
First separate the network call functions like:
func fetchUsers(completion: #escaping(_ dictionary: [String: NSDictionary])->()){
let uid = Auth.auth().currentUser!.uid
let uids = Database.database().reference().child("users")
uids.observe(DataEventType.value, with: { (snapshot) in
let dict = snapshot.value as! [String:NSDictionary]
completion(dict)
})
}
func fetchURLS(completion: #escaping(_ dictionary: [String: String])->()){
let ref = Database.database().reference().child("posts")
ref.observe(DataEventType.value, with: { (snapshot2) in
let dict2 = snapshot2.value as! [String:String]
completionTwo(dict2)
})
}
Then, the parsing functions:
func parseUsers(dictionary: [String: NSDictionary]){
for (_,value) in dictionary {
if let uid = value["uid"] as? String{
self.following.append(uid)
}
}
fetchURLS { (urlDictionary) in
self.parseImageURLS(dictionary: urlDictionary)
}
}
func parseImageURLS(dictionary: [String: String]){
for(key, value) in dictionary{
for uid2 in self.following{
if (uid2 == key){
for (key2,value2) in value as! [String:String]{
//print(key2 + "this is key2")
if(key2 == "urlToImage"){
let urlimage = value2
//print(urlimage)
self.posts1.append(urlimage)
self.collectionview.reloadData()
print(self.posts1.count)
}
}
}
}
}
}
Then you add:
fetchUsers { (usersDictionary) in
self.parseUsers(dictionary: usersDictionary)
}
in viewDidLoad()
Hope this solves your problem. On a side note: I recommend using models and separating the network calls in a different file. Feel free to ask any questions.
I figured out how to do it after more searching.
I was incorrectly assuming that the CollectionView is loaded after the viewDidLoad() function is done. The helper classes for a CollectionView are called to a call of reloadData.
I observed that my reloadData call wasn't being called. In order to make this work, I add 2 lines of code to the viewDidLoad function:
collectionview.delegate = self
collectionview.dataSource = self
With this change, the images now load.
import Foundation
class Persistence
{
let partiesKey = "parties"
let namesKey = "names"
func saveParty(party: Party)
{
let userDefaults = UserDefaults.standard
var parties = fetchParties()
parties.append(party)
let data = NSKeyedArchiver.archivedData(withRootObject: parties)
userDefaults.set(data, forKey: partiesKey)
userDefaults.synchronize()
}
func fetchParties() -> [Party]
{
let userDefaults = UserDefaults.standard
let parties = userDefaults.object(forKey: partiesKey) as? Data
if let parties = parties
{
return NSKeyedUnarchiver.unarchiveObject(with: parties) as! [Party]
}
else
{
return [Party]()
}
}
func deleteParty(party: Party)
{
let userDefaults = UserDefaults.standard
var parties = fetchParties()
}
func saveName(partyName: String)
{
var name = fetchName()
name.append(partyName)
let userDefaults = UserDefaults.standard
userDefaults.set(name, forKey: namesKey)
userDefaults.synchronize()
}
func fetchName() -> [String]
{
let userDefaults = UserDefaults.standard
let name = (userDefaults.value(forKey: namesKey) as? [String])
return name ?? [String]()
}
}
Hi, I am very new to iOS programming. I am working on todo list for my first iOS app. I am trying to delete table view cell that is added by fetching and saving. I was able to save data and show name in the tableview cell. How do we delete table view cell in UserDefault? func deleteParty() method will be called where the tableView cell is like this:
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath)
{
if(editingStyle == .delete)
{
persistence.deleteParty(party: (parties?[indexPath.row])!)
//tableView.deleteRows(at: [indexPath], with: .fade)
tableView.reloadData()
}
}
This is my code for adding names to the table:
#IBAction func addButtonAction(sender: AnyObject) {
mineSpillere.append(namesTextBox.text)
myTableView.reloadData()
}
This is my "mineSpillere":
var mineSpillere = [String]()
This is my code for saving the key:
func saveKey(){
println("saving key")
var defaults = NSUserDefaults.standardUserDefaults()
defaults.setObject(mineSpillere, forKey: "spillereKey")
defaults.synchronize()
println(mineSpillere)
}
This is my viewDidLoad code:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.x
if (NSUserDefaults.standardUserDefaults().objectForKey(spillereKey) != nil) {
if let testArray : AnyObject? = NSUserDefaults.standardUserDefaults().objectForKey(spillereKey) {
var readArray : [NSString] = testArray! as! [NSString]
println(readArray)
}
}
self.myTableView?.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cell")
self.myTableView.dataSource = self
}
This is my code for loading the key:
if (NSUserDefaults.standardUserDefaults().objectForKey("spillereKey") != nil) {
if let testArray : AnyObject? = NSUserDefaults.standardUserDefaults().objectForKey("spillereKey") {
var readArray : [NSString] = testArray! as! [NSString]
println(readArray)
}
}
The "println(readArray)", gives me an output in the console that looks like this:
["erik", "tom", "phil"]
This is my tableView cellForRowAtIndexPath:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell:UITableViewCell = self.myTableView.dequeueReusableCellWithIdentifier("cell") as! UITableViewCell
cell.textLabel!.text = self.mineSpillere[indexPath.row]
return cell;
}
How can i add in the "viewDidLoad" that the key load it´s data into the tableView? I have saved table data into a key using NSUserDefaults. I want to write that data to the UITableView when opening the view controller(viewDidLoad).
Normally you would not have to use the reload command but under certain circumstances it may be appropriate.
self.tableView?.reload()
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.