Extending functionality with extensions
Extensions let us add functionality to our existing classes, structs, enums, and protocols. These can be especially useful when the original type is provided by an external framework, which means you aren’t able to add functionality directly.
Imagine that we often need to obtain the first word from a given string. Rather than repeatedly writing the code to split the string into words and then retrieving the first word, we can extend the functionality of String
to provide its own first word.
Getting ready
In this recipe, we won’t be using any components from the previous recipes, so you can create a new playground for this recipe.
How to do it...
Let’s get started:
- Create an extension of
String
:extension String { }
- Within the extension’s curly brackets, add a function that returns the first word from the string:
func firstWord() -> String { let spaceIndex = firstIndex(of: " ") ?? endIndex let word = prefix(upTo: spaceIndex) return String(word) }
- Now, we can use this new method on
String
to get the first word from a phrase:let llap = "Ask me about Loom" let firstWord = llap.firstWord() print(firstWord) // Ask
How it works...
We can define an extension using the extension
keyword and then specify the type we want to extend. The implementation of this extension is defined within curly brackets:
extension String { //... }
Methods and computed properties can be defined in extensions in the same way that they can be defined within classes, structs, and enums. Here, we will add a firstWord
function to the String
struct:
extension String { func firstWord() -> String { let spaceIndex = firstIndex(of: " ") ?? endIndex let word = prefix(upTo: spaceIndex) return String(word) } }
The implementation of the firstWord
method is not important for this recipe, so we’ll just touch on it briefly.
In Swift, String
is a collection, so we can use the collection methods to find the first index of an empty space. However, this could be nil
, since the string may contain only one word or no characters at all, so if the index is nil
, we must use endIndex
instead. The nil coalescing operator (??
) is only used to assign endIndex
if firstIndex(of: " ")
is nil
.
More generally, the operation will evaluate the value on the left-hand side of the operator, unless it is nil
, in which case it will assign the value on the right-hand side.
Then, we use the index of the first space to retrieve the substring up to the index, which has a SubString
type. We then use that to create and return String
.
Extensions can implement anything that uses the existing functionality, but they can’t store information in a new property. Therefore, computed properties can be added, but stored properties cannot. Let’s change our firstWord
method so that it’s a computed property instead:
extension String { var firstWord: String { let spaceIndex = firstIndex(of: " ") ?? endIndex let word = prefix(upTo: spaceIndex) return String(word) } }
There’s more...
Extensions can also be used to add protocol conformance, so let’s create a protocol that we want to add conformance to:
- The protocol declares that something can be represented as
Int
:protocol IntRepresentable { var intValue: Int { get } }
- We can extend
Int
and have it conform toIntRepresentable
by returning itself:extension Int: IntRepresentable { var intValue: Int { return self } }
- Now, we’ll extend
String
, and we’ll use anInt
constructor that takesString
and returnsInt
if ourString
contains digits that represent an integer:extension String: IntRepresentable { var intValue: Int { return Int(self) ?? 0 } }
- We can also extend our own custom types and add conformance to the same protocol, so let’s create an enum that can be
IntRepresentable
:enum CrewComplement: Int { case enterpriseD = 1014 case voyager = 150 case deepSpaceNine = 2000 }
- Since our enum is
Int
-based, we can conform toIntRepresentable
by providingrawValue
:extension CrewComplement: IntRepresentable { var intValue: Int { return rawValue } }
- We now have
String
,Int
, andCrewComplement
all conforming toIntRepresentable
, and since we didn’t defineString
orInt
, we have only been able to add conformance through the use of extensions. This common conformance allows us to treat them as the same type:var intableThings = [IntRepresentable]() intableThings.append(55) intableThings.append(1200) intableThings.append("5") intableThings.append("1009") intableThings.append(CrewComplement.enterpriseD) intableThings.append(CrewComplement.voyager) intableThings.append(CrewComplement.deepSpaceNine) let over1000 = intableThings.compactMap { $0.intValue > 1000 ? $0.intValue: nil } print(over1000)
The preceding example includes the use of compactMap
and the ternary operator (?
), which haven’t been covered in this book. Further information can be found in the See also section.
See also
Further information about extensions can be found in Apple’s documentation on the Swift language at http://swiftbook.link/docs/extensions.
The documentation for compactMap
can be found at https://developer.apple.com/documentation/swift/sequence/compactmap(_:).
Further information about the ternary operator can be found at https://docs.swift.org/swift-book/documentation/the-swift-programming-language/basicoperators/#Ternary-Conditional-Operator.