Skip to main content
April 1, 2026Noble Desktop Publishing Team/11 min read

Initializers, Structs, Inheritance, & Extensions

Master Swift Classes, Structs, and Object-Oriented Programming

Core iOS Development Concepts

Designated Initializers

Set property values automatically when creating class instances. Essential for proper object initialization and data management.

Structures vs Classes

Learn when to use lightweight structs for data grouping versus full-featured classes for complex object hierarchies.

Inheritance & Extensions

Extend functionality through class inheritance and add methods to existing Apple frameworks using extensions.

Topics Covered in This iOS Development Tutorial:

Designated Initializers, Structs, Inheritance, Method Overriding, Extensions

Exercise Overview

This exercise takes you deeper into Swift's object-oriented programming fundamentals—concepts that remain the backbone of iOS development even as the ecosystem has evolved with SwiftUI and modern architectural patterns. You'll master designated initializers for precise object creation, explore the nuanced relationship between structs and classes, implement inheritance hierarchies, and learn to extend Apple's frameworks safely. These skills are essential whether you're building traditional UIKit applications or modern SwiftUI interfaces, as the underlying Swift language principles remain constant across all iOS development paradigms.

Learning Path Structure

1

Foundation Setup

Launch Xcode and prepare the playground environment with the Classes.playground file

2

Initializer Implementation

Create designated initializers to set property values during class instantiation

3

Struct Integration

Build and integrate structures within classes for organized data management

4

Inheritance Mastery

Implement class inheritance and method overriding for code reusability

Getting Started

  1. Launch Xcode if it isn't already open.

  2. If you completed the previous exercise, Classes.playground should still be open. If you closed it, re-open it now.

  3. We strongly recommend completing the previous exercise (3A) before proceeding, as this builds directly on those concepts. If you did not complete the previous exercise, follow these steps:

    • Go to File > Open.
    • Navigate to Desktop > Class Files > yourname-iOS App Dev 1 Class and double–click on Classes Ready for Initializers.playground.

Designated Initializers

