How to use For Loop through keys of NSUserDefault Dictionary - ios

I can't get a value out of this computed property (userList)...is my init set up correctly? I'm trying to build a list that will populate the rows in my table view. (*Edit: I've edited my question with a saveData function, but "cannot invoke setObject" with an argument list of type ([String : User], forKey: String)" –
import Foundation
class DataManager {
static let sharedInstance = DataManager()
var users = [String : User]()
init() {
let userDefaults = NSUserDefaults.standardUserDefaults()
if let var userFromDefaults = userDefaults.objectForKey("userKey") as? [String : User] {
users = userFromDefaults
}
else {
// add default values later
}
}
func saveData() {
let userDefaults = NSUserDefaults.standardUserDefaults()
userDefaults.setObject(users, forKey: "userKey")
}
var userList: [String] {
var list: [String] = []
for name in users.keys {
list.append(name)
}
list.sort(<)
return list
}
My viewcontroller:
import UIKit
class NameViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
var users: [String] = DataManager.sharedInstance.userList
User Struct with a method to convert into json:
import Foundation
struct User {
var name = ""
var stores: [Store] = []
init?(json: [String: AnyObject]) {
if let name = json["name"] as? String,
storesJSON = json["stores"] as? [[String: AnyObject]]
{
self.name = name
self.stores = storesJSON.map { Store(json: $0)! }
} else {
return nil
}
}
init() { }
func toJSON() -> [String: AnyObject] {
return [
"name": name,
"stores": stores.map { $0.toJSON() }
]
}
}

You can grab all keys/Values using (from apple documentation)
dictionaryRepresentation() Returns a dictionary that contains a union
of all key-value pairs in the domains in the search list.
Using this you can loop all the elements in the dictionary
for element in NSUserDefaults.standardUserDefaults().dictionaryRepresentation() {
println(element)
//Do what you need to do here
}
You can also retrieve just the keys:
println(NSUserDefaults.standardUserDefaults().dictionaryRepresentation().keys.array);
or just the values
println(NSUserDefaults.standardUserDefaults().dictionaryRepresentation().values.array);

As Matt pointed out in his comment, you need to save contents to NSUserDefaults before you can read them.
Something else that will prevent your code from working:
NSUserDefaults will only save "property list" objects (NSString, NSData, NSDate, NSNumber, NSArray, or NSDictionary objects or their Swift equivalents.)
If you try to save anything else, or a collection containing anything but those data types, the save fails and you get nil when you try to read it back. Your code shows you trying to save a dictionary where the value for each key is an object of type "User". That's going to fail since "User" is not one of the short list of "property list" object types.

This is not a good user of NSUserDefaults, which is meant to store preferences and other small bits of data between app launches. See the documentation on NSUserDefaults.
You should be managing this dictionary of users in memory when you are displaying data, and if you want to persist it to disk between app launches you should use CoreData or make your User class conform to NSCoding and read/write it with archiving. Here's a nice tutorial.

Related

What is the best way to save an array to a file that can be reloaded

Similar to how a game allows you to save your progress, I have an application in iOS that stores a user's progress in a single array. I would like to store that array to a file so that when the user reopens their app this file loads their current status.
The easiest way is to use a NSKeyedArchiver/NSKeyedUnarchiver pair and be sure each of the objects in the array is conforming to NSCoding. You could read here about it.
NSKeyedArchiver.archiveRootObject(myArray, toFile: filePath)
and then unarchiving
if let array = NSKeyedUnarchiver.unarchiveObjectWithFile(filePath) as? [Any] {
objects = array
}
Here is an example object conforming to NSCoding (taken from the article linked above):
class Person : NSObject, NSCoding {
struct Keys {
static let Name = "name"
static let Age = "age"
}
var name = ""
var age = 0
init(dictionary: [String : AnyObject]) {
name = dictionary[Keys.Name] as! String
age = dictionary[Keys.Age] as! Int
}
public func encode(with archiver: NSCoder) {
archiver.encodeObject(name, forKey: Keys.Name)
archiver.encodeObject(age, forKey: Keys.Age)
}
required init(coder unarchiver: NSCoder) {
super.init()
name = unarchiver.decodeObjectForKey(Keys.Name) as! String
age = unarchiver.decodeObjectForKey(Keys.Age) as! Int
}
}
If the objects in your array are all "property list objects" (dictionaries, arrays, strings, numbers (integer and float), dates, binary data, and Boolean values) then you could use the array method write(toFile:atomically:) to save your array to a file, and then reload the resulting file with arrayWithContentsOfFile: or init(contentsOfFile:).
If you have objects in your array's object graph that are not property list objects then that approach won't work. In that case, #BogdanFarca's suggestion of using NSKeyedArchiver/NSKeyedUnarchiver would be a good way to go.

use NSUserDefaults with array of struct type

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")
}
}
}

