- All posts
- Namespace your Swift extensions
Namespace your Swift extensions
SwifterSwift passed five hundred extensions and began colliding with other libraries. A member added to a type you do not own is not scoped to your library at all — it is global, and no module qualifier reaches it.
Extensions are the feature that made me like Swift. You can add behaviour to a class, a struct, an enum, even a protocol, without subclassing it and without owning its source. When I started writing Swift I liked it enough to build SwifterSwift out of it, which now carries more than five hundred extensions on the standard library and on UIKit.
Somewhere past the first hundred, the pull requests started arriving with the same problem. A useful extension is useful to more than one person, so the better an idea is, the likelier it already ships in a library the same app depends on.
The collision
Two modules, both extending Date. One is yours; the other is something the
app already depends on and is not going to drop.
// Module A
extension Date {
public var isToday: Bool { ... }
}
// Module B — yours
extension Date {
public var isToday: Bool { ... }
}
At the call site:
Date().isToday // Ambiguous use of 'isToday'
The instinct is to qualify it, the way you would with two types of the same
name: A.Date and B.Date are both perfectly sayable. But a module name
qualifies a type. It does not qualify a member added to somebody else’s type,
and there is no spelling of B.isToday, because isToday was never in module
B’s namespace. It went into Date’s, next to timeIntervalSinceNow, and so
did the other one.
The consequence is easy to miss: a member you add to a type you do not own is not scoped to your library at all. It is global, and it shares a namespace with every other module in the build.
Ambiguity errors are the visible symptom. The other one appeared in SwifterSwift
years before anyone filed a bug: type a dot after a String in a project that
imports it, and autocomplete offers a hundred members, most of them mine, none
of them marked as such. Nothing is broken. It is just no longer possible to
tell at a glance which of these came with the language.
A namespace of your own
If you have used RxSwift, Kingfisher or SnapKit you have already typed the
answer: view.rx, imageView.kf, view.snp. Each of them keeps its additions
behind a short property instead of putting them on the type.
It takes two pieces. A generic wrapper holding the value being extended:
public struct Extension<Base> {
public let base: Base
public init(_ base: Base) {
self.base = base
}
}
And a protocol whose only job is to hand out that wrapper:
public protocol ExtensionCompatible {}
extension ExtensionCompatible {
public var ext: Extension<Self> { Extension(self) }
public static var ext: Extension<Self>.Type { Extension<Self>.self }
}
Self rather than an associatedtype: the wrapped type is always the
conforming type, and an associated type that is only ever Self adds a line to
explain and nothing else.
The static overload is there so UIColor.random has somewhere to go. Without
it the namespace covers instance members only, the static ones stay on the bare
type, and half the library is back in the collision you just left.
Now conform the type once, and extend the wrapper instead of the type:
extension UILabel: ExtensionCompatible {}
extension Extension where Base: UILabel {
public var trimmedText: String? {
let text = base.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return text.isEmpty ? nil : text
}
}
let label = UILabel()
label.text = " hello world! \n\n"
label.text // " hello world! \n\n"
label.ext.trimmedText // "hello world!"
One conformance per type, and everything you add for that type afterwards goes
on the wrapper. Module A keeps its isToday and you keep yours, because yours
is on Extension<Date> and its is on Date. Two libraries that both do this
do not collide with each other either, as long as they chose different property
names — and rx, kf and snp suggest the convention holds in practice.
What it does not cover
Mutation through the wrapper is the first trap. You will find a version of this
pattern with an empty setter on ext:
public var ext: Extension<Self> {
get { Extension(self) }
set { } // makes `foo.ext.thing = x` compile
}
It does make the assignment compile. For a class that is harmless: the wrapper
holds a reference, so writing through it reaches the same object. For a struct
it is worse than a compile error — the write lands on a temporary copy, the
empty setter throws it away, and the call site looks exactly like code that
works. Keep base a let, keep the wrapper read-through, and write mutations
as methods that take the value inout or return a new one.
Protocol conformances cannot be namespaced at all. extension Date: Identifiable
is a fact about Date for the whole program, and a second module declaring the
same conformance is a conflict no wrapper resolves. Operators are the same:
they are matched on the types of their operands and never see your namespace.
For both, the old advice stands — do not put them on types you do not own, in
code other people depend on.
And it costs four characters at every call site, forever.
label.ext.trimmedText is not as nice to write as label.trimmedText, and no
argument about namespaces makes it nicer. What you get back is that a reader
can see where the member came from, and that your additions sit together rather
than scattered through a type that already has two hundred members.
For a type you own, none of this applies; extend it directly. The pattern pays for itself when you are adding to somebody else’s type, in code somebody else will depend on — a narrower case than it sounds, and nearly the whole of what a library does.
SwifterSwift is the case in point, and it is too late for it. Five hundred extensions on types nobody in that repository owns is exactly what the wrapper is for, and adding one now would rename every member in the library at once. A namespace cannot be introduced quietly later, so it has to be chosen before the first release — when the library still looks too small to need one.