Topics Covered in This iOS Development Tutorial:
Creating & Using Enumerations, Adding Functions, Instantiating a Class
Exercise Overview
In this exercise, we'll explore advanced enumeration concepts by demonstrating how they integrate seamlessly with classes and methods to enforce type safety. You'll master complex property patterns including computed properties, dive deep into memory management with weak references, and understand how Swift's automatic reference counting prevents memory leaks in real-world applications.
Exercise Workflow
Environment Setup
Launch Xcode and create a new Playground file for hands-on coding practice
Enum Creation
Build a MaritalStatus enumeration within a Person class structure
Property Implementation
Add computed properties, weak references, and property observers
Function Development
Create marry and divorce functions that manipulate enum states
Testing & Validation
Test the implementation with multiple Person instances and verify functionality
Getting Started
Launch Xcode if it isn't already open.
Navigate to File > New > Playground.
Under iOS, double–click on Blank.
Navigate into Desktop > Class Files > yourname-iOS App Dev 1 Class.
Save the file as: Enums.playground
Click Create.
Using Xcode Playground allows for immediate code execution and testing. You can see results in real-time as you build your enumeration and function implementations.
Setup Requirements
Ensure you have the latest version for iOS development features
Provides clean environment for experimenting with Swift code
Maintains organized project structure for course materials
Clear naming convention helps identify exercise content
Creating & Using Enumerations
Enumerations are fundamental to writing maintainable Swift code. By encapsulating related values within a single type, they eliminate the possibility of invalid states and make your code self-documenting. Let's build a practical example that showcases their power.
In your new Playground file, delete all the existing code.
Create a new class that contains an enum called MaritalStatus, as shown below in bold:
class Person { enum MaritalStatus { } }Add the following cases to the MaritalStatus enum:
enum MaritalStatus { case single case married case divorced case complicated }By nesting this MaritalStatus enum inside the Person class, we create a logical namespace. This pattern is particularly valuable in larger codebases where you need to avoid naming conflicts while maintaining clear semantic relationships.
Now we'll add properties to the Person class. Below the enumeration, add these familiar variables:
enum MaritalStatus { case single case married case divorced case complicated } var name: String var age: IntDon't worry about the red error for now—we'll initialize these properties shortly to resolve it.
Add the maritalStatus property shown in bold below:
var name: String var age: Int var maritalStatus: MaritalStatusBy defining this property with our custom MaritalStatus type, we guarantee compile-time safety. The property can only ever hold one of our four defined cases, making invalid states impossible.
Next, we'll create an optional partner variable with sophisticated logic that adapts based on relationship status:
var name: String var age: Int var maritalStatus: MaritalStatus var partner: String? { return }This is a computed property, indicated by the return statement within curly braces. Unlike stored properties, computed properties derive their values from other properties, making them ideal for presenting data in different formats without duplication.
This partner computed property will intelligently derive information from a spouse property we'll create next. Add the bold code as shown below:
var name: String var age: Int var maritalStatus: MaritalStatus var partner: String? { return spouse?.name ?? "No one" }We're using Swift's nil-coalescing operator (??), which provides elegant nil-handling. The expression spouse?.name attempts to access the name property, while the operator provides "No one" as a fallback if spouse is nil. This pattern is essential for building robust iOS applications that handle optional data gracefully.
The question mark in spouse?.name demonstrates optional chaining—a safer alternative to force unwrapping that prevents runtime crashes when dealing with potentially nil values.
Now we'll add the stored spouse variable, which references another Person instance:
var partner: String? { return spouse?.name ?? "No one"} var spouse: Person? { }This Person? optional creates a relationship between two person instances. This self-referential pattern is common in modeling real-world relationships but requires careful memory management, which we'll address next.
Make this property private to encapsulate internal implementation details:
var partner: String? { return spouse?.name ?? "No one"} private var spouse: Person? { }Setting spouse as private follows the principle of encapsulation. External code can read partner information through the computed property, but cannot directly manipulate the internal spouse reference, preventing inconsistent states.
Add the critical weak keyword to prevent memory leaks:
var partner: String? { return spouse?.name ?? "No one"} private weak var spouse: Person? { }The weak keyword is essential here because we're creating a potential retain cycle. When two Person instances reference each other through spouse properties, strong references would create a deadlock where neither can be deallocated. Weak references allow one side of the relationship to be deallocated, automatically setting the weak reference to nil and preventing memory leaks—a critical consideration in iOS development where memory management directly impacts app performance.
Add a didSet property observer to track relationship changes:
private weak var spouse: Person? { didSet { } }Property observers are powerful tools for maintaining data consistency and triggering side effects. The didSet observer executes whenever the property value changes, making it perfect for logging, updating UI, or maintaining related state.
Implement the logic that executes when someone enters a relationship:
private weak var spouse: Person? { didSet { if spouse != nil { print("\(name) is now with \(spouse!.name)") } } }When spouse is assigned a value, this condition triggers, providing immediate feedback about relationship changes. In production apps, this is where you might update a database, refresh the UI, or trigger notifications.
Handle the case when someone becomes single:
private weak var spouse: Person? { didSet { if spouse != nil { print("\(name) is now with \(spouse!.name)") } else { print("\(name) is now sadly alone") } } }Now we'll initialize our properties with a comprehensive initializer:
private weak var spouse: Person? {Code Omitted To Save Space
} init(name: String, age: Int, maritalStatus: MaritalStatus = .single) { self.name = name self.age = age self.maritalStatus = maritalStatus }We're providing a default value of .single for maritalStatus, which reflects the most common initial state. This demonstrates how enums with default values can simplify object creation while maintaining type safety.
Note that we don't initialize the partner property because computed properties derive their values at runtime rather than storing data directly.
MaritalStatus Enum Cases
The partner property is computed and draws information from the spouse property without storing data itself. This pattern allows for dynamic value calculation based on other property states.
Property Types in Person Class
Stored Properties
Name, age, and maritalStatus variables that hold actual data values. These properties require initialization and consume memory storage.
Computed Properties
Partner property that calculates its value from spouse property. Uses nil-coalescing operator to handle optional values gracefully.
Private Properties
Spouse property marked as private and weak to prevent memory leaks. Only accessible within the Person class for internal operations.
Adding Functions
With our data model established, let's add behavior that demonstrates how methods can work with enums to maintain consistent object states.
Add a marry function that handles relationship logic:
self.maritalStatus = maritalStatus } func marry(personToMarry: Person) { }Implement validation logic to prevent invalid marriages:
func marry(personToMarry: Person) { if spouse != nil || personToMarry.spouse != nil { print("Oh, I wish… but no, \(name) is already married. Please divorce first ;)") return } }This guard condition demonstrates defensive programming—we validate preconditions before executing business logic. The return statement provides an early exit, eliminating the need for complex nested else statements and keeping the happy path code clean.
Implement the marriage logic when validation passes:
return } spouse = personToMarry; maritalStatus = .married personToMarry.spouse = self; personToMarry.maritalStatus = .married }This code updates both Person instances atomically, ensuring relationship consistency. The self keyword explicitly refers to the current instance, while the semicolon keeps related statements visually grouped. Notice how we use enum dot syntax (**.married**) for clean, readable code.
Add a companion divorce function for relationship dissolution:
func marry(personToMarry: Person) {Code Omitted To Save Space
} func divorce() { }Implement the divorce logic with proper cleanup:
func divorce() { spouse?.spouse = nil; spouse?.maritalStatus = .divorced spouse = nil; maritalStatus = .divorced }This function uses optional chaining to safely update the former spouse's state, then clears the local spouse reference. The careful ordering ensures both sides of the relationship are properly updated, demonstrating how weak references and proper state management work together.
Strong vs Weak References
| Feature | Strong Reference | Weak Reference |
|---|---|---|
| Memory Retention | Prevents deallocation | Allows deallocation |
| Reference Cycles | Can create deadlocks | Prevents cycles |
| Default Behavior | Swift default | Explicit keyword needed |
| Use Case | Primary ownership | Secondary references |
The didSet property observer monitors spouse property changes and automatically prints relationship status updates. This provides immediate feedback when relationships are established or broken.
Testing the Implementation
Now let's validate our implementation with realistic test scenarios that demonstrate the robustness of our enum-based design.
Create test instances below the Person class:
class Person {Code Omitted To Save Space
} let june = Person(name: "June", age: 23) let march = Person(name: "March", age: 18) let jane = Person(name: "Jane", age: 22, maritalStatus: .divorced) let jim = Person(name: "Jim", age: 17)Notice how our default parameter allows most instances to omit maritalStatus, while Jane explicitly starts as divorced. This flexibility is a key benefit of well-designed initializers.
Test the marriage functionality:
let jim = Person(name: "Jim", age: 17) jane.marry(personToMarry: jim)View the debug output by clicking the top right middle button
to show the Debug area.Observe the output showing both Jane is now with Jim and Jim is now with Jane, demonstrating how our property observers track both sides of the relationship.
Let's improve the API by removing verbose parameter labels. Locate the marry function definition:
func marry(personToMarry: Person) {Add an underscore to eliminate the external parameter label:
func marry(_ personToMarry: Person) {The underscore tells Swift we don't want an explicit argument label, creating cleaner call sites—a common pattern in iOS development where method names are already descriptive.
Update the method call to use the cleaner syntax:
let jim = Person(name: "Jim", age: 17) jane.marry(jim)Much more readable! This demonstrates how thoughtful API design improves code clarity.
Test the computed property functionality:
jane.marry(jim) jane.partnerThe right sidebar should display "Jim", demonstrating how computed properties provide clean access to derived data.
Verify the enum state management:
jane.marry(jim) jane.partner jane.maritalStatusThe sidebar should show that Jane is now married, proving our enum-based state management is working correctly.
Check initial states before marriage:
let jim = Person(name: "Jim", age: 17) jane.maritalStatus jim.maritalStatus jane.marry(jim) jane.partner jane.maritalStatusYou should see divorced for Jane and single for Jim initially, then married for both after the ceremony.
Verify Jim's updated status:
jane.marry(jim) jane.partner jane.maritalStatus jim.maritalStatusJim should now show as married, confirming our bidirectional relationship logic.
Test Jim's partner property as well:
jane.marry(jim) jane.partner jane.maritalStatus jim.partner jim.maritalStatusThe sidebar should display "Jane", demonstrating perfect symmetry in our relationship model.
Test the divorce functionality:
jim.maritalStatus jane.divorce()Observe both Jim is now sadly alone and Jane is now sadly alone in the Debug area, showing how property observers track relationship dissolution.
Verify the final states after divorce:
jane.divorce() jane.partner jane.maritalStatusThe sidebar should show "No one" and divorced, demonstrating how our computed properties and enums work together to maintain consistent state.
This exercise showcases the power of combining enumerations with classes to create robust, type-safe data models. Enumerations are ubiquitous in iOS development—from representing UI states to handling network responses—making these patterns essential for professional iOS development in 2026.
Save and close the file. Keep Xcode open for the next exercise.