Swift mutable dictionary being treated as immutable - swift

I am trying to implement In App Purchases, and I am tracking which purchases a user has purchased via NSUserDefaults. I have a function that sets the values of each purchase, but when it runs, I get an error saying that I am mutating the dictionary of purchase values even though the dictionary is declared with a var instead of a let and is an NSMutableDictionary. Sometimes it does work, but most of the time it doesn't. I get a few warnings about declaring my variables with let instead of var, but I ignore them to give my variables maximum mutability. Why does this not work?
The error I get is:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '-[__NSCFDictionary setObject:forKey:]: mutating method sent to immutable object'
Code:
static func setPurchased(purchase:PurchaseID, value:Bool)
{
let defaults = NSUserDefaults.standardUserDefaults()
if (defaults.objectForKey(PURCHASES_KEY) == nil)
{
initializePurchases() // go set some initial values
}
if var purchases = (defaults.objectForKey(PURCHASES_KEY) as? NSMutableDictionary) // WARNING: should be declared with let because not mutated
{
print("setting purchase \(purchase.id()) to \(value)")
var key = purchase.id() // WARNING: should be declared with let because not mutated
purchases[key] = value // CRASH HERE
defaults.setObject(purchases, forKey:PURCHASES_KEY)
defaults.synchronize()
}
}

This is not the right way of converting an immutable dictionary into its mutable counterpart.
var already ensures that whatever is returned from defaults.objectForKey(PURCHASES_KEY) will be copied as a mutable type so all you need is specify the type of the mutable object which in our case can safely be Dictionary<String: AnyObject> if you are sure all keys are String type:
if var purchases = defaults.objectForKey(PURCHASES_KEY) as? Dictionary<String: AnyObject> {
...
purchases[key] = value
}
Please see this SO question for more information about mutability/immutability in collection types.

Related

This class is not key value coding-compliant for the key openKey. How to fix errors

The source of the error.
#objc func selectHeader(sender: UIButton) -> Void {
var testSection = NSDictionary()
var openValue = String()
testSection = self.arrHeader.object(at: sender.tag) as! NSDictionary
openValue = testSection.value(forKey: self.isOpenKey) as! String
// value change
if openValue == "Y" {
testSection.setValue("N", forKey: self.isOpenKey) <—— App crash self.isOpenKey == “openKey”
} else {
testSection.setValue("Y", forKey: self.isOpenKey) <—— App crash self.isOpenKey == “openKey”
}
let section: NSIndexSet = NSIndexSet(index: (sender.tag))
self.collectionView.reloadSections(section as IndexSet)
}
TestSection.setValue crashes with the following error.
2018-02-09 16:43:38.141[10730:2091077] * Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<_TtGCs26_SwiftDeferredNSDictionarySSSS_ 0x1665dea0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key openKey.'
* First throw call stack:
libc++abi.dylib: terminating with uncaught exception of type NSException
What is the problem?
You are using the NSDictionary wrong.
First - in response to the comments of #Willeke, you cannot modify an NSDictionary, you need a NSMutableDictionary. You even better might want to use Dictionary.
Then, you should call setObject:forKey:, not setValue:forKey:
I must admit that this API might be a little confusing, because it is so look-alike:
What you want is to map a Dictionary key entry to an object, which is done by setObject:forKey:, in your example (with a mutable dictionary): testSection.setObject("N", forKey: "openKey"). Using the Swift way with a Swift Dictionary, you would write testSection["openKey"] = "N"
When using setValue:forKey:, you are Key-Value-Coding, e.g. you are calling a method ("sending a message") to the NSDictionary instance, and the name of the Message is what you provided in the key value. This would result in something like calling textSection.openKey = "N", thus the exeption
this class is not key value coding-compliant for the key openKey
In the special case of NSMutableDictionary, as #matt mentioned, both setValue:forKey and setObject:forKey behave the same as long as you do not provide an NSString as a key parameter - in that latter case, KVC will be used.
What is the problem?
The "problem" is that you are using NSDictionary in a Swift program, plus you are using key-value coding (value(forKey:), setValue(_:forKey:)) all over the place. Stop it. This is Swift, not Objective-C. Use Swift types and Swift means of talking to them.

