Mastering SwiftUI Foundations
May 5, 2025 · 8 min read
iOS, SwiftUI, Mobile Development, Swift, Interview Prep
Introduction
SwiftUI has fundamentally changed iOS development since its introduction in 2019. As of 2025, approximately 70% of new iOS projects start with SwiftUI, though UIKit remains essential for enterprise applications and complex custom interfaces. Understanding SwiftUI's declarative paradigm is now a baseline expectation in iOS interviews.
Building Glucoplate's iOS app gave me deep appreciation for SwiftUI's strengths and limitations. This post covers the foundational concepts that interviewers consistently ask about.
Views as Structs
SwiftUI views are structs conforming to the View protocol. This is a fundamental departure from UIKit's class-based approach.
struct NutritionCard: View {
let food: Food
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(food.name)
.font(.headline)
Text("\(food.calories) cal")
.font(.subheadline)
.foregroundColor(.secondary)
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
}
}
Why Structs Matter
| Aspect | Struct (SwiftUI) | Class (UIKit) |
|---|---|---|
| Memory | Stack allocation | Heap allocation |
| Copying | Value semantics | Reference semantics |
| Identity | Content-based | Reference-based |
| Thread safety | Inherently safe | Requires synchronization |
Interview Insight
Interviewers often ask why SwiftUI uses structs. The key points: value semantics enable safe comparison for diffing, stack allocation is faster than heap, and structs are thread-safe by default. SwiftUI recreates view structs frequently - this is cheap because they're lightweight descriptions, not actual UI objects.
The some View Opaque Return Type
The some View syntax is an opaque return type that tells the compiler "this returns some specific type conforming to View, but I won't specify which."
// This works - compiler infers the concrete type
var body: some View {
Text("Hello")
}
// This would fail - different types returned
var body: some View {
if condition {
Text("A") // Returns Text
} else {
Image("B") // Returns Image - different type!
}
}
// Solution: Type erasure or Group/AnyView
var body: some View {
Group {
if condition {
Text("A")
} else {
Image(systemName: "star")
}
}
}
ViewBuilder Under the Hood
@ViewBuilder enables the DSL syntax we use in SwiftUI:
@ViewBuilder
func nutritionSection(for category: NutritionCategory) -> some View {
switch category {
case .macros:
MacronutrientView()
case .vitamins:
VitaminView()
case .minerals:
MineralView()
}
}
Modifiers and the View Hierarchy
Modifiers in SwiftUI create new views wrapping the original. Order matters significantly.
// These produce different results
Text("Hello")
.padding()
.background(Color.blue) // Blue background includes padding
Text("Hello")
.background(Color.blue)
.padding() // Blue background only around text, padding outside
The Modifier Chain
// Each modifier wraps the previous view
Text("Calories: 250")
.font(.title) // ModifiedContent<Text, _FontModifier>
.foregroundColor(.primary) // ModifiedContent<ModifiedContent<...>, _ForegroundColorModifier>
.padding() // ModifiedContent<ModifiedContent<...>, _PaddingModifier>
Modifier Order Rule
Think of modifiers as layers. .background() colors everything that exists at that point. .padding() adds space. If you want padding inside a colored area, add padding first, then background.
Layout System: Stacks and Frames
SwiftUI's layout system uses a three-step process:
- Parent proposes size to child
- Child chooses its own size
- Parent positions child within its bounds
Stack Views
// Vertical arrangement
VStack(alignment: .leading, spacing: 12) {
Text("Breakfast")
Text("Lunch")
Text("Dinner")
}
// Horizontal arrangement
HStack(spacing: 8) {
Image(systemName: "flame.fill")
Text("320 cal")
Spacer()
Text("Logged")
}
// Layered (z-axis)
ZStack(alignment: .bottomTrailing) {
Image("food-photo")
Text("AI Analyzed")
.padding(4)
.background(Color.black.opacity(0.7))
}
Frame Modifiers
// Fixed size
Text("Protein")
.frame(width: 100, height: 44)
// Flexible sizing
Text("Description")
.frame(maxWidth: .infinity, alignment: .leading)
// Aspect ratio
Image("meal")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(height: 200)
.clipped()
ForEach and Identifiable
ForEach creates views from collections. Items must be Identifiable or you must provide an id parameter.
struct Food: Identifiable {
let id: UUID
let name: String
let calories: Int
}
struct FoodList: View {
let foods: [Food]
var body: some View {
List {
ForEach(foods) { food in
FoodRow(food: food)
}
.onDelete(perform: deleteFood)
}
}
func deleteFood(at offsets: IndexSet) {
// Handle deletion
}
}
// Without Identifiable - provide key path
ForEach(foods, id: \.name) { food in
Text(food.name)
}
Identity Matters
Using unstable identifiers (like array indices) causes SwiftUI to lose track of which view is which during updates. Always use stable, unique identifiers. I learned this the hard way when meal logging animations broke because foods were identified by array position.
Custom ViewModifiers
Create reusable styling with custom ViewModifier:
struct CardStyle: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(color: .black.opacity(0.1), radius: 4, x: 0, y: 2)
}
}
extension View {
func cardStyle() -> some View {
modifier(CardStyle())
}
}
// Usage
NutritionSummary()
.cardStyle()
Parameterized Modifiers
struct GlowModifier: ViewModifier {
let color: Color
let radius: CGFloat
func body(content: Content) -> some View {
content
.shadow(color: color.opacity(0.6), radius: radius)
.shadow(color: color.opacity(0.3), radius: radius * 2)
}
}
extension View {
func glow(_ color: Color, radius: CGFloat = 8) -> some View {
modifier(GlowModifier(color: color, radius: radius))
}
}
// Usage for achievement unlocked effect
AchievementBadge()
.glow(.yellow, radius: 12)
Environment and Preferences
Environment Values
SwiftUI provides system values through the environment:
struct AdaptiveText: View {
@Environment(\.colorScheme) var colorScheme
@Environment(\.sizeCategory) var sizeCategory
@Environment(\.horizontalSizeClass) var horizontalSizeClass
var body: some View {
Text("Adaptive Content")
.font(sizeCategory.isAccessibilityCategory ? .title : .body)
.foregroundColor(colorScheme == .dark ? .white : .black)
}
}
Custom Environment Values
private struct NutritionUnitKey: EnvironmentKey {
static let defaultValue: NutritionUnit = .metric
}
extension EnvironmentValues {
var nutritionUnit: NutritionUnit {
get { self[NutritionUnitKey.self] }
set { self[NutritionUnitKey.self] = newValue }
}
}
// Usage
ContentView()
.environment(\.nutritionUnit, userPreferences.unit)
Interview Questions
Q1: Why does SwiftUI use structs instead of classes for views?
Answer: SwiftUI views are structs for several reasons:
- Value semantics - Safe comparison for diffing algorithm
- Performance - Stack allocation faster than heap
- Thread safety - No shared mutable state
- Lightweight - Views are descriptions, not actual UI objects
SwiftUI recreates view structs frequently during updates. This is cheap because they're just data describing what the UI should look like.
Q2: Explain why modifier order matters with an example.
Answer:
// Background includes padding
Text("A").padding().background(.blue)
// Background only around text
Text("B").background(.blue).padding()
Each modifier wraps the previous view in a new view. Think of it as layers: .background() colors everything that exists at that point. Padding added after background creates space outside the colored area.
Q3: What's the difference between @ViewBuilder and some View?
Answer:
some Viewis an opaque return type - tells compiler we return a specific View type without naming it@ViewBuilderis a result builder that enables SwiftUI's DSL syntax - allows multiple statements, conditionals, and switches to build view hierarchies
@ViewBuilder produces a type that conforms to View, which we can return as some View.
Q4: When would you use AnyView and why is it discouraged?
Answer: AnyView is a type-erased wrapper when you need to return different view types. It's discouraged because:
- Loses type information for SwiftUI's diffing
- Can hurt performance - SwiftUI can't optimize updates
- Usually indicates a design issue
Better alternatives: @ViewBuilder, Group, or restructuring with conditional modifiers.
Common Mistakes
1. Unstable Identifiers
// ❌ Index changes when items reorder
ForEach(items.indices, id: \.self) { index in
ItemView(item: items[index])
}
// ✅ Stable identifier
ForEach(items) { item in
ItemView(item: item)
}
2. Heavy Computation in body
// ❌ Expensive operation on every render
var body: some View {
let processed = items.map { expensiveTransform($0) }
List(processed) { ... }
}
// ✅ Compute outside or use @State
3. Forgetting Accessibility
// ❌ Missing accessibility
Image("checkmark")
// ✅ Proper accessibility
Image("checkmark")
.accessibilityLabel("Completed")
Project Structure Best Practices
Features/
├── FoodLogging/
│ ├── Views/
│ │ ├── FoodLogView.swift
│ │ └── Components/
│ │ ├── FoodSearchBar.swift
│ │ └── NutritionCard.swift
│ ├── ViewModels/
│ │ └── FoodLogViewModel.swift
│ └── Models/
│ └── FoodEntry.swift
├── Dashboard/
└── Settings/
Shared/
├── Components/
├── Modifiers/
└── Extensions/
Lessons from Building Glucoplate
Organizing by feature rather than by type (all views together, all models together) scaled much better as the app grew. Each feature is self-contained with its views, view models, and models. Shared components live in a common folder. This structure made it easy to work on features independently.
Summary
| Concept | Key Point |
|---|---|
| Views as structs | Value semantics, lightweight, thread-safe |
some View | Opaque return type for type inference |
| Modifiers | Order matters - each wraps previous view |
| Layout | Parent proposes, child decides, parent positions |
| ForEach | Requires stable identifiers for proper updates |
| ViewModifier | Reusable styling and behavior |
| Environment | System and custom values flowing down hierarchy |
SwiftUI's declarative approach requires shifting mental models from imperative UIKit thinking. The framework handles the "how" - you focus on the "what." This foundation is essential for the more advanced topics we'll cover in the series.
Part 1 of the iOS Interview Prep series.