If you’ve done any work on iOS apps these last few years, you’ll know how concise and expressive the Swift language can be. And you may well have benefited from Swift extensions, a powerful mechanism that lets you, the developer, extend both your functionality and Swift’s, by extending any Objects
and Structures
you need.
However, the variety and versatility of Swift extensions does present teething challenges. To make full use of them, we need to understand their specific use cases, build a clear methodology and grasp related concepts like Objects
and Structures
.
So in this article, we’re going to look at:
- The many benefits of Swift extensions.
- The instances when they are, and aren’t, useful.
- Some simple Swift extensions code examples that demonstrate what we’re talking about.
The article is aimed at iOS developers of all levels: we hope that both the code and the commentary are clear, simple and accessible.
Table of Contents
Ok, so first off… what are Swift extensions?
Swift extensions
enable developers to add new functionality to existing Classes
, Structs
, Enums
, or Protocols
.
Some other languages require you to use Subclasses
to add functionality to existing code or an existing class. However, Swift extensions
provide a simpler alternative, enabling you to add functionality using merely the base elements.
The syntax of Swift extensions
The syntax to create a Swift extension
is actually quite simple, as we can see here:
extension _typeToBeExtended_ {
// Your extended functionality
}
You can replace _typeToBeExtended_
with the specific type you want to extend. This could be a class
, struct
, enum
, or protocol
.
Great. And what are the use cases for extensions?
We can use extensions
for all the following:
- Protocol conformance.
- Adding custom functionality to existing types.
- Custom initialisers.
Now, let’s take a closer look at each.
Protocol conformance
Protocols
are a crucial part of Swift, and extensions
allow us to organize them effectively by separating our code and adapting it to our business needs.
To illustrate the point, here’s a Struct
that represents a very simplified version of a Citizen struct
:
struct Citizen {
let citizenID: String
let citizenName: String
init(citizenID: String, citizenName: String) {
self.citizenID = citizenID
self.citizenName = citizenName
}
}
Now, through the business logic, we need to be able to identify a certain citizen by its ID. Swift makes our life easy by giving us the default Identifiable Protocol
:
As you can see, as soon as we have added the protocol conformance, the compiler automatically prompts us to add the needed stubs, and now we have an extension
that contains all the identifiable implementation.
The full code now looks like this:
struct Citizen {
let citizenID: String
let citizenName: String
init(citizenID: String, citizenName: String) {
self.citizenID = citizenID
self.citizenName = citizenName
}
}
extension Citizen: Identifiable {
var id: String {
citizenID
}
}
Let’s now add conformance to Hashable
, Comparable
, and Equatable
. While these are very simple implementations, the code could look like:
struct Citizen {
let citizenID: String
let citizenName: String
init(citizenID: String, citizenName: String) {
self.citizenID = citizenID
self.citizenName = citizenName
}
}
extension Citizen: Identifiable {
var id: String {
citizenID
}
}
extension Citizen: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(citizenID)
}
}
extension Citizen: Comparable {
static func < (lhs: Citizen, rhs: Citizen) -> Bool {
lhs.citizenID < rhs.citizenID
}
}
extension Citizen: Equatable {
static func ==(lhs: Citizen, rhs: Citizen) -> Bool {
return lhs.citizenID == rhs.citizenID && lhs.citizenName == rhs.citizenName
}
}
If we didn’t have extensions
to fall back on, the code might instead look like:
struct Citizen: Identifiable, Hashable, Comparable, Equatable {
let citizenID: String
let citizenName: String
init(citizenID: String, citizenName: String) {
self.citizenID = citizenID
self.citizenName = citizenName
}
var id: String {
citizenID
}
func hash(into hasher: inout Hasher) {
hasher.combine(citizenID)
}
static func < (lhs: Citizen, rhs: Citizen) -> Bool {
lhs.citizenID < rhs.citizenID
}
static func ==(lhs: Citizen, rhs: Citizen) -> Bool {
return lhs.citizenID == rhs.citizenID && lhs.citizenName == rhs.citizenName
}
}
While the code is shorter, the non-extension version makes it harder to understand which methods belong to each protocol. Besides that, however, the functionality is exactly the same in both cases. So if you personally prefer not having extensions
for protocol conformance, that is totally up to you. Just remember to be consistent!
Adding custom functionality to existing types
While you’ve seen that we can use extensions
to wrap our protocols and ensure a cleaner separation between responsibilities of our types, arguably the key benefit of extensions
is the ability to add custom methods, or properties, to existing types.
Adding methods
Methods can be added to our extensions
quite simply, as we previously demonstrated while adding protocol conformance. We simply have to declare what we’re extending, and add the custom method to the body of our extension
.
Now let’s look at an extension
to the existing String
class, which tells us whether a certain String
is a numeric value or not:
extension String {
func isNumeric() -> Bool {
return Double(self) != nil
}
}
The function itself simply attempts to convert the String to a Double.
If it succeeds, then it’s a numeric value. Otherwise, it isn’t.
Once this extension
has been added to our codebase, we can use it in any String
element we like:
"123.455".isNumeric() //returns true
let aVariable = "a"
aVariable.isNumeric() //returns false
So if, in the specific app we’re building, we need to frequently ascertain whether a given String
is numeric or not, we’ve now got a great helper method that can save us a lot of effort, and code, in manual checks everywhere. This also helps if we ever want to change the isNumeric
logic, or remove it completely, since it centralises it.
Adding properties
If adding methods is as simple as adding a regular method to any of our types, then adding properties must follow the same logic. So when we add the following property, it will just work perfectly… right?
extension String {
var optionalNumericValue: Int?
}
Well, no actually.
If you just try to add that extension to your codebase, and we really recommend that you do, you’ll be faced with a compiler error that clearly states Extensions must not contain stored properties
.
Does this mean we can never add properties to extensions? Again, no. We can add properties to extensions, just not stored properties
, because they need to be computed properties
.
At the end of the day, extensions exist to add functionality without changing the memory structure. If we want to fundamentally change a type’s behaviour, we can do so by using inheritance: this will create a new type for us to use and customise at will.
Computed properties
enable us to represent functions as properties, and to use them as such. So while we can’t add a property like the optionalNumericValue
that we just explored, we can have a property that performs the same role as the isNumeric
we saw earlier. Take a look here:
extension String {
var isNumeric: Bool {
return Double(self) != nil
}
}
This means we can now change how we use isNumeric
, so it performs the same role as any other property. However, since it’s a computed property
and not a stored property
, it is read-only and we cannot write to it:
"123.455".isNumeric // true
var aVariable = "a"
aVariable.isNumeric // false
aVariable.isNumeric = true
// Compiler error: Cannot assign to property: 'isNumeric' is a get-only property
Custom initialisers
While we can add any initialiser to any custom type we own, this isn’t true when we don’t own the type. Extensions
are quite handy in these instances. Here are some examples:
New initialiser for Nib loading
If we have an app that relies on Nibs
for the UI, and we load them often, the loading of a Nib
file can look like:
guard let view = UINib(nibName: String(describing: MyViewClass.self), bundle: nil)
.instantiate(withOwner: nil)
.first as? MyViewClass else {
fatalError("There needs to be a View")
}
//do whatever we want with our view
There is no simpler way to load a nib, and our MyViewClass
is usually contained in a file called MyViewClass.swift
. We don’t own UINib
, so we can’t write an additional init that simplifies our life; in this context, an extension
comes in quite handy.
Here’s an example:
extension UIView {
static func fromNib() -> Self {
guard let view = UINib(nibName: String(describing: Self.self), bundle: nil)
.instantiate(withOwner: nil)
.first as? Self else {
fatalError("There needs to be a View")
}
return view
}
}
With this code, which is very similar to the version that loads a specific Nib
, we can simply load Nibs
with a single, and simple, line:
let myView = MyViewClass.fromNib()
And that’s it! We can now load any View
from a Nib
without having to write all the boilerplate code, assuming our View
lives in a file with the same name of the View
, as is the standard.
This is just one example. In fact, there are many other scenarios where initialisers like this come in handy.
Narrowing the scope of our Extensions
While extensions
can be applied to nearly anything, it’s always good practice to narrow their scope to the core essentials.
Lets say that our business logic needs us to write a piece of code that shows a sum of all the numeric Strings
on any Array
. We can extend an Array
for the added functionality:
extension Array {
var stringNumericSum: Double {
var total = 0.0;
self.forEach { element in
total += Double((element as? String) ?? "0.0") ?? 0.0
}
return total
}
}
And there are many other ways to do this. Keep in mind that our intention is to iterate through every element of the Array
, and then to check whether it’s a String.
If it isn’t, we provide a default ‘0.0’ String
, and we also provide a default 0.0 Double
value to add to our total if the String
isn’t convertible to a Double
.
We can now use this stringNumericSum
in every single Array
in our codebase:
let stringArray = ["cat", "10.2", "duck", "5"]
print(stringArray.stringNumericSum) //prints 15.2
****let intArray = [1,2,4,5]
print(intArray.stringNumericSum) //prints 0.0
While this would be useful to have, in this scenario it seems totally unnecessary, since Arrays
of any type besides String
carry absolutely no advantage.
So instead we can narrow the scope of our extension
thus:
extension Array where Element == String {
var numericSum: Double {
var total = 0.0;
self.forEach { element in
total += Double(element) ?? 0.0
}
return total
}
}
By simply extending those Arrays
that are composed of String
elements, we limit the ones that have access to the stringNumericSum
property. To return to our previous example:
let stringArray = ["cat", "10.2", "duck", "5"]
print(stringArray.stringNumericSum) //prints 15.2
****let intArray = [1,2,4,5]
print(intArray.stringNumericSum)
//Won't print anything at all, in fact it will give us a compiler error
//and we won't even be able to compile our code
Or, we can do the exact opposite and actually widen the scope of our extensions
. To return to the example of stringNumericSum
, we can widen it by changing to:
extension Collection {
var stringNumericSum: Double {
var total = 0.0;
self.forEach { element in
total += Double((element as? String) ?? "0.0") ?? 0.0
}
return total
}
}
Now any Collection
will have a way to call and use stringNumericCollection
, regardless of whether it is an Array
, a Set
, a Dictionary
, or any other custom Collection
we might have created for our own project.
So, to sum up…
Swift extensions
enable developers to enhance existing types with new functionality. Whether you’re adding computed properties
, methods
, or conforming to protocols
, extensions
provide a clean and modular approach to code organisation. By following best practices and using extensions
when appropriate, you can help ensure your codebase is maintainable and scalable.
Now go and make your code even better!