You're building a social media app. You need to model a UserProfile.
Requirements:
username: A StringfollowerCount: An Inttier: The user's subscription levelThe tiers have different data requirements:
String?) they used to sign upDate)The challenge: Your model should make it impossible to represent an invalid state. A free user cannot have an expiration date. A pro user must have one.
Model this UserProfile and its tier.
Use a Swift enum for the tier—we expect that from your Java background.
Write the code. Think about how you'll prevent invalid states.
Note: Swift properties are public by default. You do not need to write explicit setters/getters like in Java.
Begin.
Most of you likely wrote something like this:
enum UserTier {
case free, premium, pro
}
struct UserProfile {
let username: String
let tier: UserTier
var proExpirationDate: Date?
var premiumPromoCode: String?
}
This works, but it has a critical flaw: it allows invalid states.
Nothing prevents this: UserProfile(username: "test", tier: .free, proExpirationDate: Date())
Your model can lie. Optional properties that should only exist for certain cases create a validation burden throughout your entire codebase.
Swift's enums can carry data specific to each case. This allows you to model the relationship between state and its data directly.
enum UserTier {
case free
case premium(promoCode: String?)
case pro(expiresOn: Date)
}
struct UserProfile {
let username: String
let tier: UserTier
}
Now it's impossible to create a user with mismatched data. The compiler enforces your business rules.
This is what we mean by "type safety"—using the type system to prevent entire categories of bugs.
switchSwift's switch is exhaustive—you must handle every case. Combined with pattern matching, you can safely extract associated values.
func describeTier(for profile: UserProfile) {
switch profile.tier {
case .free:
print("\(profile.username) is a Free user")
case .premium(let promoCode):
let code = promoCode ?? "none"
print("\(profile.username) is Premium (code: \(code))")
case .pro(let expirationDate):
print("\(profile.username) is Pro, expires \(expirationDate)")
}
}
If you add a new tier, this code won't compile until you handle it. The compiler becomes your safety net.
Associated values are just one feature. Enums can also have raw values for simple cases and contain their own logic.
enum UploadError: String, Error {
case tooLarge = "Upload exceeds file size limit."
case unsupportedFormat = "File format is not supported."
case networkFailed = "The network connection was lost."
}
// You can create an enum from its raw value
let error = UploadError(rawValue: "Upload exceeds file size limit.") // .tooLarge
// And because it conforms to `Error`, you can `throw` it.
An enum is not just a dumb data type. It can have functions and computed properties. This is often a better place for logic than a global function.
Add a computed property to the UserTier enum called isActivePro.
It should return true only if the tier is .pro AND its expiration date is in the future. Otherwise, it should return false.
// Add this inside your UserTier enum
var isActivePro: Bool {
// Your implementation here
}
// You can check for parameters within an if with
case .someCase(let someParameter) = Seft // 'Self' needs to be used it's accessing it's own information within a computed property
guard vs. ifHere are two ways to solve that task.
The if approach (works, but is nested):
var isActivePro: Bool {
if case .pro(let expirationDate) = self {
if expirationDate > Date() {
return true
}
}
return false
}
The guard approach (cleaner, flatter):
var isActivePro: Bool {
guard case .pro(let expirationDate) = self, expirationDate > Date() else {
return false
}
return true
}
guard is perfect for checking multiple conditions and exiting early if any of them fail. This makes the "happy path" (the successful return) clear and un-nested.
Swift's function syntax is designed to make call sites read like natural English sentences. This is a major philosophical difference from Java/C.
Look at the call sites, not the function definitions:
// Confusing: What is "Mike"? A name? An ID?
printUserDetails("Mike")
// Clear: The argument label explains the value.
printUserDetails(for: "Mike")
The syntax that enables this:
// func <name>(<external name> <internal name>: <Type>)
func printUserDetails(for username: String) {
// inside the function, you use 'username'
print("User: \(username)")
}
// Use _ to have no external name
func printUserDetails(_ username: String) {
print("User: \(username)")
}
The goal is always readability at the point of use.
// This reads like a sentence: "Get item for user 'Michael-525' from 'server' with retries 3"
getItem(for: "Michael-525", from: "server", withRetries: 3)
Instead of a summary, let's test your understanding.
switch statement to be exhaustive, and why is that a good thing?guard is a better choice than a simple if statement.Open a new SwiftUI project. Look at ContentView.swift.
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()
}
}
This code applies the Swift concepts we just covered. Your task is to understand why it's written this way.
This isn't magic. It's the deliberate application of Swift's type system.
You'll be assigned a specific part of this code. Your job is to research it and explain the language concepts behind it.
Timeline:
Refer to the next slides for your group's specific task.
Don't just read articles about SwiftUI. Run experiments. Change code. See what breaks. That's how you learn.
You are in charge of knowlege Transfer to the others. Do your best. You can do this!
Structure your live demonstration:
struct ContentView: ViewThe Experiment: Change struct to class. What happens? Document the compiler errors exactly.
The Type Question: Why does SwiftUI use struct instead of class?
💡 Hint: Create a simple struct vs. class outside SwiftUI. What happens when you assign one to another variable and modify it? How does copying behavior differ?
The Architecture Question: SwiftUI views are lightweight, disposable descriptions of UI. How does using struct support this design? What would happen if views were reference types that get shared?
The Identity Problem: Value types don't have identity - two structs with the same data are considered equal. Reference types have identity - two class instances are different even with the same data. Why is "no identity" actually good for SwiftUI views?
Your Conclusion: Provide a technical explanation for why SwiftUI requires views to be value types.
Optional Performance Question: SwiftUI creates and destroys views constantly. Research: Are structs stored on the stack or heap? Classes? Why does this matter for performance?
var body: some ViewComputed Property: body is a computed property. It calculates its value when asked; it doesn't store one. Why is this essential for a system where views are constantly being re-created?
The Protocol Requirement: View is a protocol. What is the only requirement of this protocol?
💡 Hint: Use Xcode's "Jump to Definition" (Cmd+Click on View).
The some Keyword: This declares an "opaque type". Research what this means.
💡 Hint: Try replacing some View with the explicit type Xcode suggests (Option+Click on body). Look at how complex it gets: VStack<TupleView<(ModifiedContent<Image, _ImageModifierTransform>, Text)>>. Why is forcing you to write this problematic?
Your Conclusion: Explain why body is a computed property returning an opaque type some View. What problems does this solve?
VStack { ... }What is VStack? Use Xcode to determine its type (struct, class, etc.). Jump to its definition.
The Trailing Closure: The {...} syntax is a trailing closure. Why can you write VStack { } instead of VStack(content: { })?
💡 Hint: This is a Swift language feature, not SwiftUI-specific. Look up "trailing closure syntax".
Multiple Children: How can VStack accept multiple views (Image AND Text) when it's just a closure that should return one thing?
💡 Experiment: Jump to VStack's definition. Look at the initializer's closure parameter. What attribute do you see marked on it? Research @ViewBuilder.
Layout Behavior: Try different numbers of views. Try HStack instead of VStack. What does VStack actually do with its children?
Your Conclusion: Explain that VStack is a container view that uses Swift's result builder feature (via @ViewBuilder) to let you write multiple views in a clean, readable way, then arranges them vertically.
.imageScale(.large), .foregroundStyle(.tint), .padding()What are modifiers? Pick one modifier (like .padding()). Option+Click on it in Xcode. What type does it return?
💡 Hint: Notice it doesn't return VStack or Image - it returns something like ModifiedContent<...>.
The Wrapping Pattern: Modifiers don't change the original view. They wrap it in a new view. Why is this the case?
💡 Experiment: Create a Text view, store it in a variable, apply .padding(), and store that in another variable. Print their types using type(of:). Are they the same type? What does this tell you?
Note: You can write 'normal' code in every swiftUI class.
print()will work as well.type(of:)see: https://developer.apple.com/documentation/swift/type(of:)
Method Chaining: You can write .padding().background(.blue).border(.red). Based on #1 and #2, explain why this works. What does each modifier return that allows the next modifier to be called?
💡 Hint: Each modifier returns something that also conforms to View.
Your Conclusion: Explain that views are immutable data structures, and modifiers create new views by wrapping old ones. Why is this important for a system where views are constantly recreated? How does this relate to struct being a value type?
Compare Image(systemName:) and Text() initializers. They're both structs that conform to View. What makes them "primitive" views versus container views like VStack?