Protocol Oriented Extensions

SHARE

Extensions are without any doubt one of Swift’s awesome features, they can be used to add new functionality to existing classes, structures, enumeration or even protocol types.

When I first started learning Swift I was fascinated by how powerful this can be, infect back then I created SwifterSwift an open source library for extensions that expanded quickly to have more than 500 extensions!

The more extensions we started to accept from contributors, the more conflicts we started to face, well, chances are if you have a nice extension, someone else has also thought about it as well and added it to their library!

To explain the problem in code; let’s assume we have two modules A, and B, B is the one we’re developing and A is a popular framework that everyone uses.

We’re super excited about this new extension that tells if a Date object is within today or not, so we’re adding it as a public variable in an extension in our framework.

Module A (the popular framework)

extension Date {
    public var isToday: Bool { ... }
}

Module B (our new framework)

extension Date {
    public var isToday: Bool { ... }
}

Using the extension from module B

Date().isToday

The above code will yield the error Ambiguous use of ‘isToday’ with A, that says both modules have the same extension (or at least 2 extensions with the same name), and the Swift compiler is confused which one to use 🤔

This is quite a situation here 😅! We can’t ask people not to use the module A, I mean common, it is very popular and in use everywhere. at the same time, we want people to start using our extension in their projects.

Wouldn’t it be great if we can have our extension grouped in one place and not conflicting with others?

Well, this is exactly what we're going to discuss here!


Enter Protocol Extensions

The idea is simple, instead of extending types, create a wrapper struct with a base generic type, and extend your wrapper when the base is the type you want to extend

If you’ve used RxCocoa, Kingfisher, or SnapKit you’ll probably understand what I mean here, it is that rx, kf, or snp extension that you use to access library's extensions so you don’t end up having conflicts with extensions from other libraries.

Let's talk some code shall we

The Wrapper struct

public struct ExtensionsWrapper<Base> {
    // 1
    public var base: Base
    // 2
    public init(_ base: Base) {
        self.base = base
    }
}
  1. The base object to extend
  2. An initializer to creates extensions with the base object.

The Compatible protocol

public protocol ExtensionsCompatible {
    // 1
    associatedtype CompatibleType
    // 2
    static var ext: ExtensionsWrapper<CompatibleType>.Type { get set }
    // 3
    var ext: ExtensionsWrapper<CompatibleType> { get set }
}
extension ExtensionsCompatible {
    public var ext: ExtensionsWrapper<Self> {
        get {
            return ExtensionsWrapper(self)
        }
        set {
            // 4
        }
    }

    public static var ext: ExtensionsWrapper<Self>.Type {
        get {
            return ExtensionsWrapper<Self>.self
        }
        set {
            // 4
        }
    }
}
  1. The type of base object to extend
  2. A static namespace holder for our extensions compatible types.
  3. A namespace holder for our extensions compatible types.
  4. An empty setter will enable using ExtensionsWrapper to mutate base type

Example

Let's say we have this cool extension for UILabel that returns the trimmed text only if it has 1 or more non-whitespace or newline character in it

// 1
extension UILabel: ExtensionsCompatible {}

// 2
extension ExtensionsWrapper where Base: UILabel {
    public var trimmedText: String? {
        let aText = base.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        if aText.isEmpty { return nil }
        return aText
    }
}
  1. Make UILabel conform to ExtensionsCompatible, this will add the ext extension to All UILabel types.
  2. Instead of extending UILabel directly extend ExtensionsWrapper explicitly when the Base is UILabel.
let label = UILabel()
label.text = "   hello world!  \n\n"
print(label.text) // prints "   hello world!  \n\n"
print(label.ext.trimmedText) // prints "hello world!"

Summary

Of course, there are some cases that require types to be extended directly, however, my suggestion is to consider creating a wrapper type to hold your extensions before publishing your next project.

Not only this will minimize the number of conflicts with other extensions, but will offer a starting point to extend almost every type by conforming to your compatible protocol and extending the wrapper struct!

That's it for now. If you have any suggestions or feedback, please let me know via Twitter!

SoftwareSwiftUIKitiOS

This is a fully integrated open-source project that uses NextJS, Redux, and Django to build. Grab your copy from Github

Copyright © 2019 Omar Albeik. All rights reserved.