Inheritance vs Composition

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Object oriented programming, at its core, provides two kinds of relationships between objects: “is-a” and “contains”. Before you learn about using model inheritance in your SwiftData app, here’s a quick refresher on whether you need to use inheritance in the first place. Open the starter project for this lesson to code along.

The “contains” relationship is usually called “Composition” and is where you compose objects (classes or structs) out of other objects. There are usually no (or very few) common properties between the objects. In the starter project, add a new Swift file to the Models folder called Cookbook. This class would be composed of, among other things, a collection of Recipe objects:

class Cookbook{
  var title: String
  var releaseDate: Date
  var recipes: [Recipe]
  
   init(title: String = "New Recipe", releaseDate: Date = Date(), 
     recipes: [Recipe] = []) {
    self.title = title
    self.releaseDate = releaseDate
    self.recipes = recipes
  }
}

As with the Recipe class, add a @Model macro to get this ready for SwiftData. This compositional relationship has been available since SwiftData was released back in iOS 17, and is noted with the @Relationship macro. Add that macro and give it a deleteRule: .cascade.

import Foundation
import SwiftData

@Model
class Cookbook{
  var title: String
  var releaseDate: Date
  
  @Relationship(deleteRule: .cascade)
  var recipes: [Recipe]
  
  init(title: String = "New Recipe", releaseDate: Date = Date(), 
    recipes: [Recipe] = []) {
    self.title = title
    self.releaseDate = releaseDate
    self.recipes = recipes
  }
}

You’ll notice that a similar relationship was provided between Recipe models and Ingredient models in the last lesson.

The only thing the Cookbook model and the Recipe model have in common is that the Cookbook is composed of Recipes. There are no shared properties or functions.

The “is-a” relationship, or “Inheritance”, allows for the sharing of properties and functions between parent objects and their children. Here’s the Recipe model from the last lesson:

@Model
class Recipe {
    @Attribute(.unique)
    var name: String
    var summary: String = ""
    var instructions: String = ""

    @Relationship(deleteRule: .cascade)
    var ingredients: [Ingredient]
    
    init(name: String, summary: String = "", instructions:  String = "", 
      ingredients: [Ingredient] = []) {
        
        self.name = name
        self.summary = summary
        self.instructions = instructions
        self.ingredients = ingredients
  }
  //sample data here...
}

This base recipe class is pretty, well, basic. A name, a summary, instructions, and a list of ingredients. There are lots of different types of recipes out there in the cooking world, so you could get a little more specific. You’ll create a class called BakedGood that extends from Recipe, and provide a panSize property. This is is an enum, PanSize. First, create a new Swift file and called it PanSize.

import Foundation

enum PanSize: Codable {
  case small
  case medium
  case large
}

Now for the BakedGood class. Create a new Swift file and call it BakedGood. Update it to the following:

import Foundation
import SwiftData

class BakedGood: Recipe {
  var panSize: PanSize
  
  init(name: String, summary: String = "", instructions: String = "", 
    ingredients: [Ingredient] = [], panSize: PanSize = .medium) {
    
    self.panSize = panSize
    super.init(name: name, summary: summary, instructions: instructions, 
      ingredients: ingredients)
  }

  required init(backingData: any SwiftData.BackingData<Recipe>) {
    fatalError("init(backingData:) has not been implemented")
  }
}

You might also want to capture the recipes for your favorite seasonal beverage. Create a new Beverage class, which also extends from Recipe but has two unique properties: season, an enum for which season this beverage is perfect for, and a bool describing whether the beverage is caffeinated or not. First add a new file called Season.swift. It should look like the following:

import Foundation

enum Season: Codable {
  case spring
  case summer
  case autumn
  case winter
}

Next, create a new swift file called Beverage.swift. It should look like the following:

import Foundation
import SwiftData

class Beverage: Recipe {

  var season: Season // a summer drink or a winter drink?
  var caffienated: Bool
  
  init(name: String, summary: String = "", instructions: String = "", 
    ingredients: [Ingredient] = [], season: Season, caffienated: Bool) {
    
    self.season = season
    self.caffienated = caffienated
    super.init(name: name, summary: summary, instructions: instructions, 
      ingredients: ingredients)
  }

  required init(backingData: any SwiftData.BackingData<Recipe>) {
    fatalError("init(backingData:) has not been implemented")
  }
}

As with the other @Models from earlier, macros are needed so SwiftData can recognize these as persistent models. Add those macros, but be prepared for some errors. In BakedGood.swift:

@Model
class BakedGood: Recipe {
	///....
}

And in Beverage.swift:

@Model
class Beverage: Recipe {
	///....
}

Once you add this code, the compiler shows errors. The ability to inherit a SwiftData @Model is restricted to iOS 26, so you need to specify platform availability. Add available macros to both BakedGood.swift:

@available(iOS 26, *)
@Model
class BakedGood: Recipe {
	///....
}

And Beverage.swift:

@available(iOS 26, *)
@Model
class Beverage: Recipe {
	///....
}

This of course means that older version of iOS won’t be able to support the new child @Models, so if you are going to support older versions of iOS, you’ll need to handle this all over your code!

These two classes, BakedGood and Beverage extend the existing Recipe type, inheriting the properties of the parent. They also add in class specific properties of their own that the base Receipe class doesn’t have.

The questions remains, however: do you need to use inheritance? Part of the answer lies in how the classes will be used in your app. If you never explicitly use the child classes by only referring to generic recipes when you perform queries then subclasses might not be needed. You could specify their differences in a shared property like the summary or the instructions. Conversely, if you only use the child classes, and don’t have many of them, those shared properties from the Recipe class just could be repeated in the child classes.

If you construct your app where you use both the parent and child classes, then inheritance will do the trick. In the next segment, you’ll learn how you can do that.

See forum comments
Download course materials from Github
Previous: Introduction Next: SwiftData Model Demo