Programming

What I swizzle

Several of the talks at 360|iDev mentioned swizzling with disdain and I got some concerned looks when I mentioned using it in my app.  So…. let’s audit the swizzling used in Stack Exchange.app!

A case-insensitive search of the app reveals 40 references.  Ignoring definitions and that some frameworks may swizzle without naming it, there are only about 25 lines of actual swizzling.

The below sections go from the safest uses to the most exciting.

Precursor: What is swizzling?

There are many good explanations of swizzling elsewhere. NSHipster article on swizzling is probably the best place to get started.

In short, swizzling is a technique for swapping two method implementations in Objective-C.  This usually involves a target method from a framework (like UIKit) and an observing method that you add in a framework.  This can be used either to completely replace the behavior of a method or to add side-effects every time a method is called.

Unused library methods

GRMustache and MagicalRecord provide optional swizzling methods.  In GRMustache’s case, it swizzles NSObject.valueForUndefinedKey(_:) and NSManagedObject.valueForUndefinedKey(_:) to avoid a type of attack. In MagicalRecord’s case, it strips off the MR_ prefixes on category methods.

I don’t use these because I don’t mind the prefixes (in ObjC anyway) and the mustache issue doesn’t affect me.

Debug-only swizzling

Swizzling is great for debugging.  It provides the same functionality as a symbolic breakpoint but makes it a lot easier to perform analysis on the function’s arguments.  You also have free reign to swizzle anything you want, so long as its DEBUG-gated.

For example, the following code prints a warning to the log whenever UIView.setAnimationsEnabled(_:) is called from the background thread. This helped me track down issues when doing size calculations on a background thread.

I’ve similarly used swizzling to log when UIView.animateWithDuration(_:delay:options:animations:completion:) is called so I could find the exact parameters passed when tableview rows animate. (wasEditingAtBeginningOfAnimation ? UIViewAnimationOptions(rawValue: 0x70404) : UIViewAnimationOptions(rawValue: 0x404)).

I swizzle UIApplication.preferredContentSizeCategory() to change its value in a debug mode where I randomly change font sizes.

I also swizzle UIApplication.sendEvent(_:) to bring up a menu when a shake event occurs.  I previously subclassed UIApplication and sent a notification, but this was easier.

One other compelling “a swizzle by any other name” is method injection for fixing a problem where Xcode’s view debugger calls methods that don’t exist in the target SDK.  You can read about it in this SO answer.

Logging

To help with crash troubleshooting, I swizzle a few methods on UINavigationController to log when view controllers are added and removed.  This is pretty much the lowest risk thing you can do with Swizzling.

Observing Methods

This is pretty much the same concept as logging, except I’m generally adding a side-effect into the action that feeds right back into the way the views are displayed.

The biggest user of this is our SESearchDisplayController, which is a clone of UISearchDisplayController but with some specific tweaks we wanted.  This listens to viewWillAppear(_:), viewDidAppear(_:), viewWillDisappear(_:), viewDidDisappear(_:), willAnimateRotationToInterface(_:duration:), and viewWilLayoutSubviews() and simply notifies the search display controller (if it exists) of the event.  The side effects are either the showing or hiding of the tab bar or layout tweaks.

Similarly, we observe UITabViewController.setSelectedIndex(_:) and change whether the tab bar is hidden or not based on whether the new view controller wants it to be.

We also swizzle our methods from our own frameworks.  On tablet, when someone is posting a comment on a post, we disable the left split controller so the user can’t accidentally tap away from the editor.  We also disable the left menu controller for the same reason.  When extracting the framework, the split controller and the comment controller no longer had access to the menu controller so I needed a way to send the message.  I could have added a delegate or posted a notification, but the simplest method was to just swizzle it, effectively breaking one function into two:

BEFORE:

class A {
    func disableThing() {
        disabled = true
        tabletThing?.disabled = true
    }
}

AFTER:

class A {
    dynamic func disableThing() {
        disabled = true
    }
}