How to fetch core data objects into Dictionary in Swift?

I've saved objects in core data, and I am looking how to fetch those objects as a Dictionary
Here is an example of my code where sections is keys for the dictionary and Company as an array of core data objects.
private var companies = Dictionary<String, Array<Company>>()
private var sections: [String] = ["Pending", "Active", "Pending"]
override func viewWillAppear(_ animated: Bool) {
let fetchRequest : NSFetchRequest<Company> = Company.fetchRequest()
let moc = DatabaseController.getContext()
do {
let request = try moc.fetch(fetchRequest)
for case let (index, object) in request.enumerated() {
companies[sections[index]]!.append(object)
}
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}}
When I am trying to execute my code, I have an error:
fatal error: unexpectedly found nil while unwrapping an Optional value
Could anyone help me with that issue?
That error message means that you're force unwrapping an optional that doesn't have a value. In other words you're using ! where you shouldn't. (You should basically never use the force unwrap operator (!).)
Let's look at the line where you do that:
companies[sections[index]]!.append(object)
If we break this down and add the inferred types we have:
let section: String? = sections[index]
let companyArray: Array<Company> = companies[section]!
You're crashing because companies starts off empty, so asking for any of the arrays will return nil. (Actually, I'm not sure how your code is compiling, since you can't subscript into the dictionary with an optional.)
However, if you fix that, we still have a problem because you're using the index of the fetched array to look up the section. If you have more than three companies, that will start to fail.
I suspect you want something like:
for let company in result {
if var companyArray = companies[company.status] {
companyArray.append(company)
} else {
companies[company.status] = [company]
}
}
Where status is an invented property on Company that returns a String like "Pending" or "Active".
I've found the solution, just need to use NSFetchResultController in order to display data in TableView by different sections.

Dictionary saved to NSUserDefaults always returns nil

I'm trying to save a dictionary to NSUserDefaults using the setObject() function but when I use the objectForKey() function to retrieve the dictionary it returns nil. Why is this happening?
var data = NSUserDefaults.standardUserDefaults();
var scoreboard = [Int : String]()
let scores = "scoresKey"
scoreboard[3] = "spencer"
scoreboard[6] = "brooke"
scoreboard[11] = "jason"
data.setObject(scoreboard, forKey: scores)
data.objectForKey(scores) // Returns nil
The first problem was that it's not possible to use NSUserDefaults in a Playground.
See: https://stackoverflow.com/a/31210205/3498950
A second problem is found when the code above runs in a normal iOS project. It throws an NSInvalidArgumentException since the dictionary was a non-property list object because the keys needed to be of type String.
Although NSDictionary and CFDictionary objects allow their keys to be
objects of any type, if the keys are not string objects, the
collections are not property-list objects.
See: "What is a Property List?" in the Apple Docs

error: execution was interrupted, reason: breakpoint 1.2. Xcode 7.1, Swift

So the context is that I made a realm object and is giving one of it's variables a value, to do this I go ahead and call an instance of this object, then I connect to my server, get some value, then say something like
let someObject = someObjectClass() //this being a realm object class
someQuerySuccessBlock { (success, error) -> void in
...
if let someValue = objects[0].value {
someObject.id = someValue //this line is where the issue is
}
...
})
let realm = RLMRealm.defaultRealm()
realm.beginWriteTransaction
realm.addObject(someObject)
realm.commitWriteTransaction
Error in llvm is error: execution was interrupted, reason: breakpoint 1.2.
Error does not show unless i make a breakpoint for all exceptions.
Also to note that code does compile, does run, will not cause a crash (but simply.. not run any of the code from that specific line onwards. That someObject does get saved and created, but the field that is to be assigned simply did not get assigned, etc
After some testing around, turns out this is because the realm object was already saved into Realm, where as the query block is async, it was trying to write into a variable of an object that had already been added.
Seems like the error was only like this because what I was trying to edit was the primaryKey of the object?
My fix:
let someObject = someObjectClass() //this being a realm object class
someQuerySuccessBlock { (success, error) -> void in
...
if let someValue = objects[0].value {
someObject.id = someValue //this line is where the issue is
let realm = RLMRealm.defaultRealm()
realm.beginWriteTransaction
realm.addObject(someObject)
realm.commitWriteTransaction
}
...
})
If you try to edit the primary key of a saved object, then you will hit an assertion. Primary keys in Realm are immutable. Depending on your exact needs for your use case, you may want to create a new instance of your object class and assign all new properties which should be saved. You can add this new object then in a write transaction with -createOrUpdateInRealm:withValue:. Note: Be careful with to-one relations and other nullable properties as the merging strategy is here that null values are overwritten.

