There are some very powerful features in the Objective-C runtime, so is this something we should be taking advantage of more?
During my time as a developer, I have seen my fair share of examples where people have abused the Objective-C runtime to solve a problem, when there was most likely a better solution.
One common example of this was when a developer uses objc_setClass() to change the class of an instance at runtime. I’ve seen this happen when developers want to instantiate a controller from a storyboard, but then subclass that class and go thorough the same initialiser.
// This is just a simple controller, that has a couple of outlets and also a class function which will initialise itself from a storyboard.
class HorseViewController: UIViewController {
@IBOutlet weak var header: UIView!
@IBOutlet weak var body: UIView!
class func instantiateViewController() -> HorseViewController {
let storyboard = UIStoryboard(name: "main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "Identifier") as! HorseViewController
return vc
}
}
This is all very straightforward, but what happens if you now want to subclass this function?
class ZebraViewController: HorseViewController {
var stripes: [Any]!
}
// What will this return?
ZebraViewController.instantiateViewController()
This will still return an instance of HorseViewController. We could then override the super method, and return our own instance. However, this would mean duplicating the controller in the storyboard for our subclass.
The problem
This is where I have seen the Objective-C runtime used incorrectly to solve the issue above.
// If we override the instantiateViewController in out subclass and then swap the class using object_setClass we can in theory change the class to our subclassed version.
…
override class func instantiateViewController() -> HorseViewController {
let vc = super.instantiateViewController()
object_setClass(vc, ZebraViewController.self)
return vc
}
}
So why is this bad?
To really understand this we need to know a little about how the Objective-C runtime works, and how swift objects are converted to run inside this environment.
// If we look into objc.h we can see exactly we make up an object.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
In essence, every object once compiled down to the Objective-C runtime, is a struct, which contains a single pointer called an isa, which is of type Class. That’s it… There isn’t really much to it. All I can say is that every object in the objc runtime has one of these pointers.
So what is an isa pointer?
The isa pointer is the first pointer-sized piece of memory that is allocated when initialising an object, and it contains information about the objects class. We can see this by looking at what makes up a Class.
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
Again a class is just a structure, and just like an object, it also has an isa pointer, which tells us that Class, is in-fact also an object. We also have a number of Objc2 only attributes.
super_class – this is a pointer to the object’s parent. This is what is returned when we call super on an object. Notice there is no pointer to retrieve the subclasses.
objc_ivar_list – this is the list of instance variables for the current class. Pretty self explanatory. Same with objc_protocol_list – which is again just a list of the protocols a class conforms to. As these are both pointers they can be both changed out at runtime.
objc_method_list – is slightly different to the two above, as it is a pointer to a pointer, which allows the objc runtime to modify the classes methods.
objc_cache – the object cache is very important, as it is this that tells the build system if there is a cached version of the class already stored. When the runtime searches for an implementation which matches a selector, it will first check the cache to see if a value exists, and if it doesn’t, it will query the rest of the hierarchy.
name, version and info are just some metadata set on the class.
instance_size – is the amount of memory that class needs to be allocated.
Now we know more about what happens behind the scenes we can finally look at why changing the class at runtime could be a problem.
Memory Allocation
All we are doing when we call object_setClass is changing the isa pointer to the one of our new class. Our original object has already been initialised, and the correct amount of memory has been allocated. When we change the pointer we now have a new class which may need more memory, if it has more methods, ivars etc. This is where the problem lies. New methods may not have enough memory to execute so we could start to see random crashes appearing.
Solution
In my opinion, if you want to share UI from a storyboard or xib, then I think the best approach would be to split it up into separate views or child view controllers. These can then easily be re-used in multiple view controllers.