Start learning free

LESSON 410 OF 415 · OBJECT-ORIENTED PROGRAMMING

OOP Capstone in Swift: An Animal Class Hierarchy

Lesson Read the full explanation

TL ; DR:

Put it all together: a parent class with a placeholder method, three concrete subclasses, and a polymorphic loop that dispatches by runtime type. This is the shape of nearly every real OOP design.


class Animal {
    var name: String
    init(_ name: String) { self.name = name }
    func sound() -> String { return "" }  // each subclass picks
    func speak() {                         // shared by all
        print("\(name): \(sound())")
    }
}
// Dog, Cat, Bird each override sound()
// let zoo: [Animal] = [Dog("Rex"), ...]
// for a in zoo { a.speak() }


Full lesson:

This problem stitches together everything you've learned in this chapter:

  1. Inheritance — three concrete classes share a common parent.
  2. Initializers — each subclass inherits the parent's init(_ name:), so Dog("Rex") stores the name.
  3. Abstract-like methods — Swift has no abstract keyword, so the parent's sound() is a placeholder that returns ""; each child overrides it with its own sound.
  4. Shared behavior — the parent has a concrete speak() that calls sound(); every subclass inherits it for free.
  5. Polymorphism — a collection typed as Animal holds a Dog, a Cat, and a Bird; speak() dispatches to the right sound() at runtime.

The shape of this code is the shape of nearly every real OOP design you'll see — a small contract at the top (the parent class), concrete variants below, and the rest of the program written against the abstract type. New variants get added without touching the consumer code.


Your task

The Animal class is already defined, with a concrete speak() that calls the placeholder sound(). Add three subclasses — Dog (returns "Woof"), Cat (returns "Meow"), and Bird (returns "Tweet"). Then in mainFunction, build a polymorphic collection holding one of each (Rex, Whiskers, Tweety in that order) and loop through it calling speak().

Expected output:

Rex: Woof
Whiskers: Meow
Tweety: Tweet
STEP 1 OF 5
    Ctrl + Enter to check

    SWIFT · OBJECT-ORIENTED PROGRAMMING

    Oop Capstone in Swift, explained

    What the capstone combines

    This exercise puts the chapter's ideas into one small program: a parent class, three subclasses, and a loop that treats them all as the parent type. It uses four ideas:

    • Inheritance. Dog, Cat and Bird all inherit from Animal.
    • Initializers. Each animal takes a name, and the parent stores it.
    • Overriding. The parent has a placeholder sound() that each subclass replaces.
    • Polymorphism. An array typed [Animal] holds all three, and one call per element picks the right sound.

    The shape is common: a small contract at the top, concrete variants below, and the consuming code written against the parent type. Adding a fourth animal later does not change the loop.

    The parent class

    The starter code gives you Animal:

    class Animal {
        var name: String
        init(_ name: String) { self.name = name }
        func sound() -> String { return "" }
        func speak() {
            print("\(name): \(sound())")
        }
    }
    

    Notice the split of work. sound() is the part that varies, so the parent gives it a neutral default, an empty string. speak() is the part that stays the same, so it is written once: it builds the line name: sound by calling sound(). Inside speak(), the call sound() means self.sound(), and Swift runs the version that belongs to the object's actual class. This is the template pattern: shared code in the parent calls a method the subclasses fill in.

    The initializer is written init(_ name: String), with an underscore, so callers pass the name without a label: Dog("Rex"), not Dog(name: "Rex").

    In Swift this parent is only "abstract by convention". Swift does not force a subclass to override sound(); one that forgets simply inherits the empty string, and its speak() prints the name followed by a colon and a space.

    Step by step

    1. Write the three subclasses. Each one is class Name: Animal with one method, override func sound() -> String, returning "Woof", "Meow" or "Tweet". They override only sound(). They do not touch speak() or the stored name.
    2. Skip the initializers. A subclass that adds no stored properties and defines no initializers of its own inherits all of the parent's designated initializers. That is why Dog("Rex") works with no code in Dog for it.
    3. Build the array in order. Inside mainFunction, declare let animals: [Animal] = [Dog("Rex"), Cat("Whiskers"), Bird("Tweety")]. The order is the order of the output.
    4. Loop and speak. Write for a in animals { a.speak() }. Each pass calls the inherited speak(), which calls the right sound().

    The solution in Swift

    class Animal {
        var name: String
        init(_ name: String) { self.name = name }
        func sound() -> String { return "" }
        func speak() {
            print("\(name): \(sound())")
        }
    }
    
    class Dog: Animal {
        override func sound() -> String { return "Woof" }
    }
    
    class Cat: Animal {
        override func sound() -> String { return "Meow" }
    }
    
    class Bird: Animal {
        override func sound() -> String { return "Tweet" }
    }
    
    class Solution {
        func mainFunction() {
            let animals: [Animal] = [Dog("Rex"), Cat("Whiskers"), Bird("Tweety")]
            for a in animals {
                a.speak()
            }
        }
    }
    

    Running mainFunction() prints:

    Rex: Woof
    Whiskers: Meow
    Tweety: Tweet
    

    Swift details that matter

    • String interpolation. "\(name): \(sound())" inserts the value of any expression between \( and ), including a method call.
    • Extending the hierarchy. A Cow subclass with its own sound() returning "Moo" works as Cow("Bessie").speak(), which prints Bessie: Moo, and fits in the same array without changes to the loop.
    • When a subclass needs its own data, it must write an initializer. It sets its own properties first and then calls super.init(name):
    class Parrot: Animal {
        var word: String
        init(_ name: String, word: String) {
            self.word = word
            super.init(name)
        }
        override func sound() -> String { return word }
    }
    

    Parrot("Polly", word: "Hello").speak() prints Polly: Hello. Without the init, the class would not compile, because word has no initial value.

    Common mistakes

    • Forgetting override. Writing func sound() in Dog is a compile error that says an override keyword is required.
    • Overriding speak() instead of sound(). It works, but each subclass then repeats the formatting. The point of the design is that the line format lives in one place.
    • Wrong order in the array. The output follows the array, so Rex, Whiskers and Tweety must be in that order.
    • Using labels on the constructor call. The initializer's first parameter has an underscore, so Dog(name: "Rex") does not compile.
    • Calling super.init first in a subclass with its own properties. Swift requires the subclass's own properties to be set before super.init is called.

    Key takeaways

    • Put shared behavior in the parent and the varying part in an overridable method.
    • Subclasses inherit the parent's initializers when they add no stored properties of their own.
    • An array typed with the parent class lets one loop work with every subclass.
    • Swift picks the overridden method from the object's real class at run time.
    Keep going with SwiftAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free