Not too long ago, my Swift-based tvOS app was crashing during state restoration. This is the Wake-on-LAN app for tvOS. The crash log said that a class was missing when decoding state restoration data:
*** Terminating app due to uncaught exception 'NSInvalidUnarchiveOperationException', reason: '*** -[UIStateRestorationKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (…) for key (NS.objects); the class may be defined in source code or a library that is not linked' ***
It was pretty puzzling at first. Having the class undefined is pretty much impossible because this is a state restoration case — all de-serialized objects were created by the exact same code. Moreover, that particular missing class (name replaced with ellipsis for brevity) is linked to the main executable — not some dynamic library that possibly not yet loaded at the time of app restoration.
Then I thought, perhaps it’s because of Swift’s name mangling. The allegedly “missing” class is an inner class contained within a view-model class. To Objective-C, it has a pretty cryptic name which showed up in the crash report. I’ve tried creating a “proper” Objective-C alias using the @objc(…)
directive. But still the same crash happened again, but this time it shows the Objective-C alias of the inner class in the crash log.
Finally I’ve solved the problem by forcibly referring to that inner class just prior to decoding state restoration data that may contain its objects. Surprisingly it works. That is in the decodeRestorableState
function, I make a reference to the view-model class to ensure it gets loaded and in turn, it also pre-emptively loads any dependent classes the same way.
The following is a greatly-simplified diagram of how my view model class gets set up:
Keeping in mind of the relationships above, the following is one way to initialize the view-model classes and ensure they are loaded prior to restoring user interface state data.
For the purpose of initializing the class and ensuring that the Objective-C half gets setup, I’ve created a series of classInit
no-value static member that’s sole purpose is just to be referred. At the end of the chain, the Entry
class just calls self
that should instantiate the Objective-C metaclass instance for it.
class ViewController : UIViewController { | |
override func decodeRestorableState(with coder: NSCoder) { | |
super.decodeRestorableState(with: coder) | |
// force class initialization for the sake of state restoration | |
ViewModel.classInit | |
viewModel = coder.decodeObject(of: ViewModel.self, forKey: "viewModel") | |
} | |
} | |
@objc(ViewModel) | |
class ViewModel : NSObject,NSCoding { | |
static let classInit : () = { | |
// force global initialization for state restoration purposes | |
Entry.classInit | |
}() | |
@objc(ViewModel_Entry) | |
class Entry : NSObject,NSCoding { | |
static let classInit : () = { | |
// force global initialization for state restoration purposes | |
_ = Entry.self | |
}() | |
} | |
} |
I found this solution partially inspired by Swift’s preference of lazy initialization. Notably, static members are computed only when it is first referred. I thought to myself, “Maybe that class wasn’t initialized during state restoration? At least the Objective-C portion of it — probably the Class
instance isn’t yet created and registered to the runtime.” Which apparently this is true.
However strangely view controllers and table view cells that are instantiated from storyboards works fine out-of-the-box. They can be instantiated by storyboards without referring to the class beforehand. Then again, maybe Apple made a hack inside the UIKit framework just to support Swift — since this is a common case after all. But my view-model class descend directly from NSObject
, which may not be included as part of this hack.
For the record, this issue occurred in tvOS 10.2.1 / Swift 3.1.
That’s all for now. Take care!
0 thoughts on “State Restoration Hiccups with Swift”
You must log in to post a comment.