Exploring Swift Extensions: Strategies for Efficient iOS Code

Exploring Swift Extensions: Strategies for Efficient iOS Code

iOSSwift
Fix bugs faster! Collect your app logs.
Get Started

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.

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 aStruct 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!

Trusted By

/assets/images/svg/customers/projects/menshealth.svg/assets/images/svg/customers/highprofile/kohler.svg/assets/images/svg/customers/highprofile/disney.svg/assets/images/svg/customers/highprofile/macys.svg/assets/images/svg/customers/highprofile/adt.svg/assets/images/svg/customers/cool/ubiquiti.svg/assets/images/svg/customers/projects/taxify.svg/assets/images/svg/customers/cool/domestika.svg

Already Trusted by Thousands

Bugfender is the best remote logger for mobile and web apps.

Get Started for Free, No Credit Card Required