Problem with nodes userdata

I have been working on a game that uses SKNode's instance property, userData, which is an NSMutableDictionary?. I have been using the scene(.sks) files to add data to these dictionaries. In code I have been adding more data to these dictionaries after the scene loads. I have been doing it this way for about a year with no problem. In Xcode 9.2 I have recently noticed that if I add more data in userData to previous levels in the level editor, my program would crash as if the userData? was now an immutable dictionary. I had to copy the dictionary with .mutablecopy and then replace it before adding more data to avoid the crashes. Is anyone else experiencing this problem? XCode shouldn't be storing this object as immutable when it is updated. Here is the error:


*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<__NSDictionaryI 0x600000eed780> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key category.'

Works just fine after copying and replacing the dictionary.

Replies

There's something more complicated going on here, I think. The error message is not one you'd expect if the dictionary was merely immutable.


You should make sure you have an exception breakpoint (for Obj-C exceptions, since that's what this is) set while running your app from Xcode, then get the backtrace at the time of the crash, and also the line of source code it was executing when it crashed.


Is "category" a key for something that's supposed to be in the userData dictionary?

Thanks for your response. I tried the breakpoint and didn't get any more info. Yes, "category" was the key I was using. I made a test project following these steps:


Step 1. Make a new SpriteKit game template project from Xcode 9.2.

Step 2. Drag a colored sprite into the scene

Step 3. Click on the new colored sprite and hit + in the attributes inspector to add some default user data to the sprites userData?.

Step 4. Add the following function to the GameScene and call it from touchesBegan().

    func setupSpriteDictionaries(){
        self.enumerateChildNodes(withName: "*") {
            node, stop in
            if (node.userData == nil){
                node.userData = NSMutableDictionary()
            }
            if let sprite = node as? SKSpriteNode {
                var dictionary:NSMutableDictionary! = sprite.userData?.mutableCopy() as! NSMutableDictionary
                print ("Sprite n \(String(describing: sprite))")
                var tempcategory = 42
                print("\(sprite.userData!) <---shows data input from .sks file.")


                /*sprite.userData?["category"] = tempcategory*/
                /*sprite.userData?.setValue(tempcategory, forKey: "category")*/

                dictionary["category"] = tempcategory
                sprite.userData = dictionary
                print("\(sprite.userData!) <---shows combined data using new dictionary")

                print("Now set sprite's userData with the new dictionary using the same code that crashed")
                sprite.userData?["category"] = tempcategory+=1
                sprite.userData?.setValue(tempcategory, forKey: "category")
                print("\(sprite.userData!) <---again, combined data from new dictionary")
            }
        }
    }


Step 5. Run to see output. Output was originally posted here but caused this post to be stuck in moderation.


Step 6. Uncomment line 14 and run. Output:

:-[__NSSingleEntryDictionaryI _swift_setObject:forKeyedSubscript:]: unrecognized selector sent to instance 0x60c00003f3a0

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSSingleEntryDictionaryI _swift_setObject:forKeyedSubscript:]: unrecognized selector sent to instance 0x60c00003f3a0'


Step 7. Recomment line 14, uncomment line 15 and run. Output:

*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<__NSSingleEntryDictionaryI 0x608000224da0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key category.'

It still looks to me like an immutable dictionary being stored by XCode. I never had this happen previously and I was adding data to those dictionaries quite a bit.

Thanks for your response Quincey. I replied days ago and it is still being moderated for some reason. Hopefully it will be approved.

I repeated your test, with the same results, and I repeated it one more time in Obj-C to eliminate any possible Swift bridging issues, and got the same crash.


>> It still looks to me like an immutable dictionary being stored by XCode.


It's not clear. The .sks file is a .plist, which is presumably being unarchived at run-time. Depending on how that's done, any mutable collections in the archive may (or may not) unarchived as immutable collections. Even if Xcode archived an immutable dictionary, I think it's still SKNode's responsibility to ensure that its "userData" property is set to an object of the correct class.


In fact, that's why the problem is harder to deal with in Swift: SKNode is violating its API contract about the type of "userData", and there's no source code syntax to test for that. In the Obj-C version of the code, I was able to simply check whether the dictionary was mutable or immutable.


For now, the Swift workaround is what I think you've been doing — a one-time replacement of the unarchived dictionary with a real mutable dictionary.


Please, please, do submit a bug report with your test case!

Thanks for double checking everything Quincey. I submitted a bug report with my test case, #36264900.