Create an Array of Object from an Array of Dictionaries with Swift

I'm receiving a JSON dictionary from a web service and I need to map the return values to existing values. Here's essentially what I'm trying to do:
class Contract {
var contractID: String?
var ebState: String?
var ibState: String?
var importerState: String?
var exportersBankRefNo: String?
var importersBankRefNo: String?
}
let contract1 = Contract()
contract1.contractID = "001"
let contract2 = Contract()
contract2.contractID = "002"
// This is the JSON return dictionary
let exportAppnStatusList: [[String: String]] = [["contractID":"001",
"ExporterBankRefNo":"ExporterBankRefNo001",
"ExporterBankState":"ACCEPTED",
"ImporterBankRefNo":"",
"ImporterBankState":"UNKNOWN",
"ImporterState":"UNKNOWN" ],
["contractID":"002",
"ExporterBankRefNo":"ExporterBankRefNo002",
"ExporterBankState":"ACCEPTED",
"ImporterBankRefNo":"ImporterBankRefNo002",
"ImporterBankState":"ACCEPTED",
"ImporterState":"UNKNOWN" ]]
I need to take the exportAppnStatusList and fill in the associated values in the existing contract1 and contract2, mapping by the contractID
This fills the contracts with available information, it ignores contracts where the id could not be found:
for contract in [contract1, contract2] {
if let contractDict = exportAppnStatusList.filter({$0["contractID"] == contract.contractID}).first {
contract.exportersBankRefNo = contractDict["ExporterBankRefNo"]
contract.ebState = contractDict["ExporterBankState"]
contract.importersBankRefNo = contractDict["ImporterBankRefNo"]
contract.ibState = contractDict["ImporterBankState"]
contract.importerState = contractDict["ImporterState"]
}
}
Why not generate the contract object by mapping over the array of dictionaries like this? You'll need to write a custom initializer that takes all these params
exportAppnStatusList.map { (dict:[Stirng:String]) -> Contract in
return Contract(contractID:dict["contractID"],
ebState:dict["ExporterBankState"],
ibState:dict["ImporterBankState"],
importerState:dict["ImporterState"],
exportersBankRefNo:dict["ExporterBankRefNo"],
importersBankRefNo:dict["ImporterBankRefNo"]
}
Try using this init (your class must inherit from NSObject):
init(jsonDict: [String: String]) {
super.init()
for (key, value) in jsonDict {
if class_respondsToSelector(Contract.self, NSSelectorFromString(key)) {
setValue(value, forKey: key)
}
}
}
Then you can do this:
exportAppnStatusList.forEach {
print(Contract(jsonDict: $0))
}

Swift[String: AnyObject] not convertible to T Swift Arrays

I am working inside a Swift Extension. I am trying to append data to an array of the type [[String: AnyObject]]. The reason that this is in an extension is because I have to do this lot's of times to lot's of arrays. The problem is, when I append an object of type: [String: AnyObject], I get the error: Dictionary'<'String, AnyObject'>' Not Convertible to T (the quotes are there because within the carrots nothing showed up).
mutating func appendData(data: [String: [String: AnyObject]]?) {
if data != nil {
for (id, object) in data! {
var mutatingObject = object
mutatingObject["id"] = id
append(mutatingObject)
}
}
}
I am not certain what exactly are you trying to achieve. but take a note - Arrays are generic collections that store specific type. Extension for Array might not know what type will be used in each case, so it cannot simply allow you to store Dictionary<String, AnyObject>.
Here is an example on how to make your code more generic:
extension Array {
mutating func appendData(data: [String: T]?) {
if data != nil {
for (id, object) in data! {
if var mutatingObject = object as? [String : AnyObject] {
mutatingObject["id"] = id
}
append(object)
}
}
}
}

Save struct in class to NSUserDefaults using Swift

I have a class and inside the class is a (swift) array, based on a global struct.
I want to save an array with this class to NSUserDefaults. This is my code:
struct mystruct {
var start : NSDate = NSDate()
var stop : NSDate = NSDate()
}
class MyClass : NSObject {
var mystructs : [mystruct]
init(mystructs : [mystruct]) {
self.mystructs = mystructs
super.init()
}
func encodeWithCoder(encoder: NSCoder) {
//let val = mystructs.map { $0 as NSObject } //this also doesn't work
let objctvtmrec = NSMutableArray(mystructs) //gives error
encoder.encodeObject(objctvtmrec)
//first approach:
encoder.encodeObject(mystructs) //error: [mystructs] doesn't conform to protocol 'anyobject'
}
}
var records : [MyClass] {
get {
var returnValue : [MyClass]? = NSUserDefaults.standardUserDefaults().objectForKey("records") as? [MyClass]
if returnValue == nil
{
returnValue = []
}
return returnValue!
}
set (newValue) {
let val = newValue.map { $0 as AnyObject }
NSUserDefaults.standardUserDefaults().setObject(val, forKey: "records")
NSUserDefaults.standardUserDefaults().synchronize()
}
}
I already subclassed to NSObject, and I know I need NSCoding. But I don't find any way to convert the struct array to an NSMuteableArray or something similar I can store. The only idea until now is to go through each entry and copy it directly to a new array or to use much or objective-c code all over the project, so i never need to convert from swift arrays to objective-c arrays. Both are things I don't want to do.
Swift structs are not classes, therefore they don't conform to AnyObject protocol. You have to rethink your approach. Here are some suggestions:
Convert your struct to final class to enforce immutability
final class MyStruct {
let start : NSDate = NSDate()
let stop : NSDate = NSDate()
}
encoder.encodeObject(mystructs)
Map them as an array dictionaries of type [String: NSDate]
let structDicts = mystructs.map { ["start": $0.start, "stop": $0.stop] }
encoder.encodeObject(structDicts)
NSUserDefaults is limited in the types it can handle: NSData, NSString, NSNumber, NSDate, NSArray, NSDictionary, and Bool. Thus no Swift objects or structs can be saved. Anything else must be converted to an NSData object.
NSUserDefaults does not work the same way as NSArchiver. Since you already have added NSCoder to your classes your best choice might be to save and restore with NSArchiver to a file in the Documents directory..
From the Apple NSUserDefaults Docs:
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.
I've developed a small library which may help. You can use it as a replacement of NSCoding for Swift structs.
You would need to implement a Koting protocol for mystruct:
struct mystruct: Koting {
var start : NSDate = NSDate()
var stop : NSDate = NSDate()
// MARK: - Koting
init?(koter: Koter) {
guard let start: NSDate = koter.dekotObject(forKey: "start"),
let stop: NSDate = koter.dekotObject(forKey: "stop") else {
return nil
}
self.init(start: start, stop: stop)
}
func enkot(with koter: Koter) {
koter.enkotObject(start, forKey: "start")
koter.enkotObject(stop, forKey: "stop")
}
}
Since then you may easily convert the struct to Data and back:
let str = mystruct(start: NSDate(/*...*/), stop: NSDate(/*...*/))
guard let data = str.de_data else { return } // potentially may be nil
let restoredStr = mystruct.de_from(data: data) // if data is irrelevant, returns `nil`
Finally, this is what you do to implement NSCoding:
class MyClass: NSObject, NSCoding {
var mystructs: [mystruct]
init(mystructs: [mystruct]) {
self.mystructs = mystructs
super.init()
}
func encode(with aCoder: NSCoder) {
guard let datas = mystructs.flatMap { $0.de_data } else { return }
aCoder.encode(datas, forKey: "mystructs")
}
required convenience init?(coder aDecoder: NSCoder) {
guard let datas = aDecoder.decodeObject(forKey: "mystructs") as? [Data],
let mystructs = datas.flatMap { mystruct.de_from(data: $0) } else {
return nil
}
self.init(mystructs : mystructs)
}
}
It's pretty much the same code you would write if NSCoding supported Swift structs.
I use this this in my project while coding with Swift 4:
let jsonData = """ {"variable1":1234,"variable2":"someString"}"""
struct MyStruct:Codable{
var variable1 :Int
var variable2:String
}
let whatever = try JSONDecoder().decode(MyStruct.self,from:jsonData)
let encoded =try JSONEncoder().encode(whatever)
UserDefaults.standart.set(encoded, forKey:"encodedData")
to fetch data from UserDefaults
if let data = UserDefaults.standard.object(forKey: "encodedData") as? Data {
let myStruct = try JSONDecoder().decode(MyStruct.self, from: data)
print(myStruct)
}

Resources