I have problems to figure out how to save my string of type "RiskEntry" with NSUserDefaults. I already went through some other posts, but somehow I still did not manage to solve this particular issue.
Let me explain what the code from below does right now: I get some data from my class CustomCell in the following code snippet. Here I first check with an "identifier" which array to update with the new array value "consequences".
It all works fine and the updated array is stored in riskEntry.
However, I cannot work out how to store this with NSUserDefaults now. When I try it with e.g. riskItemDefaults.set(riskEntry, forKey: "riskItem")
I get an exception error.
Any idea what I am doing wrong here?
SWIFT3 (I removed all code not relevant for this question)
class: RiskPlan: UIViewController, UITableViewDelegate, UITableViewDataSource, CustomCellUpdaterDelegate {
var riskEntry = [RiskEntry]()
var riskItemDefaults = UserDefaults.standard
// ------ start of delegate function (receiving from CustomCell) ---------
func transferData(consequencesTranferred: String, identifier: String) {
if let index = riskEntry.index(where: {$0.title as String == identifier}) {
riskEntry[index].consequences = consequencesTranferred
} else {
print ("nothing")
}
// save with NSUserDefaults
riskItemDefaults.set(riskEntry, forKey: "riskItem")
}
}
This is my struct:
public struct RiskEntry {
let title: String
var consequences: String
}
my Custom Cell
// ---------------- delegate to transfer entered data to VC -----------------
protocol CustomCellUpdaterDelegate {
func transferData(consequencesTranferred: String, identifier: String)
}
// ---------------- start of class CellCustomized -----------------
class CustomCell: UITableViewCell, UIPickerViewDataSource, UIPickerViewDelegate, UITextViewDelegate {
var delegate: CustomCellUpdaterDelegate?
// text fields, text views and picker views
@IBOutlet weak var riskTitle: UITextView!
@IBOutlet weak var consequences: UITextView!
// ---------------- listener for text view to save input in string when editing is finished -----------------
func textViewDidEndEditing(_ textView: UITextView) {
if textView.tag == 1 {
textConsequences = consequences.text
nameIdentifier = riskTitle.text
delegate?.transferData(consequencesTranferred: self.textConsequences, identifier: nameIdentifier)
} else {
print ("nothing")
}
}
}
The problem is you can't save your custom array in NSUserDefaults. To do that you should change them to NSData then save it in NSUserDefaults
Here is the code I used in my project it's in swift 2 syntax and I don't think it's going be hard to convert it to swift 3
let data = NSKeyedArchiver.archivedDataWithRootObject(yourObject);
NSUserDefaults.standardUserDefaults().setObject(data, forKey: "yourKey")
NSUserDefaults.standardUserDefaults().synchronize()
and to the get part use this combination
if let data = NSUserDefaults.standardUserDefaults().objectForKey("yourKey") as? NSData {
let myItem = NSKeyedUnarchiver.unarchiveObjectWithData(data) as? yourType
}
hope this will help
Saving objects in UserDefaults
have very specific restrictions:
set(_:forKey:) reference:
The value parameter can be only property list objects: NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. For NSArray and NSDictionary objects, their contents must be property list objects.
You need to serialize your model, either using NSCoding or as an alternative using JSON, to map to a supported value by UserDefaults
.
The closest type to a Swift struct that UserDefaults supports might be an NSDictionary. You could copy the struct elements into an Objective C NSDictionary object before saving the data.
I was able to program a solution based on @ahruss (How to save an array of custom struct to NSUserDefault with swift?). However, I modified it for swift 3 and it also shows how to implement this solution in a UITableView. I hope it can help someone in the future:
Add the extension from below to your structure (adjust it to your own variables)
Save the required array item like this:
let encoded = riskEntry.map { $0.encode() }
riskItemDefaults.set(encoded, forKey: "consequences")
riskItemDefaults.synchronize()
Load your item like this
let dataArray = riskItemDefaults.object(forKey: "consequences") as! [NSData]
let savedFoo = dataArray.map { RiskEntry(data: $0)! }
If you'd like to show the saved array item in your cells, proceed this way:
cell.consequences.text = savedFoo[indexPath.row].consequences as String
Here is the complete code, modified for Swift3
structure
// ---------------- structure for table row content -----------------
struct RiskEntry {
let title: String
var consequences: String
}
extension
extension RiskEntry {
init?(data: NSData) {
if let coding = NSKeyedUnarchiver.unarchiveObject(with: data as Data) as? Encoding {
title = coding.title as String
consequences = (coding.consequences as String?)!
} else {
return nil
}
}
func encode() -> NSData {
return NSKeyedArchiver.archivedData(withRootObject: Encoding(self)) as NSData
}
private class Encoding: NSObject, NSCoding {
let title : NSString
let consequences : NSString?
init(_ RiskEntry: RiskEntry) {
title = RiskEntry.title as NSString
consequences = RiskEntry.consequences as NSString?
}
public required init?(coder aDecoder: NSCoder) {
if let title = aDecoder.decodeObject(forKey: "title") as? NSString {
self.title = title
} else {
return nil
}
consequences = aDecoder.decodeObject(forKey: "consequences") as? NSString
}
public func encode(with aCoder: NSCoder) {
aCoder.encode(title, forKey: "title")
aCoder.encode(consequences, forKey: "consequences")
}
}
}