Skip to content

Instantly share code, notes, and snippets.

@bnickel
Last active January 12, 2023 09:47
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save bnickel/410a1bdc02f12fbd9b5e to your computer and use it in GitHub Desktop.
Save bnickel/410a1bdc02f12fbd9b5e to your computer and use it in GitHub Desktop.
A few friendly methods to help you detect state restoration problems in your non-storyboard apps.
// USAGE:
// Call RestorationDefender.printViewControllerClassesThatAreProbablyNotRestorable() to print a list of view controllers that will probably not return from state restoration.
// Call RestorationDefender.crashWhenViewControllersDoNotImplementStateRestoration() to crash your app when a view controller appears without setting restorationIdentifier and restorationClass.
// Call RestorationDefender.shoutWhenViewControllersDoNotImplementStateRestoration() to print a big message when a view controller appears without setting restorationIdentifier and restorationClass.
import Foundation
private func objc_getClassList() -> [AnyClass] {
let expectedClassCount = objc_getClassList(nil, 0)
var allClasses = UnsafeMutablePointer<AnyClass?>.alloc(Int(expectedClassCount))
var autoreleasingAllClasses = AutoreleasingUnsafeMutablePointer<AnyClass?>(allClasses) // Huh? We should have gotten this for free.
let actualClassCount:Int32 = objc_getClassList(autoreleasingAllClasses, expectedClassCount)
var classes = [AnyClass]()
for i in 0 ..< actualClassCount {
if let currentClass: AnyClass = allClasses[Int(i)] {
classes.append(currentClass)
}
}
allClasses.dealloc(Int(expectedClassCount))
return classes
}
private func class_getName(cls:AnyClass!) -> String! {
if cls != nil {
let className:UnsafePointer<Int8> = class_getName(cls)
return NSString(bytes: UnsafePointer<Void>(className), length: Int(strlen(className)), encoding: NSUTF8StringEncoding)
} else {
return nil
}
}
private func class_hasSuperclass(cls:AnyClass, superclass:AnyClass) -> Bool {
var currentClass:AnyClass? = cls
do {
currentClass = class_getSuperclass(currentClass)
// currentClass !== superclass will crash if we're dealing a root swift class. Assume it's not what we're looking for.
if let className = class_getName(currentClass) {
if className == "SwiftObject" || className.hasPrefix("Swift.") {
currentClass = nil
}
}
} while currentClass != nil && currentClass !== superclass
return currentClass != nil
}
private func class_getAllSubclasses(cls:AnyClass) -> [AnyClass] {
return objc_getClassList().filter() { class_hasSuperclass($0, cls) }
}
private func class_swizzleSelectors(cls:AnyClass!, original:Selector, modified:Selector) -> Bool {
let originalMethod = class_getInstanceMethod(cls, original)
let modifiedMethod = class_getInstanceMethod(cls, modified)
if originalMethod != nil && modifiedMethod != nil {
class_addMethod(cls, original, class_getMethodImplementation(cls, original), method_getTypeEncoding(originalMethod))
class_addMethod(cls, modified, class_getMethodImplementation(cls, modified), method_getTypeEncoding(modifiedMethod))
method_exchangeImplementations(class_getInstanceMethod(cls, original), class_getInstanceMethod(cls, modified))
return true
} else {
return false
}
}
@objc class RestorationDefender {
class func printViewControllerClassesThatAreProbablyNotRestorable() {
let probablyBadViewControllers = class_getAllSubclasses(UIViewController.self).filter({
return !class_conformsToProtocol($0, UIViewControllerRestoration.self) && NSBundle(forClass: $0) == NSBundle.mainBundle()
})
println("The following view controllers may fail in state restoration (\(probablyBadViewControllers.count)):")
println(join("\n", sorted(probablyBadViewControllers.map({ " \(class_getName($0)!)" }))))
}
class func crashWhenViewControllersDoNotImplementStateRestoration() {
println("Together we will watch the world burn.")
class_swizzleSelectors(UIViewController.self, "viewWillAppear:", "SEUI_crashing_viewWillAppear:")
}
class func shoutWhenViewControllersDoNotImplementStateRestoration() {
println("I'll keep my eyes open.")
class_swizzleSelectors(UIViewController.self, "viewWillAppear:", "SEUI_shouting_viewWillAppear:")
}
}
extension UIViewController {
dynamic func SEUI_crashing_viewWillAppear(animated: Bool) {
assert(restorationIdentifier != nil, "You need to specify a restoration identifier in \(self)")
assert(restorationClass != nil, "You need to specify a restoration class in \(self)")
SEUI_crashing_viewWillAppear(animated)
}
dynamic func SEUI_shouting_viewWillAppear(animated: Bool) {
if self.restorationIdentifier == nil || self.restorationClass == nil {
println()
println()
println("------------------------------------------------------------------------------")
println("Hey, hey you. Guess what? I found something wrong with this view controller:")
println(self)
if restorationIdentifier == nil {
println("It doesn't set its restoration identifier!")
}
if restorationClass == nil {
println("It doesn't set its restoration class!")
}
println("------------------------------------------------------------------------------")
println()
println()
}
SEUI_shouting_viewWillAppear(animated)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment