Think in Swift: Recipe Scaling Engine

Build a recipe scaling engine in Swift using type-driven design.

Introduction

30 Second Summary

You know how doubling a recipe sounds simple until you realize half the ingredients need converting from cups to tablespoons and the other half are in grams? That mental math is a perfect mirror for a deeper programming challenge: making your code reject bad data before it ruins the result.

In this project, you will build a recipe scaling engine from a completely blank Swift Playground. You will start the "Python way" (loose strings and arrays), watch it silently accept bugs, then rebuild it with Swift's type system so the compiler catches mistakes for you.

What You'll Build

By the end, you will type a number of servings, hit Play, and see your pancake recipe scale perfectly while the compiler blocks every invalid operation you throw at it.

By the end of this project, you'll have:

  • A recipe scaler that takes any recipe from 4 servings to 12 (or any number) and prints the correct ingredient quantities instantly.
  • A unit converter where you can convert cups to tablespoons or grams to ounces, and the compiler flat-out refuses incompatible conversions before your code runs.
  • A protocol-driven engine that works with any recipe source. You can filter, scale, and chain operations together using closures.
  • Secret Mission: Build a meal prep planner that takes a weekly meal plan and outputs a combined, de-duplicated shopping list you could actually take to the store.

Are there any prerequisites?

You should have basic Python experience and familiarity with Swift syntax (variables, functions, if/else, loops). You also need Xcode installed on your Mac.

Before We Start

Before We Start...

Before we dive in, let's lock in what you're about to build and why it matters.

You're going to build a recipe scaling engine from a completely blank file. The real goal is not the engine itself. It's experiencing why designing your types first makes the logic almost write itself.

Set Up Your Swift Playground

You need a place to write Swift and see results instantly. A Swift Playground gives you that fast feedback loop without the overhead of a full app project.

In this step, you'll create a blank Playground in Xcode. This is the canvas where you'll build your entire recipe engine from scratch.

In this step, get ready to:
  • Create a new macOS Blank Playground in Xcode.
  • Verify the playground runs successfully.
  • Clear the template code for a fresh starting point.
Create a new playground in Xcode
  • Press Cmd+Space to open Spotlight.
  • Type Xcode and press Enter to open it.
  • In the menu bar, click File.
  • Click New, then click Playground.
  • Select the macOS tab at the top of the template chooser.
  • Select Blank and click Next.
  • Name the file RecipeEngine and click Create.

Your new playground opens with some template code. You'll see something like import Cocoa and var greeting = "Hello, playground".

What is a Playground?

A Playground is a special file that runs Swift code immediately and shows results in a sidebar. It skips all the app boilerplate (no AppDelegate, no storyboards, no SwiftUI views) so you can focus purely on the language.

Verify the playground runs
  • Click at the end of the template code and press Enter to create a new line.
  • Type the following code on that new line:
print("Ready to think in Swift")

Before you run this, what do you expect to see? Will the output appear in the sidebar, in a console area, or somewhere else?

  • Click the blue Play button at the bottom left of the editor to run the playground.

The text Ready to think in Swift appears in the console output area at the bottom of the window.

Don't see any console output?

The console might be hidden. Press Cmd+Shift+Y to toggle the Debug Area where console output appears.

If the playground seems stuck, click the Stop button (square icon) and then Play again.

Why isn't my playground showing output?

Clear the template code for a fresh start

You have confirmed the playground works. Now you need a blank canvas. Everything you build in this project starts from an empty file with a single import.

  • Select all the code in the editor by pressing Cmd+A.
  • Delete it by pressing Delete.
  • Type the following line as your fresh starting point:
import Foundation

Why import Foundation instead of Cocoa?

The template imported Cocoa, which pulls in the entire macOS UI framework. You only need Foundation, which gives you basic Swift utilities (strings, arrays, dates) without the UI overhead.

Lighter imports mean faster playground execution.

You now have a completely empty file with one import. This is where you start building.

✔️ Awesome, I've got everything!

Great. Make sure you have saved your playground file.

ⓧ I'd like to double check the full code

Your RecipeEngine.playground should contain exactly this:

import Foundation

Your blank canvas is ready. Next up, you'll write recipe code the "Python way" using loose strings and arrays, and watch Swift let bugs slip through silently.

Build It the Python Way (And Watch It Break)

Your playground is ready and waiting. Now it is time to write your first recipe code.

You will start the way a Python developer would. Loose arrays of strings, a simple scaling function, and no structure telling Swift what your data means. This approach feels natural, but you are about to discover that Swift will not protect you from the bugs hiding inside it.

In this step, get ready to:
  • Write a recipe scaler using arrays of strings and doubles.
  • Discover bugs that compile and run without any warning.
Model a recipe with arrays and strings

In Python, you might represent a recipe as a dictionary with string keys and list values. Swift does not have dictionaries as its default data container, but you can mimic the same loose approach using parallel arrays.

  • In your empty playground, add the following code to create a pancake recipe as parallel arrays and a scaling function:
// MARK: - Step 2: The "Python Way" (fragile version)

let ingredientNames = ["Flour", "Milk", "Eggs", "Sugar"]
let quantities = [2.0, 1.5, 3.0, 0.5]
let units = ["cups", "cups", "whole", "cups"]

func scaleRecipe(quantities: [Double], factor: Double) -> [Double] {
    return quantities.map { $0 * factor }
}

let scaled = scaleRecipe(quantities: quantities, factor: 3.0)
print("Python way scaled: \(scaled)")

What does this code do?

  • ingredientNames, quantities, and units are parallel arrays. Index 0 in each array refers to the same ingredient (Flour, 2.0, cups).
  • scaleRecipe multiplies every quantity by the given factor using map. It has no idea what the numbers represent.
  • This mimics how you might use a Python dictionary with string keys. The data has no enforced shape.
  • Run the playground by clicking the blue Play button in the bottom-left corner of the editor.

You should see the output in the console area at the bottom:

Python way scaled: [6.0, 4.5, 9.0, 1.5]

The function worked. Every quantity tripled correctly.

Not seeing any output?

  • Make sure the console area is visible. Click View > Debug Area > Show Debug Area if you do not see it at the bottom.
  • Check that your code is below the import Foundation line. If you deleted it, add it back at the top.
  • help me debug my Xcode Playground not showing output
Introduce a bug Swift cannot catch

Your scaler works for valid input. But what about invalid input? In a type-safe language, you would expect the compiler to reject nonsense. Let's test that assumption.

  • Add these lines at the bottom of your playground, below the existing print statement:
// Bug: "cups" vs "cup" typo would go unnoticed
// Bug: negative factor accepted silently
let brokenScaled = scaleRecipe(quantities: quantities, factor: -1.0)
print("Negative scale (no protection): \(brokenScaled)")

Before you run this, what do you think will happen? Will Swift reject a negative scale factor, or will it happily produce negative ingredient quantities?

  • Click the Play button to run the playground again.

You will see this output with no errors or warnings:

Negative scale (no protection): [-2.0, -1.5, -3.0, -0.5]

Swift accepted -1.0 as a scale factor without complaint. Negative cups of flour. That is physical nonsense, but the compiler did not stop you.

Why did Swift let this through?

You gave Swift nothing to work with. Your data is raw strings and doubles. Swift cannot tell the difference between a valid scale factor and a nonsensical one, because you never told it what a valid scale factor looks like.

If you changed "cups" to "cup" in the units array, the code would still compile and run. The problem is not your logic. The problem is that your data has no shape. Swift cannot help you if you do not tell it what your data means.

How is this different from Python?

In Python, this same fragility exists, but nobody expects the interpreter to catch these errors. In Swift, you have a powerful type system designed to prevent exactly these bugs. But it only works if you give it something to enforce. Right now, you are writing Swift like Python and getting Python-level safety.

✔️ Awesome, I've got everything!

Great. Double check you have saved your playground file.

ⓧ I'd like to double check the full code

Here is the full contents of your playground so far:

import Foundation

// MARK: - Step 2: The "Python Way" (fragile version)

let ingredientNames = ["Flour", "Milk", "Eggs", "Sugar"]
let quantities = [2.0, 1.5, 3.0, 0.5]
let units = ["cups", "cups", "whole", "cups"]

func scaleRecipe(quantities: [Double], factor: Double) -> [Double] {
    return quantities.map { $0 * factor }
}

let scaled = scaleRecipe(quantities: quantities, factor: 3.0)
print("Python way scaled: \(scaled)")
// Bug: "cups" vs "cup" typo would go unnoticed
// Bug: negative factor accepted silently
let brokenScaled = scaleRecipe(quantities: quantities, factor: -1.0)
print("Negative scale (no protection): \(brokenScaled)")

You now have a working recipe scaler that produces correct numbers for valid input. But it silently accepts garbage. In the next step, you will rebuild this recipe using types that make invalid states impossible to represent. The compiler will become your safety net.

Model Your Domain with Types

In the previous step, you saw code that compiles and runs but silently accepts garbage input. The problem was not your logic. The problem was that your data had no shape.

This is the core mental model shift from Python to Swift. Instead of starting with logic and figuring out structure later, you start by describing what your data IS. Swift's enum and struct system lets you make invalid states impossible to represent. The compiler becomes your safety net.

In this step, get ready to:
  • Define a Unit enum that replaces string-based units with a closed set of valid options.
  • Define Ingredient and Recipe structs that give your data a clear, compiler-enforced shape.
  • Create a pancake recipe using your new types and watch the compiler catch a typo.
Define the Unit enum

In the Python-style version, units were plain strings. That meant "cups" and "cup" were two different values that Swift treated as equally valid. An enum defines a closed set of options. If a value is not in the set, the compiler rejects it.

  • Below the existing code in your playground, add the Unit enum by copying this code:
// MARK: - Step 3: Model Your Domain with Types

enum Unit: String, CaseIterable {
    // Volume
    case cups
    case tablespoons
    case teaspoons
    case milliliters

    // Weight
    case grams
    case ounces

    var isVolume: Bool {
        switch self {
        case .cups, .tablespoons, .teaspoons, .milliliters:
            return true
        case .grams, .ounces:
            return false
        }
    }
}

What does this code do?

  • enum Unit: String, CaseIterable declares a type with exactly six valid values. No typos possible.
  • The cases are split into volume (cups, tablespoons, teaspoons, milliliters) and weight (grams, ounces). This grouping matters for unit conversion later.
  • isVolume is a computed property that tells you which family a unit belongs to. You will use this to prevent nonsense conversions like cups to grams.
  • Click the Play button at the bottom left of the playground.

The playground runs without errors. In the results sidebar, you should see the Unit type acknowledged by the compiler.

Seeing a red error?

  • Check that you placed the enum code BELOW the existing Step 2 code. The playground runs top to bottom.
  • Make sure every opening brace { has a matching closing brace }. The enum has three levels of nesting.

Why isn't my Unit enum compiling in the playground?

Define the Ingredient struct

Each ingredient has a name, a quantity, and a unit. A struct bundles these together into one value. Structs in Swift are value types. They get copied when passed around, so you cannot accidentally mutate someone else's ingredient.

  • Below the Unit enum, add the Ingredient struct by copying this code:
struct Ingredient {
    let name: String
    var quantity: Double
    let unit: Unit
}

What does this code do?

  • let name: String and let unit: Unit are immutable. An ingredient's identity and measurement type do not change after creation.
  • var quantity: Double is mutable. When you scale a recipe, only the quantity changes.
  • The unit property uses your Unit enum. The compiler now guarantees every ingredient has a valid unit.

What is a value type?

Structures are always copied when they are passed around in your code. If you create an Ingredient and pass it to a function, the function gets its own independent copy. Changes to the copy do not affect the original.

This means when you scale a recipe later, you get a brand new Recipe with new quantities. The original stays untouched.

  • Click Play to confirm the playground still compiles without errors.
Create a recipe with your new types

Now you need a Recipe struct to hold a title, serving count, and a list of ingredients. Once the type exists, you will create the same pancake recipe from Step 2. This time, every field is type-checked.

  • Below the Ingredient struct, add the Recipe struct, create a pancake recipe, and print it by copying this code:
struct Recipe {
    let title: String
    let servings: Int
    let ingredients: [Ingredient]
}

let pancakeRecipe = Recipe(
    title: "Pancakes",
    servings: 4,
    ingredients: [
        Ingredient(name: "Flour", quantity: 2.0, unit: .cups),
        Ingredient(name: "Milk", quantity: 1.5, unit: .cups),
        Ingredient(name: "Eggs", quantity: 3.0, unit: .cups),
        Ingredient(name: "Sugar", quantity: 0.5, unit: .cups)
    ]
)

print("\nRecipe: \(pancakeRecipe.title) (\(pancakeRecipe.servings) servings)")
for ingredient in pancakeRecipe.ingredients {
    print("  \(ingredient.quantity) \(ingredient.unit.rawValue) \(ingredient.name)")
}

What does this code do?

  • Recipe bundles a title, serving count, and an array of Ingredient values together. Every recipe now has a guaranteed structure.
  • pancakeRecipe uses dot syntax (.cups) for units. Swift infers the enum type from context. No strings involved.
  • The for loop prints each ingredient using .rawValue to convert the enum case back to its string representation for display.
  • Click Play to run the playground.

In the console output area at the bottom, you should see your recipe printed with its title, serving count, and each ingredient on its own line.

Not seeing the recipe output?

  • Make sure the console area is visible at the bottom of the playground. If it is hidden, go to View > Debug Area > Activate Console.
  • Check that each Ingredient initializer has all three parameters: name, quantity, and unit. A missing parameter causes a compiler error.

Why isn't my Recipe printing in the playground console?

Now test what happens when you feed the compiler an invalid unit. Before you try it, what do you think Swift will do if you type .cusp instead of .cups?

  • In the pancakeRecipe initializer, temporarily change .cups on any ingredient to .cusp.

The compiler immediately shows a red error. It refuses to build your code because .cusp is not a valid case in your Unit enum. Compare this to the Python-style version above, which silently accepted any string. This is Swift protecting you.

  • Change .cusp back to .cups so your playground compiles again.

✔️ Awesome, I've got everything!

Great. Double check that you have changed .cusp back to .cups and that the playground runs without errors.

ⓧ I'd like to double check the full code

Scroll up through your playground and confirm each of these sections is present, in order:

  • The Step 2 "Python Way" code at the top (ingredientNames, quantities, units arrays and scaleRecipe function).
  • The // MARK: - Step 3 comment followed by the Unit enum with six cases and an isVolume computed property.
  • The Ingredient struct with name, quantity, and unit properties.
  • The Recipe struct, the pancakeRecipe constant, and the for loop that prints each ingredient.

You have not written a single line of logic yet, but your code is already safer than the Python-style version. The compiler now rejects typos, wrong types, and missing fields. Next up, you will add the actual scaling logic. And because your types are solid, the logic will be surprisingly simple.

Add Scaling Logic with Optionals and Guard

Your types are in place. The Unit enum, Ingredient struct, and Recipe struct define what your data looks like. Now it is time to add the logic that actually scales recipes.

Unlike the fragile version from Step 2, your function will refuse to produce garbage output. You will use optionals and guard to handle invalid input at the boundary, so the rest of your code never has to worry about bad data sneaking through.

In this step, get ready to:
  • Write a scaling method that rejects invalid input using guard and optionals.
  • Test scaling with valid and invalid input to see optionals in action.
  • Add a unit conversion method that prevents cross-family conversions at compile time.
Write the scaling method

In Python, a function that receives bad input might silently return wrong results. In Swift, you can make a function's return type communicate "this might fail" by returning an optional.

  • Add the following code below your existing pancakeRecipe print loop in RecipeEngine.playground:
// MARK: - Step 4: Scaling with Optionals and Guard

extension Recipe {
    func scaled(to newServings: Int) -> Recipe? {
        // Reject invalid input immediately
        guard newServings > 0 else { return nil }

        // Calculate the ratio between new and original servings
        let factor = Double(newServings) / Double(servings)
        let scaledIngredients = ingredients.map { ingredient in
            Ingredient(
                name: ingredient.name,
                quantity: ingredient.quantity * factor,
                unit: ingredient.unit
            )
        }

        return Recipe(
            title: title,
            servings: newServings,
            ingredients: scaledIngredients
        )
    }
}

What does this code do?

This extension adds a scaled(to:) method to your existing Recipe struct without modifying the original definition.

  • The return type Recipe? (with the question mark) means "this returns either a Recipe or nothing." That question mark is an optional.
  • guard newServings > 0 else { return nil } exits the function immediately if the input is invalid. The caller gets nil back, which means "this operation failed."
  • The .map closure creates new Ingredient values with scaled quantities. Because structs are value types, the original recipe stays unchanged.
Test scaling with valid and invalid input

Your method returns an optional. That means you cannot use the result directly. Swift forces you to handle the "it might be nil" case before accessing the value inside.

  • Add this code below the extension you just wrote to test both cases:
if let scaledRecipe = pancakeRecipe.scaled(to: 8) {
    print("\nScaled to \(scaledRecipe.servings) servings:")
    for ingredient in scaledRecipe.ingredients {
        print("  \(ingredient.quantity) \(ingredient.unit.rawValue) \(ingredient.name)")
    }
}

let invalidResult = pancakeRecipe.scaled(to: 0)
print("\nScaled to 0 servings: \(String(describing: invalidResult))")

What is if let doing here?

if let scaledRecipe = pancakeRecipe.scaled(to: 8) is called optional binding. If scaled(to:) returns a real Recipe, Swift unwraps it and assigns it to scaledRecipe. If it returns nil, the entire if block is skipped.

This is how Swift forces you to handle failure. You cannot accidentally use a nil value because the compiler will not let you.

Before you click Play, predict: what do you think the second print statement will output when you scale to 0 servings?

  • Click the Play button to run the playground.

You should see the scaled recipe printed with doubled quantities (from 4 servings to 8). The second line prints nil. No crash, no garbage. The function communicated "this operation failed" through its return type.

Not seeing any output?

  • Make sure your new code is below the closing brace of the extension Recipe { } block, not inside it.
  • Check for a red error icon in the gutter. If you see one, click it to read the compiler message.
  • Confirm the playground is set to run automatically or click the blue Play button in the bottom-left corner.

Still stuck? Why isn't my playground printing the scaled recipe?

Add unit conversion

Scaling quantities is only half the problem. Recipes also need unit conversion. But not all conversions make sense. You cannot convert cups to grams without knowing the ingredient's density. Your method will reject impossible conversions by returning nil.

  • Add this extension below the invalidResult print statement:
extension Ingredient {
    func convert(to targetUnit: Unit) -> Ingredient? {
        // Reject cross-family conversions (volume to weight)
        guard unit.isVolume == targetUnit.isVolume else { return nil }

        let conversionFactor: Double?
        switch (unit, targetUnit) {
        case (.cups, .tablespoons): conversionFactor = 16.0
        case (.cups, .teaspoons): conversionFactor = 48.0
        case (.cups, .milliliters): conversionFactor = 236.588
        case (.tablespoons, .cups): conversionFactor = 1.0 / 16.0
        case (.tablespoons, .teaspoons): conversionFactor = 3.0
        case (.tablespoons, .milliliters): conversionFactor = 14.787
        case (.teaspoons, .cups): conversionFactor = 1.0 / 48.0
        case (.teaspoons, .tablespoons): conversionFactor = 1.0 / 3.0
        case (.teaspoons, .milliliters): conversionFactor = 4.929
        case (.milliliters, .cups): conversionFactor = 1.0 / 236.588
        case (.milliliters, .tablespoons): conversionFactor = 1.0 / 14.787
        case (.milliliters, .teaspoons): conversionFactor = 1.0 / 4.929
        case (.grams, .ounces): conversionFactor = 0.03527
        case (.ounces, .grams): conversionFactor = 28.3495
        default: conversionFactor = unit == targetUnit ? 1.0 : nil
        }

        guard let factor = conversionFactor else { return nil }
        return Ingredient(name: name, quantity: quantity * factor, unit: targetUnit)
    }
}

What does this code do?

The first guard checks whether both units belong to the same family (both volume or both weight). Converting cups to grams is a cross-family conversion that requires density data, so it returns nil immediately.

The switch on a tuple (unit, targetUnit) matches every valid conversion pair and assigns the right factor. The default case handles same-unit conversions (factor 1.0) and any pair not explicitly listed (nil).

  • Add this test code below the extension to try both a valid and an invalid conversion:
let flour = Ingredient(name: "Flour", quantity: 2.0, unit: .cups)
if let flourInTbsp = flour.convert(to: .tablespoons) {
    print("\n\(flour.quantity) \(flour.unit.rawValue) = \(flourInTbsp.quantity) \(flourInTbsp.unit.rawValue)")
}

let invalidConversion = flour.convert(to: .grams)
print("Cups to grams (cross-family): \(String(describing: invalidConversion))")

Before you click Play, predict: will converting cups to grams succeed or return nil? Why?

  • Click the Play button to run the playground.

You should see 2.0 cups = 32.0 tablespoons for the valid conversion. The cross-family conversion prints nil. No crash, no garbage. The type system told you these operations might fail, and forced you to handle it.

Seeing a compiler error on the extension?

  • Make sure the extension Ingredient block is outside the extension Recipe block. Extensions cannot be nested inside each other.
  • Check that the switch cases use .cups, .tablespoons, etc. with the leading dot. Without the dot, Swift does not recognize them as enum cases.

Still stuck? Why is my convert method giving a compiler error in my Swift Playground?

✔️ Awesome, I've got everything!

Great. Double check you have saved your playground file.

ⓧ I'd like to double check the full code

Your playground should contain these sections in order. Scroll up through your code and confirm each one is present:

  • The Step 2 "Python Way" arrays, scaleRecipe function, and print statements.
  • The Step 3 Unit enum, Ingredient struct, Recipe struct, and pancakeRecipe constant with its print loop.
  • The MARK: - Step 4 comment, followed by extension Recipe with the scaled(to:) method.
  • The if let scaledRecipe unwrap block and the invalidResult print statement.
  • The extension Ingredient with the convert(to:) method containing the switch on (unit, targetUnit).
  • The flour test variable, the valid conversion print, and the invalidConversion print.

Your recipe engine now refuses to produce garbage. Invalid scaling returns nil. Cross-family conversions return nil. The type system communicates failure through the return type, and Swift forces every caller to handle it. Next up, you will make the engine flexible with protocols and closures so it can work with recipes from any source.

Define Behavior with Protocols and Closures

Your recipe engine can scale quantities and convert units safely. But right now it only works with one hardcoded pancake recipe. What happens when recipes come from different sources?

In this step, you will define what a recipe source can do using a protocol, then make the engine work with any source that conforms to it. Closures let you make the engine configurable without subclassing or inheritance.

In this step, get ready to:
  • Define a RecipeSource protocol and create two conforming types.
  • Write a function that accepts any recipe source and prints its menu.
  • Combine filtering, scaling, and closures into a composable pipeline.
Define the RecipeSource protocol and a hardcoded source

A protocol describes what something can do without specifying how it does it. Your engine needs exactly one capability from any recipe source: the ability to return all its recipes.

  • Below your existing code in RecipeEngine.playground, add the protocol definition and a struct that conforms to it:
// MARK: - Step 5: Protocols and Closures

protocol RecipeSource {
    func allRecipes() -> [Recipe]
}

struct HardcodedRecipes: RecipeSource {
    func allRecipes() -> [Recipe] {
        let soup = Recipe(
            title: "Tomato Soup",
            servings: 2,
            ingredients: [
                Ingredient(name: "Tomatoes", quantity: 4.0, unit: .cups),
                Ingredient(name: "Onion", quantity: 0.5, unit: .cups),
                Ingredient(name: "Garlic", quantity: 2.0, unit: .teaspoons),
                Ingredient(name: "Salt", quantity: 1.0, unit: .teaspoons)
            ]
        )
        return [pancakeRecipe, soup]
    }
}

What does this code do?

The RecipeSource protocol has a single requirement: any conforming type must provide an allRecipes() method that returns an array of Recipe.

The HardcodedRecipes struct conforms by returning your existing pancake recipe plus a new Tomato Soup recipe. The protocol does not care where the recipes come from. It only cares that the method exists.

Next, you need a second conforming type that filters recipes using a closure. This proves the protocol works with completely different implementations.

  • Add the FilteredRecipes struct directly below HardcodedRecipes:
struct FilteredRecipes: RecipeSource {
    let source: any RecipeSource
    let filter: (Recipe) -> Bool

    func allRecipes() -> [Recipe] {
        return source.allRecipes().filter(filter)
    }
}

What does this code do?

The source property uses any RecipeSource to accept any type that conforms to the protocol. The any keyword creates a boxed protocol type (an existential) that can hold different concrete types at runtime.

The filter property stores a closure. This closure takes a Recipe and returns Bool. The allRecipes() method applies that filter to whatever source it wraps.

Why closures instead of subclassing?

In Python you might create a base class and override a method. In Swift, you store a closure as a property. This means you can create different filtering behaviors without writing a new class each time. The behavior is data, not structure.

  • Click the Play button to confirm the playground compiles without errors.

You will not see new output yet because you have not called these types. The next chunk puts them to work.

Seeing a compiler error?

  • Make sure HardcodedRecipes and FilteredRecipes both include : RecipeSource after their name. Without conformance, the protocol requirement is not satisfied.
  • Check that allRecipes() returns [Recipe] in both structs. The return type must match the protocol requirement exactly.
  • help me fix the compiler error on my RecipeSource conformance
Write a function that accepts any recipe source

The power of protocols appears when you write functions that depend on the protocol instead of a concrete type. One function works with every source, past and future.

  • Add the printMenu function and the test calls below your FilteredRecipes struct:
func printMenu(from source: some RecipeSource) {
    print("\nMenu:")
    for recipe in source.allRecipes() {
        print("  - \(recipe.title) (\(recipe.servings) servings)")
    }
}

let allRecipes = HardcodedRecipes()
printMenu(from: allRecipes)

let bigRecipes = FilteredRecipes(
    source: allRecipes,
    filter: { $0.ingredients.count > 3 }
)
printMenu(from: bigRecipes)

What does this code do?

The some RecipeSource parameter type means this function accepts any concrete type that conforms to RecipeSource. The some keyword is shorter syntax for generics in Swift. It creates an unnamed generic type parameter with a protocol constraint.

You call printMenu twice: once with HardcodedRecipes and once with FilteredRecipes. The same function works with both because it depends on the protocol, not the concrete type.

  • Click the Play button to run the playground.
  • In the console output, you should see two menus printed. The first shows both Pancakes and Tomato Soup. The second (filtered to recipes with more than 3 ingredients) also shows both recipes, because each has exactly 4 ingredients.

Not seeing two menus?

  • Make sure you placed this code after the FilteredRecipes struct, not inside it. The let allRecipes and printMenu calls must be at the top level.
  • If you see Cannot find 'pancakeRecipe' in scope, confirm that your HardcodedRecipes.allRecipes() method references pancakeRecipe (the constant from Step 3 that should still exist higher up in your file).
  • help me debug why printMenu is not printing output
Combine scaling and filtering with closures

Now you will chain everything together. A closure filters recipes by ingredient. The scaling method you built in Step 4 handles the math. This is the composable design that protocols and closures unlock.

  • Add the combined operation below your printMenu(from: bigRecipes) line:
// Combined: scale all recipes to 2 servings, filter for those with flour
let flourRecipes = FilteredRecipes(
    source: allRecipes,
    filter: { recipe in
        recipe.ingredients.contains { $0.name == "Flour" }
    }
)

