Object Oriented Programming in Swift

Learn how object oriented programming works in Swift by breaking things down into objects that can be inherited and composed from. By Cosmin Pupăză.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Composition

Now that you have a handy amplifier component, it’s time to use it in an electric guitar. Add the ElectricGuitar class implementation at the end of the playground right after the Amplifier class declaration:

// 1
class ElectricGuitar: Guitar {
  // 2 
  let amplifier: Amplifier
  
  // 3
  init(brand: String, stringGauge: String = "light", amplifier: Amplifier) {
    self.amplifier = amplifier
    super.init(brand: brand, stringGauge: stringGauge)
  }
  
  // 4
  override func tune() -> String {
    amplifier.plugIn()
    amplifier.volume = 5
    return "Tune \(brand) electric with E A D G B E"
  }
  
  // 5
  override func play(_ music: Music) -> String {
    let preparedNotes = super.play(music)
    return "Play solo \(preparedNotes) at volume \(amplifier.volume)."
  }
}

Taking this step by step:

  1. ElectricGuitar is a concrete type that derives from the abstract, intermediate base class Guitar.
  2. An electric guitar contains an amplifier. This is a has-a relationship and not an is-a relationship as with inheritance.
  3. A custom initializer that initializes all of the stored properties and then calls the super class.
  4. A reasonable tune() method.
  5. A reasonable play() method.

In a similar vain, add the BassGuitar class declaration at the bottom of the playground right after the ElectricGuitar class implementation:

class BassGuitar: Guitar {
  let amplifier: Amplifier

  init(brand: String, stringGauge: String = "heavy", amplifier: Amplifier) {
    self.amplifier = amplifier
    super.init(brand: brand, stringGauge: stringGauge)
  }

  override func tune() -> String {
    amplifier.plugIn()
    return "Tune \(brand) bass with E A D G"
  }

  override func play(_ music: Music) -> String {
    let preparedNotes = super.play(music)
    return "Play bass line \(preparedNotes) at volume \(amplifier.volume)."
  }
}

This creates a bass guitar which also utilizes a (has-a) amplifier. Class containment in action. Time for another challenge!

[spoiler title=”Electric Guitar”]Add the following test code at the bottom of the playground right after the BassGuitar class declaration:

[/spoiler]

Challenge: You may have heard that classes follow reference semantics. This means that variables holding a class instance actually hold a reference to that instance. If you have two variables with the same reference, changing data in one will change data in the other, and it’s actually the same thing. Show reference semantics in action by instantiating an amplifier and sharing it between a Gibson electric guitar and a Fender bass guitar.

[spoiler title=”Electric Guitar”]Add the following test code at the bottom of the playground right after the BassGuitar class declaration:

let amplifier = Amplifier()
let electricGuitar = ElectricGuitar(brand: "Gibson", stringGauge: "medium", amplifier: amplifier)
electricGuitar.tune()

let bassGuitar = BassGuitar(brand: "Fender", stringGauge: "heavy", amplifier: amplifier)
bassGuitar.tune()

// Notice that because of class reference semantics, the amplifier is a shared
// resource between these two guitars.

bassGuitar.amplifier.volume
electricGuitar.amplifier.volume

bassGuitar.amplifier.unplug()
bassGuitar.amplifier.volume
electricGuitar.amplifier.volume

bassGuitar.amplifier.plugIn()
bassGuitar.amplifier.volume
electricGuitar.amplifier.volume

[/spoiler]

let amplifier = Amplifier()
let electricGuitar = ElectricGuitar(brand: "Gibson", stringGauge: "medium", amplifier: amplifier)
electricGuitar.tune()

let bassGuitar = BassGuitar(brand: "Fender", stringGauge: "heavy", amplifier: amplifier)
bassGuitar.tune()

// Notice that because of class reference semantics, the amplifier is a shared
// resource between these two guitars.

bassGuitar.amplifier.volume
electricGuitar.amplifier.volume

bassGuitar.amplifier.unplug()
bassGuitar.amplifier.volume
electricGuitar.amplifier.volume

