Protocol Oriented Extensions
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)
swift
extension Date {
public var isToday: Bool { ... }
}
Module B (our new framework)
swift
extension Date {
public var isToday: Bool { ... }
}
Using the extension from module B
swift
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
swift
public struct ExtensionsWrapper<Base> {
// 1
public var base: Base
// 2
public init(_ base: Base) {
self.base = base
}
}
- The base object to extend
- An initializer to creates extensions with the base object.
The Compatible protocol
swift
public protocol ExtensionsCompatible {
// 1
associatedtype CompatibleType
// 2
static var ext: ExtensionsWrapper<CompatibleType>.Type { get set }
// 3
var ext: ExtensionsWrapper<CompatibleType> { get set }
}
swift
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
}
}
}
- The type of base object to extend
- A static namespace holder for our extensions compatible types.
- A namespace holder for our extensions compatible types.
- 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
swift
// 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
}
}
- Make
UILabel
conform toExtensionsCompatible
, this will add theext
extension to AllUILabel
types. - Instead of extending
UILabel
directly extendExtensionsWrapper
explicitly when theBase
isUILabel
.
swift
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!