When you create an instance of a class, you're invoking Swift's initialization process. Initialization prepares an instance for use by establishing initial values for all stored properties defined in the class. While Swift provides default initialization behavior, professional iOS development demands more control over object creation—this is where designated initializers become indispensable.

  1. To the far right of let mustang = Car(), mouse over the results sidebar and click the Quick Look eye quick look eye. In the pop-up, it displays nil because we declared speed and mpg as optionals, which default to nil until explicitly assigned values.

    A designated initializer serves as the primary initialization method for a class, ensuring that all necessary properties receive appropriate values at the moment of instantiation. Rather than creating objects with incomplete state, designated initializers enforce proper object construction from the outset—a crucial practice for building robust, maintainable applications.

  2. Designated initializers use the init keyword and can accept parameters to configure the new instance. Near the top of your class, implement the following designated initializer that requires both speed and mpg parameters:

    var mpg: Float?
    
    init(speed: Int, mpg: Float) {
       self.speed = speed
       self.mpg = mpg
    }
    
    func start() {
  3. Notice the red error red alert icon on the let mustang = Car() line near the bottom. Swift now requires you to use the designated initializer since we've defined one. Update the instantiation to provide the required parameters:

    let mustang = Car(speed: 0, mpg: 25.0)

    This pattern ensures that Car instances are always created with meaningful initial values, eliminating the uncertainty of nil properties and reducing potential runtime errors.

  4. In the results sidebar, next to let theGasUsed = mustang.gasUsed(milesDriven: 100.0) at the bottom, notice that theGasUsed still calculates to 3.333333. This occurs because our designated initializer sets mpg to 25.0, but the subsequent line overrides this value to 30.0.

  5. Remove that override line to see the designated initializer in action:

    let mustang = Car(speed: 0, mpg: 25.0)
    mustang.mpg = 30.0
    mustang.start()
  6. Check the results sidebar again—theGasUsed now displays 4, reflecting the calculation with the mpg value set by our designated initializer.

With initialization patterns established, let's explore how Swift's struct type complements classes in modern iOS architecture.

Default vs Designated Initializers

Without designated initializers, Swift assigns default values to optionals (nil). Designated initializers using the init keyword allow you to set required property values immediately upon class instantiation.

Initialization Methods Comparison

FeatureDefault InitializationDesignated Initialization
Property Valuesnil (for optionals)Set immediately
Code Requiredlet mustang = Car()let mustang = Car(speed: 0, mpg: 25.0)
Control LevelAutomaticDeveloper-defined
Recommended: Use designated initializers to ensure proper class setup with required starting values

Structures

Structures (structs) represent one of Swift's most powerful features, offering many capabilities similar to classes while providing distinct advantages in terms of memory management and thread safety. In contemporary iOS development, structs are heavily favored for data models, especially in SwiftUI applications where value semantics provide significant performance and safety benefits. Understanding when to choose structs over classes is a hallmark of experienced Swift developers.

  1. Declare a structure using the struct keyword. Below the Car class and above the mustang instance, add this Engine struct:

    func gasUsed(milesDriven: Float) -> Float {
          let gas = milesDriven / mpg!
          return gas
       }
    
    }
    
    struct Engine {
       var type = "V8"
       var horsePower = 300
    }
    
    let mustang = Car(speed: 0, mpg: 25.0)

    Note that structs differ from classes in several important ways: they don't support inheritance, they use value semantics (copying) rather than reference semantics, and they don't require explicit memory management. These characteristics make structs ideal for representing discrete data entities.

  2. Instantiate the struct using the same syntax as class instantiation. Swift automatically generates a memberwise initializer for structs:

    struct Engine {
       var type = "V8"
       var horsePower = 300
    }
    
    let engine = Engine()
    
    let mustang = Car(speed: 0, mpg: 25.0)
  3. Access the struct's properties using familiar dot notation:

    let engine = Engine()
    engine.type
    
    let mustang = Car(speed: 0, mpg: 25.0)
  4. The results sidebar displays V8, confirming that the property access works identically to classes.

  5. Now integrate the struct with our class. Inside the Car class variable declarations, create an Engine property:

    var mpg: Float?
    var engine: Engine
    
    init(speed: Int, mpg: Float) {

    This demonstrates a crucial Swift concept: defining custom structs or classes creates new types that can be used anywhere built-in types like String or Int are used. Following Apple's naming conventions, custom types should use UpperCamelCase (Engine, Car) to distinguish them from instances and variables.

  6. Swift's compiler will flag an error indicating that not all stored properties have been initialized. Complete the initialization in the designated initializer:

    init(speed: Int, mpg: Float) {
       self.speed = speed
       self.mpg = mpg
       self.engine = Engine()
    }
  7. Notice that the standalone let engine = Engine() line continues to work without errors, demonstrating that structs function independently or as components within larger class hierarchies.

  8. Reset the Engine struct to use more appropriate default values for initialization:

    struct Engine {
       var type = ""
       var horsePower = 0
    }
  9. Configure the Car instance's engine properties after initialization:

    let mustang = Car(speed: 0, mpg: 25.0)
    mustang.engine.type = "V8"
    mustang.engine.horsePower = 300
    mustang.start()
  10. Examine the results sidebar to see how property chaining works. The syntax mustang.engine.type traverses the object graph: from the Car instance's engine property, to the Engine struct, to the struct's type property. This property chaining is fundamental to working with complex data structures in iOS applications.

Having explored structs as data containers, let's examine how inheritance allows us to build sophisticated class hierarchies.

Structs vs Classes Feature Comparison

FeatureStructuresClasses
Variables & PropertiesYesYes
MethodsYesYes
InheritanceNoYes
Type CastingNoYes
Use CaseSmall data groupsComplex objects
Recommended: Use structs for simple data containers, classes for complex object hierarchies
Custom Types in Swift

Defining a new class or structure creates a new Swift type. Apple recommends UpperCamelCase naming (like Car, Engine) to match standard Swift types such as Bool, Double, Array, and Dictionary.

Inheritance

Inheritance enables one class to acquire properties and methods from another class, establishing hierarchical relationships that mirror real-world categorizations. This fundamental object-oriented principle allows developers to create specialized classes while reusing tested, proven code from parent classes. In iOS development, inheritance is particularly powerful when working with UIKit frameworks, where custom view controllers inherit from UIViewController and custom views inherit from UIView.

  1. Create a specialized Mustang class that inherits from the generic Car class. Position it between the engine constant and the mustang instance:

    engine.type
    
    class Mustang: Car {
    
    }
    
    let mustang = Car(speed: 0, mpg: 25.0)

    The : Car syntax establishes Mustang as a subclass of Car. This relationship means Mustang automatically inherits all of Car's properties and methods while maintaining the ability to add its own specialized functionality. The parent Car class, conversely, cannot access anything defined in its Mustang subclass.

  2. Add specialized properties to the Mustang class that define characteristics specific to this vehicle type:

    class Mustang: Car {
       var initialSpeed: Int = 10
       var milesPerGallon: Float = 20.0
    }
  3. Implement a custom initializer for the Mustang class:

    var milesPerGallon: Float = 20.0
    
       init() {
    
       }
    }
  4. Use the super keyword to call the parent class's designated initializer, passing values from the subclass properties:

    init() {
       super.init(speed: self.initialSpeed, mpg: self.milesPerGallon)
    }

    The super keyword provides access to the parent class's implementation, similar to how self references the current instance. This pattern allows subclasses to leverage parent class initialization logic while customizing the input values.

  5. Update the instance creation to use the new Mustang class:

    let mustang = Mustang()
    mustang.engine.type = "V8"

    Notice how the Mustang initializer requires no parameters, providing a more convenient interface while still ensuring proper initialization through the inheritance chain.

  6. Move the engine configuration into the Mustang initializer for better encapsulation. Remove these lines:

    let mustang = Mustang()
    mustang.engine.type = "V8"
    mustang.engine.horsePower = 300
    mustang.start()
  7. Add the engine configuration directly in the designated initializer:

    init() {
       super.init(speed: self.initialSpeed, mpg: self.milesPerGallon)
       self.engine.type = "V8"
       self.engine.horsePower = 300
    }
  8. Verify that the initialization worked by accessing the engine type:

    let mustang = Mustang()
    mustang.start()
    mustang.engine.type
  9. The results sidebar displays V8, confirming successful initialization. Remove the verification line since it's no longer needed:

    mustang.engine.type
  10. Add a Mustang-specific property to demonstrate subclass specialization:

    class Mustang: Car {
       var initialSpeed: Int = 10
       var milesPerGallon: Float = 20.0
       var type = "Mach 1"
    
       init() {
  11. Access this subclass-specific property:

    let mustang = Mustang()
    mustang.start()
    mustang.type

    The results sidebar shows Mach 1, demonstrating how subclasses can extend parent functionality with specialized features.

  12. Demonstrate the one-way nature of inheritance by attempting to access a subclass property from a parent class instance:

    let car = Car(speed: 30, mpg: 20.0)
    car.type
    
    let mustang = Mustang()

    This code produces an error because parent classes cannot access properties or methods defined in their subclasses—inheritance flows only downward in the class hierarchy.

  13. Remove the demonstration code to clean up your playground:

    let car = Car(speed: 30, mpg: 20.0)
    car.type

    This inheritance pattern is extensively used throughout iOS frameworks, allowing you to create custom components that build upon Apple's robust foundation classes while adding application-specific functionality.

Now that you understand inheritance hierarchies, let's explore how to customize inherited behavior through method overriding.

Inheritance Hierarchy Benefits

Code Reusability

Subclasses inherit all properties and methods from parent classes, eliminating code duplication and maintaining consistency.

Specialized Functionality

Child classes can add unique properties while accessing parent functionality. Parent classes cannot access child-specific features.

Super Keyword Usage

Use super.init() to pass subclass properties to parent initializers, enabling seamless inheritance chains.

Method Overriding

Method overriding allows subclasses to provide specialized implementations of methods inherited from their parent classes. This capability is essential for customizing framework behavior—for instance, overriding viewDidLoad() in UIViewController subclasses or drawRect() in custom UIView implementations. Swift's explicit override keyword prevents accidental method replacement while enabling intentional customization of inherited behavior.

  1. Add a Mustang-specific method that we'll incorporate into an overridden parent method:

    self.engine.type = "V8"
       self.engine.horsePower = 300
    }
    
    func revEngine() {
       print("The engine is revving")
    }
  2. Override the inherited start method using the override keyword, which is required for replacing parent class methods:

    func revEngine() {
       print("The engine is revving")
    }
    
    override func start() {
       self.revEngine()
       super.start()
    }
  3. Examine the results sidebar next to the print function to see The engine is revving has been output automatically when the override was defined.

    The method override creates a specialized version of start() that first calls the Mustang-specific revEngine() method, then delegates to the parent class's start() implementation using super.start(). When mustang.start() is called, Swift executes the overridden version in the Mustang subclass rather than the original Car implementation, demonstrating polymorphic behavior.

With inheritance and overriding mastered, let's explore how extensions provide another powerful way to enhance existing classes.

Method Overriding Implementation

1

Add Custom Methods

Create subclass-specific methods like revEngine() for specialized functionality

2

Use Override Keyword

Implement override func methodName() to replace parent class method behavior

3

Call Parent Methods

Use super.methodName() within overridden methods to maintain parent functionality

Override Method Execution

When calling mustang.start() on an overridden method, it executes the subclass version first, which can include both custom functionality (revEngine) and parent class behavior (super.start()).

Extensions

Extensions provide a sophisticated mechanism for adding functionality to existing classes, structs, and enums—even those you don't have source code access to, such as Apple's framework classes or third-party libraries. This capability is invaluable in iOS development, where you frequently need to enhance UIKit or Foundation classes with custom behavior specific to your application's needs. Extensions maintain clean separation between original implementation and custom additions while preserving type safety.

  1. Create an extension for UIAlertController, one of Apple's framework classes used for displaying alert dialogs:

    super.start()
       }
    }
    
    extension UIAlertController {
    
    }
    
    let mustang = Mustang()
    mustang.start()

    The extension keyword enables you to augment existing types without modifying their original source code—a particularly powerful feature when working with compiled frameworks or when you want to organize functionality across multiple files.

  2. Add a custom method to the UIAlertController class through the extension:

    extension UIAlertController {
       func gasAlert() {
          print("The car is low on gas!")
       }
    }
  3. Create an instance of the UIAlertController class to demonstrate how extensions integrate seamlessly with existing classes:

    extension UIAlertController {
       func gasAlert() {
          print("The car is low on gas!")
       }
    }
    
    let alert = UIAlertController()

    The alert instance is now a fully functional UIAlertController that includes both Apple's original implementation and your custom gasAlert() method.

  4. Call the custom method to see the extension in action:

    let alert = UIAlertController()
    alert.gasAlert()
  5. The results sidebar displays The car is low on gas!, confirming that your extension method has been successfully integrated into the UIAlertController class.

    Extensions are particularly valuable for organizing code functionality, implementing protocol conformance, and adding convenience methods to framework classes. They maintain the same access levels and capabilities as the original type while keeping custom code clearly separated and organized.

  6. Save and close the file. Keep Xcode open for the next exercise.