bassGuitar.amplifier.plugIn()
bassGuitar.amplifier.volume
electricGuitar.amplifier.volume

Polymorphism

One of the great strengths of object oriented programming is the ability to use different objects through the same interface while each behaves in its own unique way. This is polymorphism meaning “many forms”. Add the Band class implementation at the end of the playground:

class Band {
  let instruments: [Instrument]
  
  init(instruments: [Instrument]) {
    self.instruments = instruments
  }
  
  func perform(_ music: Music) {
    for instrument in instruments {
      instrument.perform(music)
    }
  }
}

The Band class has an instruments array stored property which you set in the initializer. The band performs live on stage by going through the instruments array in a for in loop and calling the perform(_:) method for each instrument in the array.

Now go ahead and prepare your first rock concert. Add the following block of code at the bottom of the playground right after the Band class implementation:

let instruments = [piano, acousticGuitar, electricGuitar, bassGuitar]
let band = Band(instruments: instruments)
band.perform(music)

You first define an instruments array from the Instrument class instances you’ve previously created. Then you declare the band object and configure its instruments property with the Band initializer. Finally you use the band instance’s perform(_:) method to make the band perform live music (print results of tuning and playing).

Notice that although the instruments array’s type is [Instrument], each instrument performs accordingly depending on its class type. This is how polymorphism works in practice: you now perform in live gigs like a pro! :]

Note: If you want to learn more about classes, check out our tutorial on Swift enums, structs and classes.

Access Control

You have already seen private in action as a way to hide complexity and protect your classes from inadvertently getting into invalid states (i.e. breaking the invariant). Swift goes further and provides four levels of access control including:

  • private: Visible just within the class.
  • fileprivate: Visible from anywhere in the same file.
  • internal: Visible from anywhere in the same module or app.
  • public: Visible anywhere outside the module.

There are additional access control related keywords:

  • open: Not only can it be used anywhere outside the module but also can be subclassed or overridden from outside.
  • final: Cannot be overridden or subclassed.

If you don’t specify the access of a class, property or method, it defaults to internal access. Since you typically only have a single module starting out, this lets you ignore access control concerns at the beginning. You only really need to start worrying about it when your app gets bigger and more complex and you need to think about hiding away some of that complexity.

Making a Framework

Suppose you wanted to make your own music and instrument framework. You can simulate this by adding definitions to the compiled sources of your playground. First, delete the definitions for Music and Instrument from the playground. This will cause lots of errors that you will now fix.

Make sure the Project Navigator is visible in Xcode by going to View\Navigators\Show Project Navigator. Then right-click on the Sources folder and select New File from the menu. Rename the file MusicKit.swift and delete everything inside it. Replace the contents with:

// 1
final public class Music {
  // 2
  public let notes: [String]

  public init(notes: [String]) {
    self.notes = notes
  }

  public func prepared() -> String {
    return notes.joined(separator: " ")
  }
}

// 3
open class Instrument {
  public let brand: String

  public init(brand: String) {
    self.brand = brand
  }

  // 4 
  open func tune() -> String {
    fatalError("Implement this method for \(brand)")
  }

  open func play(_ music: Music) -> String {
    return music.prepared()
  }

  // 5
  final public func perform(_ music: Music) {
    print(tune())
    print(play(music))
  }
}

Save the file and switch back to the main page of your playground. This will continue to work as before. Here are some notes for what you’ve done here:

  1. final public means that is going to be visible by all outsiders but you cannot subclass it.
  2. Each stored property, initializer, method must be marked public if you want to see it from an outside source.
  3. The class Instrument is marked open because subclassing is allowed.
  4. Methods can also be marked open to allow overriding.
  5. Methods can be marked final so no one can override them. This can be a useful guarantee.
Cosmin Pupăză

Contributors

Cosmin Pupăză

Author

Ray Fix

Tech Editor and Team Lead

Chris Belanger

Editor

Matt Galloway

Final Pass Editor

Over 300 content creators. Join our team.