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

Object oriented programming is a fundamental programming paradigm that you must master if you are serious about learning Swift. That’s because object oriented programming is at the heart of most frameworks you’ll be working with. Breaking a problem down into objects that send messages to one another might seem strange at first, but it’s a proven approach for simplifying complex systems, which dates back to the 1950s.

Objects can be used to model almost anything — coordinates on a map, touches on a screen, even fluctuating interest rates in a bank account. When you’re just starting out, it’s useful to practice modeling physical things in the real world before you extend this to more abstract concepts.

In this tutorial, you’ll use object oriented programming to create your own band of musical instruments. You’ll also learn many important concepts along the way including:

  • Encapsulation
  • Inheritance
  • Overriding versus Overloading
  • Types versus Instances
  • Composition
  • Polymorphism
  • Access Control

That’s a lot, so let’s get started! :]

Getting Started

Fire up Xcode and go to File\New\Playground…. Type Instruments for Name, select iOS for Platform and click Next. Choose where to save your playground and click Create. Delete everything from it in order to start from scratch.

Designing things in an object-oriented manner usually begins with a general concept extending to more specific types. You want to create musical instruments, so it makes perfect sense to begin with an instrument type and then define concrete (not literally!) instruments such as pianos and guitars from it. Think of the whole thing as a family tree of instruments where everything flows from general to specific and top to bottom like this:

Object Oriented Programming Relationship Diagram

The relationship between a child type and its parent type is an is-a relationship. For example, “Guitar is-a Instrument.” Now that you have a visual understanding of the objects you are dealing with, it’s time to start implementing.

Properties

Add the following block of code at the top of the playground:

// 1
class Instrument {
  // 2
  let brand: String
  // 3
  init(brand: String) {
    //4 
    self.brand = brand
  }
}

There’s quite a lot going on here, so let’s break it down:

  1. You create the Instrument base class with the class keyword. This is the root class of the instruments hierarchy. It defines a blueprint which forms the basis of any kind of instrument. Because it’s a type, the name Instrument is capitalized. It doesn’t have to be capitalized, however this is the convention in Swift.
  2. You declare the instrument’s stored properties (data) that all instruments have. In this case, it’s just the brand, which you represent as a String.
  3. You create an initializer for the class with the init keyword. Its purpose is to construct new instruments by initializing all stored properties.
  4. You set the instrument’s brand stored property to what was passed in as a parameter. Since the property and the parameter have the same name, you use the self keyword to distinguish between them.

You’ve implemented a class for instruments containing a brand property, but you haven’t given it any behavior yet. Time to add some behavior in the form of methods to the mix.

Methods

You can tune and play an instrument regardless of its particular type. Add the following code inside the Instrument class right after the initializer:

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

The tune() method is a placeholder function that crashes at runtime if you call it. Classes with methods like this are said to be abstract because they are not intended for direct use. Instead, you must define a subclass that overrides the method to do something sensible instead of only calling fatalError(). More on overriding later.

Functions defined inside a class are called methods because they have access to properties, such as brand in the case of Instrument. Organizing properties and related operations in a class is a powerful tool for taming complexity. It even has a fancy name: encapsulation. Class types are said to encapsulate data (e.g. stored properties) and behavior (e.g. methods).

Next, add the following code before your Instrument class:

class Music {
  let notes: [String]

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

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

This is a Music class that encapsulates an array of notes and allows you to flatten it into a string with the prepared() method.

Add the following method to the Instrument class right after the tune() method:

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

The play(_:) method returns a String to be played. You might wonder why you would bother creating a special Music type, instead of just passing along a String array of notes. This provides several advantages: Creating Music helps build a vocabulary, enables the compiler to check your work, and creates a place for future expansion.

Next, add the following method to the Instrument class right after play(_:):

func perform(_ music: Music) {
  print(tune())
  print(play(music))
}

The perform(_:) method first tunes the instrument and then plays the music given in one go. You’ve composed two of your methods together to work in perfect symphony. (Puns very much intended! :])

That’s it as far as the Instrument class implementation goes. Time to add some specific instruments now.

Inheritance

Add the following class declaration at the bottom of the playground, right after the Instrument class implementation:

// 1
class Piano: Instrument {
  let hasPedals: Bool
  // 2
  static let whiteKeys = 52
  static let blackKeys = 36
  
  // 3
  init(brand: String, hasPedals: Bool = false) {
    self.hasPedals = hasPedals
    // 4
    super.init(brand: brand)
  }
  
  // 5
  override func tune() -> String {
    return "Piano standard tuning for \(brand)."
  }
  
  override func play(_ music: Music) -> String {
    // 6 
    let preparedNotes = super.play(music)
    return "Piano playing \(preparedNotes)"
  }
}

Here’s what’s going on, step by step:

  1. You create the Piano class as a subclass of the Instrument parent class. All the stored properties and methods are automatically inherited by the Piano child class and available for use.
  2. All pianos have exactly the same number of white and black keys regardless of their brand. The associated values of their corresponding properties don’t change dynamically, so you mark the properties as static in order to reflect this.
  3. The initializer provides a default value for its hasPedals parameter which allows you to leave it off if you want.
  4. You use the super keyword to call the parent class initializer after setting the child class stored property hasPedals. The super class initializer takes care of initializing inherited properties — in this case, brand.
  5. You override the inherited tune() method’s implementation with the override keyword. This provides an implementation of tune() that doesn’t call fatalError(), but rather does something specific to Piano.
  6. You override the inherited play(_:) method. And inside this method, you use the super keyword this time to call the Instrument parent method in order to get the music’s prepared notes and then play on the piano.

Because Piano derives from Instrument, users of your code already know a lot about it: It has a brand, it can be tuned, played, and can even be performed.

Note: Swift classes use an initialization process called two-phase-initialization to guarantee that all properties are initialized before you use them. If you want to learn more about initialization, check out our tutorial series on Swift initialization.

The piano tunes and plays accordingly, but you can play it in different ways. Therefore, it’s time to add pedals to the mix.

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.