Leave a rating/review
Notes: 43. Protocols & Extensions
Update Notes: This course was originally recorded in 2019. It has been reviewed and all content and materials updated as of October 2021.
We’ve gone over how inheriting from a class is different than adopting a protocol. But it’s possible for a protocol to inherit from another protocol. And as we’ll see in a moment, unlike with superclasses, a protocol can inherit from more than one other protocol.
While inside of a protocol definition, you define requirements, not implementations. But there is somewhere else you can do that.
Extensions let you literally extend the functionality of a named type by adding methods, properties, and even initializers outside of the initial type definition. That’s true of any named typed, whether you wrote it or not.
With protocols, extensions have an extra special capability: they let you provide default implementations! We’ll come back to some general uses for extensions later in this episode, but right now, let’s try one out with a protocol.
We’re continuing to work with the same code from the last episode. Create a protocol named AloofAnimal, which inherits from Animal, but doesn’t have any new requirements.
protocol AloofAnimal: Animal { }
Using an extension, we can provide a default method, for speak.
42 extension AloofAnimal {
}
What we have for Cat, right now, would work great. So copy that over.
43 func speak() {
print("My name is \(name). Please leave me alone. I must look at this wall.")
}
Because an AloofAnimal is an Animal, it gets to use its name property. Now, a dog usually isn’t aloof, but a cat probably is, so let’s be more specific, and change Cat to adopt AloofAnimal instead of just Animal.
74 Cat: AloofAnimal {
And because every AloofAnimal has a default implementation of speak, we can delete the implementation from Cat…
74 Cat: AloofAnimal { }
…and our print statement is exactly the same, for Mr. Midnight. And maybe, in your app, you’d have certain things that were aloof, but not animals.
40 protocol Aloof {
}
And maybe they’d all have a name.
41 var name: String { get }
You could give them a greeting property, in an extension of Aloof.
44 extension Aloof {
var greeting: String {
}
}
And the beginning of what an AloofAnimal says, would work great.
46 return "My name is \(name). Please leave me alone."
Now, in Swift, say that AloofAnimal adopts Aloof and Animal, using a comma.
50 protocol AloofAnimal: Aloof, Animal { }
Now, you can use the greeting property, when it speaks!
54 print("\(greeting) I must look at this wall.")
Putting together the Aloof and Animal protocols is an example of how to use “composition”. It’s a much more flexible way to design your types than the strict hierarchy we saw with class inheritance. And as you’ve seen, structures –and even enumerations– can also adopt protocols (if they conform).
This was only a taste of using protocols, but it should give you a sense of how you might be able to use them, if class inheritance ever starts to feel like… maybe it’s not quite working to model shared behavior properly between your types.
If you don’t feel ready to write your own protocols yet, that’s okay! But some of your types will need to conform to protocols that Apple wrote.
Now that you know how protocols work, you should be ready to adopt them. You’ll be doing that a lot in everyday app development whether you end up using SwiftUI or UIKit. Both frameworks make extensive use of protocols.
We also worked a bit with extensions in this episode. There are a few important limitations on what you can put in an extension:
- No Stored Properties
- No Required or Designated Initializers
Those things need to be implemented in the original type definition. But Methods, Computed Properties, and convenience initializers are all open to you! That applies to all named types, not just protocols. That’s especially nice for making it easier to work with types that you didn’t write yourself.
There’s another common use for extensions, and that’s as an organizational tool. For example, you’ll often see protocol adoption declared with an extension, and any code required to conform to that protocol is written within the extension.
That way, you keep most of the code related to that protocol in one place! other than the exceptions, of course.
For example, we can move Cat’s protocol conformance declaration out into an extension.
class Cat {
let name: String
required init(name: String) {
self.name = name
}
}
extension Cat: AloofAnimal { }
And maybe we want to customize the speak method for Cat again. That could go in the extension as well.
func speak() {
print(greeting + "Meow!")
}
Ideally, it’d be great to have everything AloofAnimal-specific in that extension, but unfortunately, you can’t put stored properties or required initializers in extensions. Methods work, though!
Let’s leave our animals and take a detour to learn more about extensions. I’ve said that you can extend types that aren’t yours. That includes all of the named types you’ve learned about: Structures, Classes, Enumerations, and Protocols.
Extensions also don’t have to have anything to do with adopting protocols! It’s not uncommon to add behavior onto a basic Swift type like String, Int, or Double. Let’s try an example.
Here are two functions that take in an Int and tell you whether it’s odd or even. We can take these free functions and add them directly to Int with an extension!
extension Int {
}
Inside of that extension, copy and paste the functions.
func isEven(_ value: Int) -> Bool {
value % 2 == 0
}
func isOdd(_ value: Int) -> Bool {
(value + 1) % 2 == 0
}
We could turn these into methods, but let’s try turning these into computed properties, instead. To do that, replace func with var
var isEven(_ value: Int) -> Bool {
value % 2 == 0
}
var isOdd(_ value: Int) -> Bool {
(value + 1) % 2 == 0
}
and get rid of the parameter lists and return token.
var isEven😺:❌(_ value: Int) ->❌ Bool {
value % 2 == 0
}
var isOdd😺:❌(_ value: Int) ->❌ Bool {
(value + 1) % 2 == 0
}
We aren’t passing in anything called value anymore. But these will be properties of a particular Int instance, so we already know what the value of that is. We can access it with self!
var isEven😺:❌(_ value: Int) ->❌ Bool {
self % 2 == 0
}
var isOdd😺:❌(_ value: Int) ->❌ Bool {
(self + 1) % 2 == 0
}
Now, you can access both of these properties on any Int! Like, 5!
5.isOdd
5.isEven
One more - You may have thought to yourself, while doing some math in these Swift courses, that it would be handy to have a function or property that squares a number instead of you having to manually multiply it by itself all the time.
We can do that with an extension! But what to extend? We could do that for Ints or Doubles, but there’s something we could extend that would give us the same functionality for a lot of types. That something is the Numeric protocol!
extension Numeric {
}
It can be a bit of work to think about what the types you might want to extend have in common, and then find a common protocol, but some googling and digging through Swift documentation will get you there, eventually.
Numeric is defined as “A type with values that support multiplication.” We want to catch everything we could potentially multiply by itself, so, that seems appropriate! Now, make a squared computed property!
extension Numeric {
var squared: Self { }
}
With the capitalized Self, we’re saying that squared is the same type as whatever type the instance will be. This is getting into more advanced Swift territory! Now, multiply the instance by itself, with the lowercased self you’re used to!
extension Numeric {
var squared: Self { self * self }
}
And now you can use that on any type that conforms to Numeric. Try it out on an Int, or a UInt, or a Double or Float!
5.squared
5.5.squared
That one might have stretched your current knowledge of Swift a little bit, but you can always come back and take a look at it once you’ve gotten into more advanced topics.
There is one more super handy thing that extensions can do for you, but this one has to do with Structures in particular. I’ve got simplified versions of our Weekday enum and Time struct here.
Note that Time has default values set for both the day and time, but we still get a memberwise initializer that takes both as parameters, for free.
Time(day: .friday, hour: 17)
But maybe we also want an initializer that just takes the day, and still assumes the hour is 0.
init(day: Weekday) {
self.day = day
}
Now, we’ve lost access to that memberwise initializer.
There is a way to get it back though! If you add your own initializers in extensions, you can keep the compiler’s member-wise initializer. So, move that init into an extension of Time!
var hour: UInt = 0
😺}
extension Time {🛑
init(day: Weekday) {
self.day = day
}
}
Now if you run the playground, the memberwise initializer works again! And you can initialize a Time with no parameters, or just a day.
Time()
Time(day: .wednesday)
Wow! Extensions can do a lot for you! They let you add functionality to Swift types. Add default implementations to Protocols. Organize your protocol conforming code and keep a struct’s memberwise initializer even when you write your own, too.