Bobby Encoded
PostsAbout
PostsAbout

© 2026 Bobby Jose

← Back to Blog

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

AspectStruct (SwiftUI)Class (UIKit)
MemoryStack allocationHeap allocation
CopyingValue semanticsReference semantics
IdentityContent-basedReference-based
Thread safetyInherently safeRequires 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:

  1. Parent proposes size to child
  2. Child chooses its own size
  3. 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:

  1. Value semantics - Safe comparison for diffing algorithm
  2. Performance - Stack allocation faster than heap
  3. Thread safety - No shared mutable state
  4. 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 View is an opaque return type - tells compiler we return a specific View type without naming it
  • @ViewBuilder is 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:

  1. Loses type information for SwiftUI's diffing
  2. Can hurt performance - SwiftUI can't optimize updates
  3. 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

ConceptKey Point
Views as structsValue semantics, lightweight, thread-safe
some ViewOpaque return type for type inference
ModifiersOrder matters - each wraps previous view
LayoutParent proposes, child decides, parent positions
ForEachRequires stable identifiers for proper updates
ViewModifierReusable styling and behavior
EnvironmentSystem 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.

← Previous

SwiftUI State Management Deep Dive

Next →

React 18 & 19: Concurrent Features, Suspense, and Server Components