Avoiding extension hell with opt-in extensions

Tjeerd in 't Veen

Tjeerd in 't Veen

— 4 min read

Learn about special opt-in extensions to get more control, and avoid namespace clashes across modules.

Extensions are a convenient technique to power up types in Swift to make our lives easier. But it can be a double-edged sword. If another developer isn’t careful, you may import a framework and run into an overload of unwanted extensions, or even worse, a clash where the owner of a framework has a same-named extension as one of yours or even another framework!

This risk is even higher once a framework extends a type that the framework doesn’t own—e.g., importing a framework that extends UIKit. The risk becomes higher because Swift doesn’t support namespaced extensions.

If we want to offer a framework—or even full-blown SDK—we can be a good housekeeper and offer opt-in extensions, where the implementer of your framework won’t get extensions unless they explicitly ask for them. Let’s see how this works.

Creating an extension

We are going to cover extensions in combination with subclasses to showcase a more granular control of extensions. Let’s use UIView as an example since it’s commonly subclassed when working with iOS.

Imagine that we want to log a whole tree—pun intended—of a UIView and its subviews. We can offer a special printTree() method inside a UIView extension, but this has a problem which we’ll cover shortly.

extension UIView: Loggable {
    func printTree() {
        print(self)
        // We recursively call printTree for each subview that we find.
        subviews.forEach { $0.printTree() }
    }
}

Now if we have a UIView instance, we can call printTree() and we’ll see its view and all of its subviews printed in the console log.

The problem is that all UIView instances have this method available; This goes for both UIView instances as well as any of its subclasses.

Before we take this approach and leave it, really think about it. UIView often gets subclassed, and the way we wrote our extension allows all view instances to log itself. Another developer gets the printTree() method on their instances, whether she likes it or not.

Even worse, if we were to make this extension public inside a framework, the implementer of this framework gets this method in their project and potentially cause a naming clash. It’s the Swift equivalent of a framework deciding to swizzle classes without your consent.

Creating an opt-in extension

Let’s be a well-disciplined developer and offer an opt-in extension; This allows another developer to decide when and if they want our extension.

First, we’ll define a Loggable protocol that defines the printTree() method; this means that we throw away the previous UIView extension.

protocol Loggable {
    func printTree()
}

Instead of extending UIView, we extend the Loggable protocol and constrain it to UIView. This way, the extension becomes available on some UIView instances, but not all of them.

In this extension, we’ll offer a free printTree() method implementation once a UIView (sub)class constrains itself to the Loggable protocol.

extension Loggable where Self: UIView {
    func printTree() {
        printChildren(view: self) // Calls recursive helper method
    }

    /// Recursively print children
    func printChildren(view: UIView) {
        view.subviews.forEach { view in
            print(view)
            printChildren(view: view)
        }
    }
}

We can’t recursively call printTree because not all subviews might conform to Loggable. Instead, we use a helper method called printChildren which we can apply to all subviews. Whereas printTree is only available on Loggable subviews.

We’ve created an extension with opt-in behavior, now if a developer manually conforms a UIView subclass to Loggable, the printTree() method becomes available.

Notice how a regular UIView doesn’t get the printTree() method, but if a custom UIView adheres to Loggable, it will get the printTree() method for free.

let view = UIView((frame: CGRect.zero)
view.printTree() // not available

// Creating a custom UIView subclass
final class CustomView: UIView, Loggable {
}

let customView = CustomView(frame: CGRect.zero)
customView.printTree() // The printTree method is available on CustomView

The downside is that a regular UIView won’t get this method. But that’s easily solved by making UIView explicitly implement the Loggable protocol.

extension UIView: Loggable {}

This way, giving UIView the printTree() extension now happens on our terms, and not by default.

Become a Swift pro

Updated for Swift 5!

Check out Swift in Depth and master tough concepts; Such as Generics, Functional Programming concepts, and Protocol-Oriented Programming.

Conclusion

Once you start working with larger projects comprising out of multiple frameworks, you may find yourself running into extensions-overload, where developers get creative by adding many extensions on many types.

Offering extension via an opt-in approach gives your fellow developers more control when and if they want extensions. It makes it more explicit when added functionality is used, which is especially helpful if you’re offering a framework or SDK.

We used UIView as an example, but you can imagine that more scenarios benefit from opt-in extensions; Such as having analytics protocols implemented by viewcontrollers or nsobjects, or clever constraints extensions added to viewcontrollers or views.


Written by

Tjeerd in 't Veen has a background in product development inside startups, agencies, and enterprises. His roles included being a staff engineer at Twitter 1.0 and iOS Tech Lead at ING Bank.