Have I mentioned that I love the esoteric? Because I most definitely do.
For a bit of context, I've been playing around with information logging via Apple System Logger API and CocoaLumberJack. And they're pretty neat, despite my wholelly uninterested state generally regarding logging. The overarching goal has been to create a system that allows users to configure different logging levels to provide information to a support team.
Fortunately, my shortsightedness became my greatest ally when trying to get a
UIButton to perform a separate class's class method. I've done this many times before to perform a selector that belonged to the class in which the button was being created in, but I hadn't done it using a selector from another class's class method. For example:
UIButton * infoLogsButton = [self createLogButtonForLog:@"Info Logs" color:[UIColor srl_hipsterMaroon] andAddToView:self.containerView]; [infoLogsButton addTarget:self action:@selector(createLogLevelButtons) forControlEvents:UIControlEventTouchUpInside];
This creates an instance of a
UIButton using an instance method I created (
createLogButtonForLog:color:andAddToView:) and then assigns that button the action of
createLogLevelButtons when pressed. Though I have a class,
SRLogConfigManager that has the following method signature
+[SRLogConfigManager changeToInfoLogLevel]. So, a straight-forward approach to performing the action I need with this button is to write:
[infoLogsButton addTarget:[SRLogConfigManager class] action:@selector(changeToInfoLogLevel) forControlEvents:UIControlEventTouchUpInside];
Though for some unfortunate reason, fatigue caused me to ignore the
addTarget parameter as what would be the invoker of the appropriate selector. And so, the rabbit hole of documentation began.
The ObjC Runtime Environment
I had read about Swizzling some time ago after a different rabbit hole, and the topic itself really intrigued me even though I had no applicable uses for it at the moment. Though the prospect of using it (finally!) clouded my judgement in the most beautiful way possible.
The situation that I faced was that I was receiving compiler errors when trying to call the class's method like:
[infoLogsButton addTarget:self action:@selector([SRLogConfigManager changeToInfoLogLevel]) forControlEvents:UIControlEventTouchUpInside];
It seemed reasonable to me that I'd receive the correct
SEL by calling the proper method signature. But after trying to adjust that
action: parameter in every creative way possible, I decided it was just not going to happen.
So then I decided to do a search in Dash for anything related to
SEL. I somehow came across
NSInvocation and then tried to play around with
sel_registerName, but was finding my efforts unsuccessful. Though,
sel_registerName lives in the Objective-C Runtime Reference Guide along with many, many other very interesting C-structs and functions. After considering the advice from the Associated Objects article of NSHipster, I made the plunge by calling
#import <objc/runtime.h>. With that import, I was now able to access all of the runtime library Objc has to offer.
After some trial and error, I found that the following suited my needs as far as getting the right
Class logManagerClass = object_getClass([SRLogConfigManager class]); Method logManagerMethod = class_getClassMethod(logManagerClass, @selector(changeToDebugLogs));
The first line gets a runtime reference to the
SRLogConfigManager class and stores it in
logManagerClass which is a
typedef struct of type
objc_class *Class (Ok, so technically it's really storing the location in memory of where the class is being defined -- and this is worth calling out given how the Objc runtime works). And the second line gets a pointer reference to the data structure that describes a given class method for the given class (
Method class_getClassMethod ( Class cls, SEL name).
Living without Compiler Checks
With the following line of code in place:
[infoLogsButton addTarget:self action:method_getName(logManagerMethod) forControlEvents:UIControlEventTouchUpInside];
I throw an
NSLog statement in my
[SRLogManager changeToInfoLogLevel] method to make sure stuff is happening. Im elated as my project builds and begins running. I go to click the button in question, eagerly anticpating my
NSLog statement to get outputted, and.... crash.
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[SRLoggingAdjustmentViewController changeToDebugLogs]: unrecognized selector sent to instance 0x7fc32480b7b0
SRLoggingAdjustmentViewController doesn't implement the
changeToDebugLogs method, the
So, lesson learned: using runtime functions bypasses the compiler error checking that would tell me there is no public method called
changeToDebugLogs in my
SRLoggingAdjustmentViewController class. And this is fairly obvious, given that the compiler is unable to check for these errors since the
struct references I'm creating are determined at runtime.
Fortunately, at this point the fix becomes obvious: I need to change the
target of the action to
[SRLogManager class]. I re-run, and get my expected
NSLog'd output. Though, I'm now aware of my roundabout means to this end... and I replace
method_getName(logManagerMethod) with simply
@selector(changeToDebugLogs). Autocompletion is telling me that with my
target correctly set to
changeToDebugLogs is now a valid method. I run my project one last time, click on my button and...
2015-03-02 00:07:11.948 SRDeviceLogs[15827:642853] Log levels are now set to debug!
So, there was absolutely no need to use runtime functions for my goal of setting a class method of a button's target. But the spark of curiousity led me to a very different, and fascinating topic.
And for posterity, here's a small sample of the code I've described.