print("\nRecipes with flour, scaled to 2 servings:")
for recipe in flourRecipes.allRecipes() {
    if let scaledRecipe = recipe.scaled(to: 2) {
        print("  \(scaledRecipe.title):")
        for ingredient in scaledRecipe.ingredients {
            print("    \(ingredient.quantity) \(ingredient.unit.rawValue) \(ingredient.name)")
        }
    }
}

What does this code do?

The closure passed to FilteredRecipes checks whether a recipe's ingredients array contains any item named "Flour". Only matching recipes pass through.

The loop then scales each matching recipe to 2 servings using if let to safely unwrap the optional. No force unwraps, no runtime crashes. The protocol, the optional, and the closure all compose together cleanly.

Before you run this, what do you think the output will show? Which recipe contains flour, and what will its quantities look like scaled from 4 servings down to 2?

  • Click the Play button to run the full playground.

You should see the Pancakes recipe listed with halved quantities (1.0 cups Flour, 0.75 cups Milk, etc.). Tomato Soup does not appear because it has no flour. No force unwraps. No runtime errors.

Not seeing the scaled output?

  • Check that $0.name == "Flour" uses a capital F. String comparison is case-sensitive in Swift.
  • Verify that flourRecipes uses allRecipes as its source (the HardcodedRecipes instance), not bigRecipes.
  • help me debug why my combined scaling and filtering is not producing output

✔️ Awesome, I've got everything!

Great. Double check you have saved your playground file.

ⓧ I'd like to double check the full code

Your playground file is over 30 lines now. Scroll through and confirm these sections exist in order:

  • The // MARK: - Step 5: Protocols and Closures comment.
  • The RecipeSource protocol with one allRecipes() requirement.
  • The HardcodedRecipes struct returning pancakeRecipe and a Tomato Soup recipe.
  • The FilteredRecipes struct with source and filter properties.
  • The printMenu(from:) function using some RecipeSource.
  • The allRecipes and bigRecipes constants with two printMenu calls.
  • The flourRecipes constant and the scaling loop that prints halved Pancakes ingredients.

Your engine now works with any recipe source. Protocols define capabilities. Closures make behavior configurable. Next up, put all of these skills together in a bonus challenge.

Secret mission

Build a Meal Prep Planner

You have a working recipe engine that scales and converts. But what if you are planning meals for an entire week? Can you combine multiple recipes into a single, de-duplicated shopping list that handles unit mismatches gracefully?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally on your Mac, so there are no ongoing costs or cloud resources to worry about. Decide whether to keep your playground file as a reference or delete it entirely.

Resources you used:

  • RecipeEngine.playground file saved locally on your Mac.

Keep everything running

No action needed. Choose this if you want to keep your playground as a reference for Swift design patterns.

  • Your RecipeEngine.playground file stays wherever you saved it when you created it in Xcode.
  • You can reopen it anytime from Xcode via File > Open or by double-clicking the file in Finder.

Pause - I'll come back to this later

Since this is a local file with no running processes, there is nothing to pause. Your playground is already idle on disk.

  • Close the playground in Xcode to free up memory.
  • The file remains on your Mac until you decide to delete it.

Delete - I don't want to use this again

Remove the playground file from your Mac entirely.

  • Close RecipeEngine.playground in Xcode if it is still open.
  • Open Finder and navigate to the folder where you saved the playground.
  • Right-click RecipeEngine.playground and select Move to Trash.
  • Empty your Trash to permanently remove the file.

Nice Work!

Nice Work!

You just built a recipe scaling engine from a completely blank file, making every design decision yourself. That is real Swift thinking in action.

Here's what you learned:

  • You experienced why "strings as data" fails and rebuilt your domain using enums and structs that make invalid states impossible to represent at compile time.
  • You used optionals and guard to handle failure at the boundary, so your scaling and conversion logic never produces garbage output.
  • You defined behavior with protocols and closures to build a composable recipe engine that works with any source, not just one hardcoded list.
  • Secret Mission: You designed a MealPlan struct from scratch that combines multiple recipes into a de-duplicated shopping list, proving you can model new domains independently.

Ready to quiz yourself?