Swift: CoreData - unrecognized selector sent to instance when insert row

This error is strange:
-[<>.Tips setTipName:]: unrecognized selector sent to instance <>
the name setTipName does not occur anywhere in the code but there is a variable tipName (note the lower case "t"
I am attempting to insert a row into a CoreData entity
class Tips: NSManagedObject {
#NSManaged var sectionNumber: NSNumber
#NSManaged var tipDescription: String
#NSManaged var viewName: String
#NSManaged var tipName: String
#NSManaged var tipOrder: NSNumber
#NSManaged var tipType: String
#NSManaged var tipLinkName: String
}
Here is the code doing the insert:
func createNewTips ()
{
// set create all switches and set to off
var error: NSError? = nil
var textCount = textArray.count
for var i = 0; i<textCount; ++i
{
var newTip = NSEntityDescription.insertNewObjectForEntityForName("Tips", inManagedObjectContext: managedObjectContext!) as! Tips
newTip.viewName = viewName
newTip.tipName = textArray [i].tipName
NSLog("after tipName")
newTip.tipDescription = textArray[i].tipDescription
NSLog("after set tipDescription")
newTip.tipOrder = textArray[i].tipOrder
NSLog("after set tipOrder")
newTip.sectionNumber = textArray[i].sectionNumber
NSLog("after set sectionNumber")
newTip.tipType = textArray[i].type
NSLog("after set type")
newTip.tipLinkName = textArray[i].tipLinkName
var error: NSError? = nil
if !managedObjectContext!.save(&error) {
NSLog("error &d", error!)
abort()
} // end save
} // end loop
} // end of createNewSwitches
I've recreated the data model several times
I've also changed the order of the attributes and the error occurs on a different attribute .. I've noticed that it appears to be the first attribute when I move the viewName attribute later in the list.
Here is the code in textArray
var textArray:[(sectionNumber: Int, tipOrder: Int, tipName: String, tipDescription: String, type: String, tipLinkName:String)] =
[
(1,0,"sw1","Check in and around your home for damage","text",""),
(1,1,"sw2","Dispose of any spoiled or contaminated foods, especially after a power outage. If you’re not sure, throw it out. You can check the food safety tips below","text",""),
(1,2,"sw3","Encourage family members to talk about their experience and their feelings, especially children","text",""),
(1,3,"sw4","Contact other family members to let them know that you are safe","text",""),
(2,0,"sw5","Check Utilities","link",""),
(3,0,"sw6","Food Safety Tips","link",""),
]
Any suggestions about this?
The method setTipName is an auto-generated setter method created for NSManagedObject subclasses behind the scenes. It won't appear in code even if you use the modeler to create the NSManagedObject Subclass.
Core Data has to wrap all all modeled attributes in getters and setters to ensure that key-value observing, validation etc gets trigger.The naming is automatic and follows the old Objective-C convention. There will also be either a tipName or getTipName method.
I suspect you are not actually getting a Tip object back from the insertion. I'm behind the curve on Swift but I'm good with Core Data, I don't think the "as!" cast should be needed here.
var newTip = NSEntityDescription.insertNewObjectForEntityForName("Tips", inManagedObjectContext: managedObjectContext!) as! Tips
… because the compiler should be expecting a Tips object. The empty "<>" in the error message suggest that you don't in fact have a Tips object (or did you edit the error message.)
Were this Objective-C the answer to the error would definitely be that you have the wrong class returned from the insert. The most common causes of the error are:
Failing to assign a NSManagedObject subclass name in the Core Data model editor and leaving it just a generic NSManageObject
Misspelling the class name in the model e.g. "Tip", "tips", Tipps" or some such.