Swift Protocol Inheritance
In Swift, protocols aren't just standalone entities - they can build upon each other through a concept called protocol inheritance. This powerful feature allows you to create hierarchies of protocols, leading to more organized and modular code.
Introduction to Protocol Inheritance
Protocol inheritance is the ability for one protocol to inherit requirements from another protocol. When a protocol inherits from another protocol, any type that adopts the child protocol must satisfy all requirements of both the child and parent protocols.
This concept is similar to class inheritance but applies to interfaces rather than implementations.
Basic Protocol Inheritance Syntax
The syntax for protocol inheritance uses the same colon notation as class inheritance:
protocol ParentProtocol {
// requirements
}
protocol ChildProtocol: ParentProtocol {
// additional requirements
}
Any type that conforms to ChildProtocol
must fulfill both the requirements of ChildProtocol
and ParentProtocol
.
Simple Example of Protocol Inheritance
Let's start with a basic example to understand protocol inheritance:
protocol Identifiable {
var id: String { get }
func identify()
}
protocol DetailedIdentifiable: Identifiable {
var name: String { get }
var description: String { get }
}
In this example, DetailedIdentifiable
inherits from Identifiable
. Now, let's implement these protocols:
struct User: DetailedIdentifiable {
var id: String
var name: String
var description: String
func identify() {
print("User ID: \(id)")
}
}
let user = User(id: "U123", name: "John Doe", description: "Administrator")
user.identify() // Output: User ID: U123
Notice that User
must implement all properties and methods from both protocols: id
and identify()
from Identifiable
, and name
and description
from DetailedIdentifiable
.
Multiple Protocol Inheritance
Unlike classes in Swift, protocols can inherit from multiple protocols. This is a powerful feature that enables composition:
protocol Named {
var name: String { get }
}
protocol Aged {
var age: Int { get }
}
protocol Person: Named, Aged {
var occupation: String { get }
}
Implementing a type that conforms to a protocol with multiple inheritance:
struct Employee: Person {
var name: String
var age: Int
var occupation: String
}
let employee = Employee(name: "Jane Smith", age: 28, occupation: "Software Developer")
print("\(employee.name), \(employee.age), \(employee.occupation)")
// Output: Jane Smith, 28, Software Developer
Protocol Inheritance with Default Implementations
Protocol inheritance becomes even more powerful when combined with protocol extensions, which provide default implementations:
protocol Vehicle {
var speed: Double { get }
func accelerate()
}
extension Vehicle {
func accelerate() {
print("Accelerating to \(speed) mph")
}
}
protocol ElectricVehicle: Vehicle {
var batteryLevel: Int { get }
func charge()
}
extension ElectricVehicle {
func charge() {
print("Charging battery to 100%")
}
}
Implementing a type that uses default implementations:
struct Tesla: ElectricVehicle {
var speed: Double
var batteryLevel: Int
}
let modelS = Tesla(speed: 120, batteryLevel: 80)
modelS.accelerate() // Output: Accelerating to 120.0 mph
modelS.charge() // Output: Charging battery to 100%
Real-World Example: Media Player Application
Let's explore a more realistic example of protocol inheritance in a media player application:
protocol Playable {
var title: String { get }
var duration: Double { get }
func play()
func pause()
}
protocol AudioPlayable: Playable {
var volume: Float { get set }
func adjustEqualizer(bass: Float, mid: Float, treble: Float)
}
protocol VideoPlayable: Playable {
var resolution: String { get }
var aspectRatio: String { get }
func adjustBrightness(level: Float)
}
Now we can create implementations for audio and video content:
class Song: AudioPlayable {
var title: String
var duration: Double
var volume: Float = 0.5
var artist: String
init(title: String, duration: Double, artist: String) {
self.title = title
self.duration = duration
self.artist = artist
}
func play() {
print("Playing song: \(title) by \(artist)")
}
func pause() {
print("Song paused")
}
func adjustEqualizer(bass: Float, mid: Float, treble: Float) {
print("Setting equalizer - Bass: \(bass), Mid: \(mid), Treble: \(treble)")
}
}
class Movie: VideoPlayable {
var title: String
var duration: Double
var resolution: String
var aspectRatio: String
var director: String
init(title: String, duration: Double, resolution: String, aspectRatio: String, director: String) {
self.title = title
self.duration = duration
self.resolution = resolution
self.aspectRatio = aspectRatio
self.director = director
}
func play() {
print("Playing movie: \(title) directed by \(director)")
}
func pause() {
print("Movie paused")
}
func adjustBrightness(level: Float) {
print("Setting brightness to \(level)")
}
}
Now let's create a media player that can handle both types:
func playContent(_ content: Playable) {
content.play()
// We can also check and use the specific type
if let audioContent = content as? AudioPlayable {
audioContent.adjustEqualizer(bass: 0.8, mid: 0.5, treble: 0.6)
}
if let videoContent = content as? VideoPlayable {
videoContent.adjustBrightness(level: 0.7)
}
}
let song = Song(title: "Bohemian Rhapsody", duration: 354, artist: "Queen")
let movie = Movie(title: "The Matrix", duration: 7380, resolution: "4K",
aspectRatio: "16:9", director: "Wachowski Sisters")
playContent(song)
// Output:
// Playing song: Bohemian Rhapsody by Queen
// Setting equalizer - Bass: 0.8, Mid: 0.5, Treble: 0.6
playContent(movie)
// Output:
// Playing movie: The Matrix directed by Wachowski Sisters
// Setting brightness to 0.7
Protocol Composition
Sometimes you want a type to conform to multiple protocols without creating a new protocol. Swift offers protocol composition using the &
operator:
protocol Payable {
var salary: Double { get }
}
protocol Employable {
var position: String { get }
}
func printEmployeeInfo(employee: Payable & Employable) {
print("Position: \(employee.position), Salary: $\(employee.salary)")
}
struct Worker: Payable, Employable {
var salary: Double
var position: String
}
let developer = Worker(salary: 90000, position: "iOS Developer")
printEmployeeInfo(employee: developer)
// Output: Position: iOS Developer, Salary: $90000.0
Best Practices for Protocol Inheritance
When working with protocol inheritance, consider these guidelines:
-
Keep protocols focused: Each protocol should represent a single, well-defined capability.
-
Favor composition over deep hierarchies: Instead of creating deep inheritance chains, consider using protocol composition.
-
Use protocol extensions wisely: Provide default implementations when they make sense for most conforming types.
-
Consider the "is-a" relationship: A protocol should inherit from another only if it truly represents a more specialized version.
-
Be mindful of requirements: Each requirement you add creates an obligation for conforming types.
Summary
Protocol inheritance in Swift allows you to:
- Create hierarchical relationships between protocols
- Inherit requirements from multiple protocols
- Build modular interfaces that types can adopt
- Combine with protocol extensions for powerful default implementations
This feature enables you to design flexible, modular code that follows interface segregation principles while maintaining clear type relationships.
Exercises
-
Create a
Drawable
protocol with basic drawing functionality, then createAnimatedDrawable
that inherits from it with animation-specific requirements. -
Design a set of protocols for a banking app:
Account
as a base protocol, withCheckingAccount
andSavingsAccount
inheriting from it. Implement aBank
struct that manages different account types. -
Create a protocol hierarchy for a shopping cart system with
Item
,DiscountableItem
, andTaxableItem
protocols. Implement products that conform to different combinations of these protocols.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)