Adding stored properties to an extension

Every iOS developer has likely encountered this not-so-friendly message:

#ForBlindPeople: The image shows an error message that says extensions must not contain stored properties.

Yes, extensions cannot contain stored properties. If you need to add a property to a native component, you must create your own button inheriting from UIButton.

In my opinion, creating your own button using UIButton inheritance is the best way to resolve this situation. However, for every rule, there is an exception, and being able to place a property in an extension would be incredibly useful if it were possible.

But is it really impossible?

There is a way to escape from this trap: the Objective-C Runtime.

Associated Objects

Associated Objects use the old concept we learned in Object-Oriented Programming classes: an object that contains another object inside it.

With Objective-C Runtime, you can create objects and associate them at runtime.

To achieve that, we’re going to use two functions available in Objective-C Runtime: objc_getAssociatedObject and objc_setAssociatedObject.

Here are their definitions, according to Apple docs:

objc_getAssociatedObject: Returns the value associated with a given object for a given key.

objc_setAssociatedObject: Sets an associated value for a given object using a given key and association policy.

Hey, wait a minute: association policy? What does that mean?

Association Policy

Association policy is about how the object will be associated with its owner. Do you remember the keywords strong, weak, copy, atomic, and nonatomic? Yes, that’s it.

There are five possible association types:

OBJC_ASSOCIATION_ASSIGN: Weak reference (weak)
OBJC_ASSOCIATION_COPY: Object copy (should implement NSCopying protocol) with atomic association.
OBJC_ASSOCIATION_COPY_NONATOMIC: Object copy (should implement NSCopying protocol) with non-atomic association.
OBJC_ASSOCIATION_RETAIN: Strong reference (strong) with atomic association.
OBJC_ASSOCIATION_RETAIN_NONATOMIC: Strong reference (strong) with non-atomic association.

Let’s see an example that demonstrates everything we’ve discussed so far.

Implementing an action handler in a UIButton

First, let’s create a UIButton extension and place our action handler.

#ForBlindPeople: The image shows code where UIButton is extended with an action handler, but it fails with an error message that says extensions must not contain stored properties.

Yeah, it didn’t work…

Let’s use objc_getAssociatedObject and objc_setAssociatedObject so we can turn our stored property into a computed property, which is allowed in extensions.

We just need to create a getter and setter for our property, as shown in the next example:

fileprivate struct UIButtonAssociatedKeys {
    static var action: UInt8 = 0
}

extension UIButton {

    var actionHandler: ((UIControl) -> Void)? {
        get {
            return objc_getAssociatedObject(self, &UIButtonAssociatedKeys.action) as? ((UIControl) -> Void)
        }
        set {
            objc_setAssociatedObject(self, &UIButtonAssociatedKeys.action, newValue, .OBJC_ASSOCIATION_COPY)
        }
    }

}

In the get section, we will search inside self (our UIButton) for an associated property defined by the reference key UIButtonAssociatedKeys.action, and return it as our action.

In the set section, we will store in self (our UIButton) an associated object (newValue) that will be referenced by the key UIButtonAssociatedKeys.action, storing a copy of this object.

Thus, we will store and retrieve our action handler in any UIButton.

Making an Action Handler Respond to a Button Tap

In the same way that we would use addTarget to implement the button tap via code, we will create a tap function to store our action handler and set it for TouchUpInside.

That way, our final code would look like this:

import Foundation
import UIKit

fileprivate struct UIButtonAssociatedKeys {
    static var action: UInt8 = 0
}

extension UIButton {

    private var actionHandler: ((UIControl) -> Void)? {
        get {
            return objc_getAssociatedObject(self, &UIButtonAssociatedKeys.action) as? ((UIControl) -> Void)
        }
        set {
            objc_setAssociatedObject(self, &UIButtonAssociatedKeys.action, newValue, .OBJC_ASSOCIATION_COPY)
        }
    }

    @objc private func executeAction(_ sender: UIControl) {
        self.actionHandler?(sender)
    }

    func tap(_ action: @escaping (UIControl) -> Void) {
        self.actionHandler = action
        self.addTarget(self, action: #selector(executeAction(_:)), for: .touchUpInside)
    }
}

That’s All, Folks

I hope you enjoyed this tip! Soon there will be more tips, more tutorials, and much more content for iOS developers.