Extending one's Intention
Leading on from Chris' Intention post, I continued the journey to make my controllers slimmer.
If you haven't read the post, please do because the things that I'll share are based on that. It explains about how to make your controllers as light as possible. So how?
Code one and only one functionality as one Intention class
Drag an NSObject in the storyboard, make it as the custom Intention class
Wire up your controllers to the classes
The fun thing is that you can add features and functionalities in your controller by wiring things up in Storyboard. One can 'code' the controller's functionalities without writing actual codes.
However, I encoutered a problem when working with delegates. An object could only have one delegate object. Delegate objects have compulsory and optional methods that should be implemented in one class that conform to the protocol. Usually each methods has different functionality. If we implement all of them in one class, it will violate the concept of SRP.
I asked Chris himself about the issue, his answer pointed me about multicast delegate.
@ixnixnixn probably the best would be to have a multicast delegate =)
— Chris Eidhof (@chriseidhof) May 11, 2014
With a little bit of google research, I found an article and a sample code that explain the techniques involved.
But still, I'm not satisfied. I need a slightly different direction on the implementation. Since using Intention relies so much on storyboard already, I want to able to wire my object to its delegates using Interface Builder rather than using written code. Also, I need all of the delegates' methods to be called, even for the ones that returns a value.
Take textFieldShouldReturn: for example. The code should traverse all the delegates and check whether it will respond to the method. If it can, then it's safe to call the method. It should return NO if there's one delegate that returns NO. Otherwise, it should return YES.
'Proof of Concept' sample app
So here's how I went with the idea of extending the concept of Intention with delegates. The objective is to be able wire many functionalities as a delegate object.
I made an app to prove that it's feasible. The app's pretty trivial. It's an app with a text field that has three features;
dismisses when return key is pressed
limits the characters allowed
enable a button if the text is a valid email
Controller on a diet (ViewController.m)
The controller in this app is really light. It consists of a text field and an implementation of viewDidAppear:. I need both to make the text field the first responder after the controller appears.
Dismiss On Enter (DismissOnEnterIntention.m)
It's the same concept as the one in the original post. The only thing that's different from the original is instead of using textField:shouldChangeCharactersInRange:replacementString:, it uses textFieldShouldReturn:.
Limiting Maximum Character (MaximumCharacterOnEditIntention.m)
The cool thing is that we can set the maximum characters from storyboard with user defined runtime attributes. If not set, then we put a constant with a reasonable value (ex. 30 characters).
Email Validation (EmailValidationOnEditIntention.m)
First, we should have an NSString category for validating email. Next, we need to define how to communicate with external object such as UIButton. I copied the technique from the original post by making an IBOutlet for the target. Make an outlet from this class to the button and set the keypath in user defined runtime attributes.
Forwarding Delegate Calls (ITNTextField.m)
So how to forward all the delegate calls from a UITextField? The important thing that I learnt from the article is to have all the calls be forwarded to all delegates in the array.
I chose with subclassing instead of creating a separate MulticastDelegate object (like one in the article). I didn't use forwardInvocation: but I just implemented necessary methods and make sure that all of the delegates is called. One thing to note, we should check first if it responds to the selector before make the actual call.
We need Storyboard to recognize our array of property. We do this by adding IBOutletCollection to our property of delegates. And don't forget to switch the text field's class to your subclass (mine is ITNTextField).
Why not use category? In my opinion, subclassing is safer and more proper since we need to add a property1. With this approach, we're not changing anything with the original UITextField.
Check the animation to see how the wires are connected. So we're adding features only by wiring things. Simple. 👌
One caveat is that IBOutletCollection can't handle id objects with protocols (ex. id<UITextFieldDelegate>). Since we already defend our code to only call methods from a delegate object that can respond to a selector, then everything should be fine.
I've uploaded to Github for anyone who is interested; extending my intention.
The concept of separating controller's code to pieces of intention class could still be improved even more. The class would turn into bits which we can use by wire them up. With this, not only our controller's code is light, but all of our class still conforms to SRP.
Technically, properties can be added to categories. But I think it's still cleaner if we don't touch UIKit classes. ↩︎