Swift 3 User Defaults set dictionary crash - ios

I have worked with Userdefaults many times before but in this case can't seem to figure out what would cause this crash to occur even though it is such a simple case.
Goal: UserDefaults.standard.set a dictionary with a value of an array of dictionary's
struct UDKey {
static let activityCount = "ActivityCount"
}
class MyClass {
let defaults = UserDefaults.standard
func initialize() {
let years = [ 2017: [[1:2],
[2:4],
[3:6],
[4:8],
[5:10],
[6:12],
[7:14],
[8:16],
[9:17],
[10:18],
[11:19],
[12:20]]]
if defaults.value(forKey: UDKey.activityCount) == nil {
defaults.set(years, forKey: UDKey.activityCount) // Crash - lldb
}
}
}
I have attempted setting others values with success, such as simple integers and strings. but this dictionary with a value of an array of dictionary's is having a problem.
What could be the cause for a UserDefaults Crash when setting a value.

Related

how to don't get a nil value from NSUserDefaults in ViewDidLoad

I have a case where when my viewControler starts in viewDidLoad I have to load some data using NSUserDefaults.standardUserDefaults() which doesn't exist in this monent. This data are saved when I tap send Button in the same viewController and I need this data when I open this viewController again. Now it looks like that:
var orderHistory = [String:String]()
vievDidLoad(){
let userDefault = NSUserDefaults.standardUserDefaults()
let orderHistory = userDefault.objectForKey("orderHistory")
if orderHistory == nil {
self.orderHistory = orderHistory["name":"", "surname":""] as! [String:String]
} else {
self.orderHistory = orderHistory as! [String:String]
{
}// end viewDidLoad
In this moment I recieve an imformation, I have a problem with memory. How should I avoid this situation?
As Leo Dabus said you should try using the ?? nil coalescing operator.
ObjectForKey does not provide a default value because it doesnt know what kind of object it is until you set it the first time. This results in a nil crash if you try to access it value without having it set once.
Compare this to say "boolForKey" where you dont have to do this, because it knows you are dealing with boolean values and therefore defaults to false automatically.
You also dont have to create 2 orderHistory dictionaries, it just makes your code more confusing.
Try this instead
var orderHistory = [String:String]()
vievDidLoad(){
let userDefault = NSUserDefaults.standardUserDefaults()
orderHistory = userDefault.objectForKey("orderHistory") as? [String: String] ?? orderHistory
//than just use the 1 dictionary without the if statements or creating another one.
}// end viewDidLoad
You check if saved data exists (as? [String: String]) and update the dictionary accordingly. If no saved data exists it will use the default values in orderHistory (?? orderHistory), which in your case is an empty dictionary.
This way you dont have to do a nil check, its all done in that one line.
Also try putting your keys into structs or global files so that you avoid typos. I see people not doing this all the time and its really bad practice.
So for example, above your class create a struct
struct Key {
static let orderHistory = "OrderHistory"
}
and use it like so
...objectForKey(Key.orderHistory)
This code makes no sense:
if orderHistory == nil
{
self.orderHistory = orderHistory["name":"", "surname":""] as! [String:String]
}
The if statement guarantees that orderHistory is nil, thereby guaranteeing that the attempt to fetch keys from orderHistory will crash. Actually, that doesn't look like valid Swift. I would expect that line to throw a compiler error.
Are you trying to create a new dictionary?
If so, your code should read like this:
if orderHistory == nil
{
self.orderHistory = ["name":"", "surname":""]
}

iOS: how to convert NSUserDefaults stored AnyObject type to array

I have put an array into NSUserDefaults() like so:
NSUserDefaults.standardUserDefaults().setObject(users, forKey: "usersArray")
NSUserDefaults.standardUserDefaults().synchronize()
Then I pull it out like so:
fetchedUserArray = NSUserDefaults.standardUserDefaults().objectForKey("usersArray")
The problem I am facing is that once it is removed from NSUserDefaults it is of type NSArray, preventing me from manipulating it like a Swift array. I have tried this to convert the type, however the compiler does not recognize the variable "castedUsersArray" when it is used later in the code despite not raising any errors upon type casting:
var fetchedArray = NSUserDefaults.standardUserDefaults().objectForKey("usersArray") as? NSArray
var castedUsersArray = fetchedArray as AnyObject as [String]
I have spent a very long time on this with no success. The type constraints of Swift are driving me nuts.
Thank you,
Nick
You almost had it. Don't cast the objectForKey to an Array but rather an Array containing a certain type like you did with castedUsersArray. Don't throw away type information like you did with fetchedArray.
let users = ["Amy", "Bill", "Cindy"]
NSUserDefaults.standardUserDefaults().setObject(users, forKey: "usersArray")
NSUserDefaults.standardUserDefaults().synchronize()
let fetched = NSUserDefaults.standardUserDefaults().objectForKey("usersArray") as? [String] ?? []
The nil coalescing at the end of the line handles the empty NSUserDefaults case.
NSUserDefaults has a specific method to get your stored string arrays called stringArrayForKey:
let stringArray = ["Hello","playground"]
store your string array
NSUserDefaults.standardUserDefaults().setObject(stringArray, forKey: "stringArray")
load it when needed
if let loadedStringArray = NSUserDefaults.standardUserDefaults().stringArrayForKey("stringArray") {
print(loadedStringArray) // ["Hello", "playground"]
}

how to save and read dictionary of touples to NSUserDefaults?

I have an dictionary = String: ([(String)], [(Int)], NSDate, Bool, [(String)]) and I attempted to deconstruct it into seperate arrays when then app calls applicationWillTerminate
var codes = [(String)]()
var messages = [[String]]()
var senders = [[Int]]()
var dates = [(NSDate)]()
var bools = [(Bool)]()
var pairs = [[String]]()
for code in self.dictionary.keys {
codes.append(code)
messages.append(self.dictionary[code]!.0)
senders.append(self.dictionary[code]!.1)
dates.append(self.dictionary[code]!.2)
bools.append(self.dictionary[code]!.3)
pairs.append(self.dictionary[code]!.4)
}
self.userDefaultsMessages.setObject(codes, forKey: "userMessagesArrays")
self.userDefaultsSenders.setObject(messages, forKey: "userSentArrays")
self.userDefaultsDates.setObject(senders, forKey: "userDatesArray")
self.userDefaultsDeletedBool.setObject(dates, forKey: "userDeletedArrays")
self.userDefaultsPairs.setObject(bools, forKey: "userPairsArrays")
self.userDefaultsCodeKeys.setObject(pairs, forKey: "userCodesArrays")
self.userDefaultsCodeKeys.synchronize()
self.userDefaultsMessages.synchronize()
self.userDefaultsSenders.synchronize()
self.userDefaultsDates.synchronize()
self.userDefaultsDeletedBool.synchronize()
self.userDefaultsPairs.synchronize()
and then I attempt to pull it all back together when the app calls applicationDidBecomeActive
//read
if let savedCodesArray : AnyObject? = self.userDefaultsCodeKeys.objectForKey("userCodesArrays") {
self.userCodes = savedCodesArray! as! [String]
if let savedMessagesArray : AnyObject? = self.userDefaultsCodeKeys.objectForKey("userMessagesArrays") {
self.Usermessages = savedMessagesArray! as! [[String]]
if let savedSendersArray : AnyObject? = self.userDefaultsCodeKeys.objectForKey("userSentArrays") {
self.Usersenders = savedSendersArray! as! [[Int]]
if let savedDatesArray : AnyObject? = self.userDefaultsCodeKeys.objectForKey("userDatesArray") {
self.Userdates = savedDatesArray! as! [NSDate]
if let savedBools : AnyObject? = self.userDefaultsCodeKeys.objectForKey("userDeletedArrays") {
self.Userbools = savedBools! as! [Bool]
if let savedPairs : AnyObject? = self.userDefaultsCodeKeys.objectForKey("userPairsArrays") {
self.Userpairs = savedPairs! as! [[String]]
var indexPath: Int = 0
for code in self.userCodes {
self.dictionary[code]! = [self.Usermessages[indexPath], self.Usermessages[indexPath], self.Userdates[indexPath], self.Userbools[indexPath], self.Userpairs[indexPath]]
}
}
}
}
}
}
}
I am fairly new to iOS development and could use help, how would i save the single dictionary = String: ([(String)], [(Int)], NSDate, Bool, [(String)]) to NSUserDefaults and then later read it.. the documentation was not very helpful since it only worked with simple dictionaries
the code looks incredibly cumbersome so I know I can't be doing it right. It should be a simple solution since I only have one variable to save to NSUserDefaults.
Short answer: You can't.
NSUserDefaults will only record "property list objects" (dictionaries, arrays, strings, numbers (integer and float), dates, binary data, and Boolean values).
You can't save any other types of data into NSUserDefaults, or into a property list. The only solution is to convert other data types into those types.
Tuples are not one of those types, so they can't be saved into user defaults.
First, your interaction with NSUserDefaults is completely wrong. You only need to interact with the shared NSUserDefaults.sharedUserDefaults() instance, not create separate instances, which seems to be what you're doing (i.e. userDefaultsMessages, userDefaultsSenders, etc.).
Second, you don't need to call synchronize() at all. There are very few conditions under which you need to call it manually, and this isn't one of them.
Third, the easiest way to store a particular tuple in NSUserDefaults is to convert it into an Array (or NSArray) and store the result. Of course, this assumes that the tuple contains only types that can be serialized, which your example seems to contain. Unfortunately there's no general solution to this, but creating an array from a tuple is straight forward, as you can just map the tuple indices to array indices.
Finally, such large tuples are usually the result of poor design somewhere along the line. Perhaps refactoring would help resolve your storage issue?

Cant set Array in NSUserDefaults

var arr = defaults.arrayForKey("kWeeks") as [WeekReport]?
if arr? {
var arr2 = arr!
arr2.append(report)
defaults.setObject(arr2, forKey: "kWeeks") // Crash
defaults.synchronize()
}
I try to set arr2 which will be the add after optional checking. But it crashes on setObject() with error: EXC_BAD_ACCESS
Ive checked, with println() and arr2 is not nil and contains the element which is appended in the sample above. And report is of type WeekReport.
var arr = defaults.arrayForKey("kWeeks") as [WeekReport]?
Is doing a forced casting to an optional array of WeekReport. Instead, you really want to do an optional casting:
if let arr = defaults.arrayForKey("kWeeks") as? [WeekReport] {
arr.append(report)
defaults.setObject(arr, forKey: "kWeeks")
defaults.synchronize()
}
This says, "if there is an array for "kWeeks" that can be cast to an array of WeekReport, do the following with it as arr"
You also need to make sure that WeekReport inherits from the NSCoding protocol so that it can be converted to data.

NSUserDefaults - How to tell if a key exists

I'm working on a small iPhone app, and I am using NSUserDefaults as my data persistence. It only has to keep track of a few things, such as some names and some numbers so I figure I might as well keep it simple.
I found this page for some reference, but I don't think it can answer my question. Basically, I want to be able to check if a value (or a key) already exists in the NSUserDefaults and then do something accordingly.
Some examples: The app starts up, if this is the first time it starts up it outputs an alert saying welcome. To tell if this is first time it has opened it reads the UserDefaults and checks.
Example 2: It says, "Hello [Name]", where Name is something you have entered. If you have opened the app and there is no name, it should say "Hello World." I need to check if you have entered a name already and act accordingly. The name would be stored in NSUserDefaults.
Some help here? I'd really appreciate it!
objectForKey: will return nil if it doesn't exist.
As mentioned above it wont work for primitive types where 0/NO could be a valid value. I am using this code.
NSUserDefaults *defaults= [NSUserDefaults standardUserDefaults];
if([[[defaults dictionaryRepresentation] allKeys] containsObject:#"mykey"]){
NSLog(#"mykey found");
}
The objectForKey: method will return nil if the value does not exist. Here's a simple IF / THEN test that will tell you if the value is nil:
if([[NSUserDefaults standardUserDefaults] objectForKey:#"YOUR_KEY"] != nil) {
...
}
Swift 3 / 4:
Here is a simple extension for Int/Double/Float/Bool key-value types that mimic the Optional-return behavior of the other types accessed through UserDefaults.
(Edit Aug 30 2018: Updated with more efficient syntax from Leo's suggestion.)
extension UserDefaults {
/// Convenience method to wrap the built-in .integer(forKey:) method in an optional returning nil if the key doesn't exist.
func integerOptional(forKey: String) -> Int? {
return self.object(forKey: forKey) as? Int
}
/// Convenience method to wrap the built-in .double(forKey:) method in an optional returning nil if the key doesn't exist.
func doubleOptional(forKey: String) -> Double? {
return self.object(forKey: forKey) as? Double
}
/// Convenience method to wrap the built-in .float(forKey:) method in an optional returning nil if the key doesn't exist.
func floatOptional(forKey: String) -> Float? {
return self.object(forKey: forKey) as? Float
}
/// Convenience method to wrap the built-in .bool(forKey:) method in an optional returning nil if the key doesn't exist.
func boolOptional(forKey: String) -> Bool? {
return self.object(forKey: forKey) as? Bool
}
}
They are now more consistent alongside the other built-in get methods (string, data, etc.). Just use the get methods in place of the old ones.
let AppDefaults = UserDefaults.standard
// assuming the key "Test" does not exist...
// old:
print(AppDefaults.integer(forKey: "Test")) // == 0
// new:
print(AppDefaults.integerOptional(forKey: "Test")) // == nil
Extend UserDefaults once to don't copy-paste this solution:
extension UserDefaults {
func hasValue(forKey key: String) -> Bool {
return nil != object(forKey: key)
}
}
// Example
UserDefaults.standard.hasValue(forKey: "username")
"objectForKey will return nil if it doesn't exist." It will also return nil if it does exist and it is either an integer or a boolean with a value of zero (i.e. FALSE or NO for the boolean).
I've tested this in the simulator for both 5.1 and 6.1. This means that you cannot really test for either integers or booleans having been set by asking for "the object". You can get away with this for integers if you don't mind treating "not set" as if it were "set to zero".
The people who already tested this appear to have been fooled by the false negative aspect, i.e. testing this by seeing if objectForKey returns nil when you know the key hasn't been set but failing to notice that it also returns nil if the key has been set but has been set to NO.
For my own problem, that sent me here, I just ended up changing the semantics of my boolean so that my desired default was in congruence with the value being set to NO. If that's not an option, you'll need to store as something other than a boolean and make sure that you can tell the difference between YES, NO, and "not set."
I just went through this, and all of your answers helped me toward a good solution, for me. I resisted going the route suggested by, just because I found it hard to read and comprehend.
Here's what I did. I had a BOOL being carried around in a variable called "_talkative".
When I set my default (NSUserDefaults) object, I set it as an object, as I could then test to see if it was nil:
//converting BOOL to an object so we can check on nil
[defaults setObject:#(_talkative) forKey:#"talkative"];
Then when I went to see if it existed, I used:
if ([defaults objectForKey:#"talkative"]!=nil )
{
Then I used the object as a BOOL:
if ([defaults boolForKey:#"talkative"]) {
...
This seems to work in my case. It just made more visual sense to me.
Try this little crumpet:
-(void)saveUserSettings{
NSNumber* value;
value = [NSNumber numberWithFloat:self.sensativity];
[[NSUserDefaults standardUserDefaults] setObject:value forKey:#"sensativity"];
}
-(void)loadUserSettings{
NSNumber* value;
value = [[NSUserDefaults standardUserDefaults] objectForKey:#"sensativity"];
if(value == nil){
self.sensativity = 4.0;
}else{
self.sensativity = [value floatValue];
}
}
Treat everything as an object. Seems to work for me.
Swift version to get Bool?
NSUserDefaults.standardUserDefaults().objectForKey(DefaultsIsGiver) as? Bool
In Swift3, I have used in this way
var hasAddedGeofencesAtleastOnce: Bool {
get {
return UserDefaults.standard.object(forKey: "hasAddedGeofencesAtleastOnce") != nil
}
}
The answer is great if you are to use that multiple times.
I hope it helps :)
Swift 3.0
if NSUserDefaults.standardUserDefaults().dictionaryRepresentation().contains({ $0.0 == "Your_Comparison_Key" }){
result = NSUserDefaults.standardUserDefaults().objectForKey(self.ticketDetail.ticket_id) as! String
}

Resources