extension A {
    dynamic func swizzledDisableThing() {
        swizzledDisableThing()
        tabletThing?.disabled = true
    }
}

Fundamentally changing behavior

There are four places where we use swizzling to fundamentally change how a method works.  This is probably the coolest use of swizzling but also the riskiest and the one you should use most sparingly.

We swizzle UIApplication.openURL(_:) to add support for third-party browsers.  I.e., if a user has Chrome or Firefox installed, we can open it in that browser instead.  In our own code, we’ve marked openURL(_:) as deprecated and use more specific methods, but swizzling allows third-party frameworks to support the behavior as well.  Of course, the same behavior can be (and was) achieved by subclassing UIApplication, but then you have to create main.swift and do some funny business when adding things that could just be category methods.

We swizzle one of our framework methods SEAPIRequest.URLRequestWithContext(_:) in order to replace production hosts with development ones.  This allows us to ship a simpler framework since we’re the only ones who will need to use these other hosts.

When a user long presses on a cell, UIKit displays the menu controller centered based on the cell’s bounds.  If the cell is not completely visible, say because another view is covering the right 80% of it, that arrow can end up just floating in the middle of no where.  Since we can’t control what gets passed to UIMenuController.setTargetRect(_:inView:), we swizzle it and tweak the value as needed.

Aside: This problem is not unique to our app.  UITableView has an undocumented delegate method UITableViewDelegate.tableView(_:calloutTargetRectForCell:forRowAtIndexPath:) that can be used by Apple apps.  Were this available to all developers, this swizzle would be unnecessary.

The last place where we currently swizzle is UIViewController.next (nextResponder).  I’ve started using the responder chain to handle view controller presentation (a topic for another post) and things have gotten so complicated that I have my own UIResponder subclass that can handle the bulk of it.  Looking at the documentation, target selection is supposed to look like this:

func target(forAction action: Selector, withSender sender: AnyObject?) -> AnyObject? {
    if canPerformAction(action, withSender: sender) {
        return self
    } else {
        return next?.target(forAction: action, withSender: sender)
    }
}

Were this the case, I could simply override the method to look like this:

var intentTarget = SEUIIntentTarget()

func target(forAction action: Selector, withSender sender: AnyObject?) -> AnyObject? {
    if canPerformAction(action, withSender: sender) {
        return self
    } else if intentTarget.canPerformAction(action, withSender: sender) {
        return intentTarget
    } else {
        return next?.target(forAction: action, withSender: sender)
    }
}

Unfortunately, the logic is actually more like this:

func target(forAction action: Selector, withSender sender: AnyObject?) -> AnyObject? {
    for responder in sequence(first: self, next: { $0.next }) {
        if responder.canPerformAction(action, withSender: foo) {
            return responder
        }
    }
    return nil
}

This meant either adding a lot of boilerplate (and maybe some Objective-C forwarding) to UIViewController or injecting my responder into the responder chain.  This is slightly more complicated than normal swizzling where the swizzled method directly calls the original, so I made a convenience binding which gives the hook to the method a much cleaner name SEUI_originalNextResponder.  Now UIViewController.next returns intentTarget and intentTarget.next returns owningViewController.SEUI_originalNextResponder().  This is probably my most dangerous use of swizzling because it could break assumptions about what UIViewController.next returns, but even so the risk is fairly low.

Again, this is a kind of swizzling that gets around a limitation in UIKit.  If UIResponder.target(forAction:withSender:) worked as expected this wouldn’t be necessary.

Summary

And, that’s pretty much it.  We mostly swizzle to avoid subclassing, to add side effects for custom behaviors, and to overcome deficiencies in frameworks.  When I started this post, I was hoping to find an instance where I could say “this doesn’t need to exist” but there really wasn’t any.  If anything, I would probably just delay some swizzling in framework methods until it the first time it is needed or put it behind flags, but that’s relatively minor.

TL;DR. AAAA++++ Would swizzle again.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s