The power of extensions in Swift
Discover page available: GenericsExtensions enable us to add new functionality to existing types and protocols, including ones that we didn’t define ourselves — such as those that ship as part of the Swift standard library or Apple’s various SDKs, or in any third party package that we’ve included within our project.
However, the ways in which Swift’s extensions can be used include far more advanced concepts that simply adding new properties and methods to external objects, which in turn makes them one of the most powerful and versatile features that the language has to offer. This week, let’s explore some of those aspects, and the kind of patterns and techniques that they enable us to adopt.
Existing types, new functionality
Let’s start with the basics. One way to use extensions is to add new, custom APIs to the various types that ship as part of the system, for example within the Swift standard library. As an example, let’s say that we’re working on an app which logic requires us to access specific elements within various arrays — so to avoid having to always check if the index that we’re looking to access is within a given array’s bounds, we could add the following method that does that work for us:
extension Array {
func element(at index: Int) -> Element? {
guard index >= 0, index < count else {
return nil
}
return self[index]
}
}
That’s already quite powerful, as we’ll now be able to use the above method on any Array
within our code base, but what’s perhaps even more powerful is the fact that we could also have made the above extension target the RandomAccessCollection
protocol instead.
Since RandomAccessCollection
defines the requirements for collections that provide random access to its elements, extending that protocol (rather than the concrete Array
type) would let us use our new method on any such collection, including Array
itself:
extension RandomAccessCollection {
func element(at index: Index) -> Element? {
guard indices.contains(index) else {
return nil
}
return self[index]
}
}
With the above in place, we’ll now be able to call our new method on types like Array
, ArraySlice
and Range
, all using one single implementation:
// Extracting an optional element from an Array
guard let fifthElement = array.element(at: 4) else {
return
}
// Doing the same thing, but using an ArraySlice instead:
let slice = array[0..<3]
guard let secondElement = slice.element(at: 1) else {
return
}
// We could also use our new method with types like Range:
guard let thirdValue = range.element(at: 2) else {
return
}
So, when it’s possible and practical to do so, extending protocols (rather than concrete types) gives us a lot more flexibility, as we’ll be able to use the methods and properties that we add with a much wider range of types.
However, not all extensions that we’ll end up adding are going to be as general-purpose as the one above, so although we might still opt for the protocol-based approach, we could also apply constraints to make such an extension more specific.
For example, the following extension adds a method that lets us calculate the total price for a sequence of products, and by using a same type constraint we can establish a compile-time guarantee that this method will only ever be called on Sequence
-conforming types that contain Product
values:
extension Sequence where Element == Product {
func totalPrice() -> Int {
reduce(0) { price, product in
price + product.price
}
}
}
The above API is defined as a method, rather than a computed property, since it has O(n)
time complexity. For more details, check out “Computed properties in Swift”.
One thing that’s really cool is that constraints can not only refer to concrete types and protocols, but also to closure types as well — which lets us do things like add a method that calls all closures within a given sequence, like this:
extension Sequence where Element == () -> Void {
func callAll() {
forEach { closure in
closure()
}
}
}
Swift 5.3 takes the above capability even further, by enabling us to now also apply constraints to individual methods declarations that refer to their enclosing type. That lets us create a second overload of the above method that accepts an argument matching the input type of the closures that are contained within the sequence:
extension Sequence {
func callAll<T>(with input: T) where Element == (T) -> Void {
forEach { closure in
closure(input)
}
}
}
The above new method could become very useful in situations where we want to pass the same value to a number of different closures — for example in order to notify all observers that an Observable
type’s value
was changed:
class Observable<Value> {
var value: Value {
didSet { observations.callAll(with: value) }
}
private var observations = [(Value) -> Void]()
...
}
For more on the observer pattern, check out the two-part article “Observers in Swift”.
The examples that we’ve explored so far have definitely all been non-essential convenience APIs — however, when tactically deployed, those kinds of APIs can often help us reduce both the verbosity and repetitiveness of our code, and continuously make a project easier to work with over time.
Organizing APIs and protocol conformances
Extensions are also commonly used as a code organization tool, which is a practice that Swift inherited from its predecessor, Objective-C. Since Objective-C’s version of extensions — categories — support giving each extension an explicit name, they’ve often been used to group a series of APIs together based on what kind of functionality that they provide.
In Swift, we could use that same approach to structure a given type’s APIs based on their access level. Let’s take a look at an example from Publish, the static site generator used to build this very website, in which a Section
type uses a series of extensions to form groups containing its public
, internal
and private
APIs:
public struct Section<Site: Website>: Location {
public let id: Site.SectionID
public private(set) var items = [Item<Site>]()
...
}
public extension Section {
func item(at path: Path) -> Item<Site>? {
...
}
func items(taggedWith tag: Tag) -> [Item<Site>] {
...
}
...
}
internal extension Section {
mutating func addItem(_ item: Item<Site>) {
...
}
}
private extension Section {
...
mutating func rebuildIndexes() {
...
}
}
Besides the organizational aspects, one benefit of the above approach is that we no longer have to give each method or property an explicit access level, as each API will automatically inherit the access level of its enclosing extension.
We could also follow the above pattern when conforming to protocols as well, as we’re able to attach such a conformance to any extension that we’ll define. For example, here we’re making a ListViewController
conform to UIKit’s UITableViewDelegate
protocol through such an extension:
extension ListViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
let item = items[indexPath.item]
showDetailViewController(for: item)
}
...
}
Just like how we previously applied constraints when defining extensions containing custom methods and properties, we can do the same thing when extending a given type to conform to a protocol as well, which is especially useful when it comes to wrapper types — such as the generic NetworkResponse
wrapper from “Creating generic networking APIs in Swift”.
Here we’re making that wrapper type conditionally conform to protocols like Equatable
and Hashable
only when its Wrapped
type also conforms to those protocols:
// The compiler can still automatically generate the code required
// to conform to protocols like Equatable and Hashable even when
// adding those conformances through extensions:
extension NetworkResponse: Equatable where Wrapped: Equatable {}
extension NetworkResponse: Hashable where Wrapped: Hashable {}
// Most protocols will probably require us to write some form of
// bridging code ourselves, though. For example, here we make our
// network response use its wrapped type's description when it's
// being converted into a string, rather than defining its own:
extension NetworkResponse: CustomStringConvertible
where Wrapped: CustomStringConvertible {
var description: String {
result.description
}
}
So extensions can also prove to be an incredibly useful tool when organizing a given type by splitting it up according to the access level or functionality of its various APIs, or when we want to make a type conform to a protocol, either with or without constraints.
Specializing generics
Finally, let’s take a look at how extensions can also be used to specialize generic types and protocols for concrete use cases.
Just like the Sequence
and RandomAccessCollection
protocols that we previously extended with convenience APIs, several of Apple’s most modern frameworks make heavy use of generics in order to make their APIs both flexible and fully type-safe. For example, all of Combine’s various publishers are implemented using the Publisher
protocol, which contains generic types that define what Output
that a given publisher produces, as well as what sort of Failure
error that might be emitted.
Those two generic types in turn enable us to write extensions that contain completely custom Combine operators — such as the following one that makes any publisher emit unified Result
values, rather than separate ones for its output and errors:
extension Publisher {
func asResult() -> AnyPublisher<Result<Output, Failure>, Never> {
self.map(Result.success)
.catch { error in
Just(.failure(error))
}
.eraseToAnyPublisher()
}
}
To learn more about the Just
publisher that’s used above, check out “Publishing constant values using Combine”.
The above extension then lets us write Combine pipelines like the one used by the following AsyncValue
, which assign their output directly to a Result
-based property — like this:
class AsyncValue<Value: Decodable>: ObservableObject {
@Published private(set) var result: Result<Value, Error>?
private var cancellable: AnyCancellable?
func load(from url: URL,
using session: URLSession = .shared,
decoder: JSONDecoder = .init()) {
cancellable = session.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: Value.self, decoder: decoder)
.asResult()
.sink { [weak self] result in
self?.result = result
}
}
}
Combining the above approach with generic type constraints also lets us take advantage of Swift’s powerful type inference capabilities, which is something that SwiftUI makes heavy use of when defining convenience APIs for its various built-in views.
As an example, let’s say that an app that we’re working on contains an IconView
that renders an icon from a pre-defined set. To make it easy to create a Button
containing such an icon, we could write the following extension — which uses a same type constraint on the generic Label
type that defines what sort of content view that a given Button
is rendering:
extension Button where Label == IconView {
init(icon: Icon, action: @escaping () -> Void) {
self.init(action: action, label: {
IconView(icon: icon)
})
}
}
The cool thing is that we can now simply use the above API to create a Button
instance, and the compiler will automatically infer that we wish to use IconView
as that button’s Label
type — like this:
struct ProductView: View {
@ObservedObject var viewModel: ProductViewModel
var body: some View {
VStack {
...
Button(icon: .shoppingCart) {
viewModel.performPurchase()
}
}
}
}
For more options on how to style SwiftUI views, check out “Encapsulating SwiftUI view styles”.
The above pattern is also used all throughout Plot, which is the HTML DSL that’s used to define themes for Publish-based websites. When using Plot, each HTML element is defined using a generic Node
type, which in turn uses phantom types to ensure that each element is placed within a valid context. Then, each built-in element and component is created just like how we defined our SwiftUI convenience API above — using constrained extensions:
public extension Node where Context: HTML.BodyContext {
static func a(_ nodes: Node<HTML.AnchorContext>...) -> Node {
.element(named: "a", nodes: nodes)
}
...
static func div(_ nodes: Node<HTML.BodyContext>...) -> Node {
.element(named: "div", nodes: nodes)
}
...
}
Similar to our earlier SwiftUI-based example, the compiler is then able to automatically infer what type of Node
that we’re looking to create purely based on the static method call used to create it:
// The type of this value will be Node<HTML.BodyContext>, which
// the compiler will infer based on our method call:
let div = Node.div(.a(.href("https://swiftbysundell.com")))
The beauty of the above capabilities is that they let us model various domains in very advanced, strongly typed ways — while still making our call sites as simple as possible, as we don’t always need to specify each underlying generic type manually.
Conclusion
While extensions might, at first glance, seem like one of Swift’s simpler features, once we start diving into all of the various patterns and capabilities that they enable us to adopt, they can actually turn out to be one of the most powerful features that the language has to offer.
Extensions give us the flexibility to add new features to existing types, to conditionally enable certain methods or properties to be used on either a type or a protocol, and to craft strongly typed APIs that remain easy to use. I hope that this article has given you a comprehensive overview of the sort of things that Swift’s extensions can do, and if you have any questions, comments, or feedback, then please reach out via either Twitter or email.
Thanks for reading! 🚀