Extensions for Third-Party Code

Pros
Add custom methods to Apple frameworks like UIAlertController
Extend functionality without accessing source code
Maintain clean separation between original and custom code
Apply to any existing class or structure
Cons
Cannot override existing methods
Limited to adding new functionality only
May conflict with future framework updates
Extension Use Cases

Extensions are particularly valuable when working with Apple frameworks or third-party libraries where you cannot modify the original source code but need additional functionality.

Key Takeaways

1Designated initializers using the init keyword allow setting property values immediately when creating class instances, replacing default nil values
2Structures are ideal for smaller data groupings and lack advanced features like inheritance, type casting, and reference counting that classes provide
3Class inheritance enables subclasses to inherit all parent properties and methods while adding specialized functionality unavailable to parent classes
4Method overriding uses the override keyword to replace parent class methods while super keyword accesses parent class functionality within subclasses
5Extensions provide a way to add custom methods to existing classes, including Apple frameworks, without requiring access to original source code
6Swift treats custom classes and structures as new data types, following UpperCamelCase naming conventions like built-in types
7Property chaining allows accessing nested properties through multiple levels, such as mustang.engine.type for struct properties within classes
8Inheritance promotes code reusability by creating base classes with generic functionality that can be customized through specialized subclasses

RELATED ARTICLES