SwiftUI
Declarative UI, views, modifiers, layout, state, bindings, and animation.
PART 5 — SWIFTUI
1. View, Body, and Composition
Why Does This Exist?
SwiftUI was created to provide a declarative way to build user interfaces across all Apple platforms. Instead of manually updating the UI when state changes (like in UIKit), you declare what the UI should look like for a given state, and SwiftUI handles the rest.
Mental Model
Think of a View in SwiftUI not as a canvas you paint on, but as a recipe. The body property is the instructions for that recipe. When the ingredients (state) change, SwiftUI reads the recipe again to see what the new meal (UI) should look like. A View is just a lightweight value type (struct), making it extremely cheap to recreate.
Break It
struct BrokenView: View {
// Missing body property
}
This won't compile. Every View must conform to the View protocol, which requires a body property returning some View.
Debug It
If your view isn't updating, check if the state it relies on is actually marked with a property wrapper like @State or @Binding. If it's a regular property, SwiftUI won't know to observe it.
Mini Project
Create a simple profile card view combining an image, text for the name, and a text for the bio.
Real App Feature
Any screen in an app! For instance, the Settings screen in the Settings app is a composition of many smaller, reusable views.
Interview Questions
Why are Views in SwiftUI structs instead of classes?
Structs are value types and are allocated on the stack, making them extremely fast to create and destroy. Since SwiftUI constantly recreates views when state changes, using lightweight structs is crucial for performance. It also prevents shared mutable state bugs.
2. Modifiers and Layout
Why Does This Exist?
Modifiers allow you to alter the appearance or behavior of a view without changing the view itself. They chain together to build complex layouts in a readable, declarative syntax.
Mental Model
Imagine modifiers as nested boxes. When you apply a modifier to a view, you aren't changing the original view; you're wrapping it in a new view that applies the modification. Order matters! .padding().background(Color.red) is different from .background(Color.red).padding().
Break It
Text("Hello")
.background(Color.red)
.padding()
// The padding will be outside the red background!
Debug It
Use the View Debugger in Xcode, or temporarily add .border(Color.red) to see the exact bounds of a view when debugging layout issues.
Mini Project
Build a styled button with a corner radius, specific background color, shadow, and custom padding.
Real App Feature
Customizing the look of a promotional banner with gradients, rounded corners, and drop shadows.
Interview Questions
Why does the order of modifiers matter in SwiftUI?
Because each modifier wraps the previous view in a new view. Applying padding and then a background wraps the padded view in a background. Applying a background and then padding wraps the backgrounded view in padding.
3. Stacks, Grids, Lists, and Scrolling
Why Does This Exist?
These are the fundamental container views for organizing content on the screen. They allow for vertical, horizontal, overlapping, and scrolling layouts.
Mental Model
Stacks (VStack, HStack, ZStack) are like organizing boxes on a shelf. Lists and Grids are like structured spreadsheets for repeating data. ScrollView is a window that lets you peek into a content area larger than the screen.
Break It
VStack {
ForEach(0...10000) { _ in
Text("Too many views!")
}
}
This will cause performance issues because VStack loads all its children immediately. For large lists, use LazyVStack or List.
Debug It
If a ScrollView isn't scrolling, check if its content actually exceeds the size of the ScrollView itself. If not, it won't scroll.
Mini Project
Build a scrollable feed of user posts using a List or LazyVStack.
Real App Feature
The Instagram feed (ScrollView + LazyVStack) or the Photos app grid (LazyVGrid).
Interview Questions
What is the difference between VStack and LazyVStack?
VStack creates and renders all of its child views immediately, even if they are off-screen. LazyVStack only creates views as they are needed (when they are about to appear on screen), making it much more memory and performance efficient for large lists.
4. Forms, Navigation, Sheets, and Alerts
Why Does This Exist?
These components map directly to standard platform UI paradigms. Forms build data entry screens, Navigation manages hierarchical flows, and Sheets/Alerts handle modal presentations.
Mental Model
A Form is a specialized List styled for user input. NavigationStack is a stack of cards you push onto or pop off of. Sheets and Alerts are temporary overlays that demand immediate attention or provide secondary workflows.
Break It
NavigationLink("Go", destination: Text("Detail"))
// Used outside a NavigationStack
A NavigationLink won't work unless it's embedded somewhere within a NavigationStack.
Debug It
If a sheet isn't appearing, ensure the isPresented binding is actually being set to true. Also, ensure the sheet modifier is attached to a view that is currently on screen.
Mini Project
Build a Settings screen using a Form, with NavigationLinks to sub-settings and an Alert to confirm "Reset Settings".
Real App Feature
The iOS Settings app (Form + Navigation) or a "Share" modal (Sheet).
Interview Questions
How do you programmatically pop to the root view in a NavigationStack?
By passing a binding to the navigation path array to the NavigationStack, and clearing that array when you want to return to the root.
5. State, Bindings, Environment, and Observation
Why Does This Exist?
This is the lifeblood of SwiftUI. It's how data flows through your app and triggers UI updates. It replaces delegation and manual KVO.
Mental Model
@State is source of truth owned by a view. @Binding is a read/write reference to that truth for a child view. @Environment is a way to pass data down the tree globally without passing it through every intermediate view. @Observable (or ObservableObject) manages complex data models outside the view layer.
Break It
struct MyView: View {
var counter = 0 // Not @State
var body: some View {
Button("Tap") { counter += 1 } // Error: self is immutable
}
}
Debug It
If a view isn't updating when a model changes, ensure the model is correctly using the @Observable macro (or conforming to ObservableObject with @Published properties) and the view is referencing it correctly.
Mini Project
Build a counter app where a parent view holds the state, a child view displays it, and another child view has a binding to increment it.
Real App Feature
A shopping cart where adding an item (child view) updates the total badge in the navigation bar (environment/shared observable).
Interview Questions
What is the difference between @State and @Binding?
@State creates and owns the data; it is the source of truth. @Binding does not own the data; it is merely a two-way reference to data owned elsewhere (usually an @State in a parent view).
6. Lifecycle, Identity, Animations, and Transitions
Why Does This Exist?
To control when views appear and disappear, understand how SwiftUI differentiates views, and make those changes visually pleasing.
Mental Model
onAppear/onDisappear are the lifecycle hooks. View Identity (often defined by id()) tells SwiftUI if a view is the *same* view with new data, or a entirely *new* view. Animations interpolate changes in view properties, while Transitions define how views enter or leave the hierarchy.
Break It
ForEach(items, id: \.self) { item in ... }
// If items have duplicate values, SwiftUI gets confused about view identity and may crash or render incorrectly.
Debug It
If an animation isn't triggering, check if the view is being completely replaced (identity change) rather than its properties being updated. Use .id() to force or preserve identity.
Mini Project
Create a view that conditionally shows a box. Add a transition so it slides in when appearing and fades out when disappearing.
Real App Feature
The fluid animations in the Apple Music app when expanding the Now Playing sheet.
Interview Questions
How does SwiftUI determine if a View is the same as before?
SwiftUI uses Structural Identity (based on the view's type and position in the view hierarchy) and Explicit Identity (using the .id() modifier or the id parameter in a ForEach). If either changes, SwiftUI destroys the old view and creates a new one.
7. Gestures, Accessibility, Adaptive Layouts, Dark Mode, and Dynamic Type
Why Does This Exist?
To make apps usable by everyone, everywhere, under any conditions. This encompasses interactivity, inclusivity, and platform adaptation.
Mental Model
Gestures translate physical touches into intents. Accessibility ensures the UI tree makes sense to screen readers (VoiceOver). Adaptive layouts use Environment values (like Size Classes) to reshape UI. Dark Mode and Dynamic Type rely on relative colors and scalable fonts to match user preferences.
Break It
Text("Label")
.font(.system(size: 14)) // Breaks Dynamic Type by hardcoding size
Debug It
Use Xcode's Environment Overrides toggle during debugging to instantly test Dark Mode, Dynamic Type sizes, and localization without restarting the app.
Mini Project
Goal (15-20 min): Build a reusable custom interactive button using SwiftUI that tracks its own tap count using @State. It should have a scaled text label for Dynamic Type support, adapt to Dark Mode using system semantic colors, and use @Binding to pass the count to a parent view.
Sample Solution
struct CustomCounterButton: View {
@Binding var totalCount: Int
@State private var isPressed = false
var body: some View {
Button(action: {
totalCount += 1
let impact = UIImpactFeedbackGenerator(style: .medium)
impact.impactOccurred()
}) {
VStack(spacing: 8) {
Text("Tapped \(totalCount) times")
.font(.headline)
Text("Tap me!")
.font(.subheadline)
}
.padding()
.foregroundColor(.white)
.background(Color.accentColor)
.cornerRadius(10)
.scaleEffect(isPressed ? 0.95 : 1.0)
.animation(.easeInOut(duration: 0.1), value: isPressed)
}
.accessibilityLabel("Counter Button")
.accessibilityValue("\(totalCount) taps")
}
}
Real Application Feature
Build a production-ready Settings screen commonly found in applications like Mail or Messages. The implementation should utilize a NavigationSplitView to adapt gracefully across iPhone and iPad sizes. Implement a Form with logically grouped Sections for preferences, use @AppStorage to persist user settings, and demonstrate proper state ownership by breaking massive views down into manageable subviews.
Interview Questions
Easy: What is the difference between @State and @Binding in SwiftUI?
@State is used to declare state that is owned and managed by the view itself. @Binding is used to create a two-way connection to state that is owned by a parent view, allowing the child view to read and mutate it without owning it.
Medium: How do you ensure a SwiftUI view supports Dynamic Type?
Use relative text styles like .font(.headline) or .font(.body) instead of hardcoding point sizes. If custom sizes are absolutely required, wrap the size value using the @ScaledMetric property wrapper so it scales proportionally with the user's OS text size settings.
Hard: Why does SwiftUI use structs for Views instead of classes like UIKit?
Structs are value types, which are extremely fast to allocate and destroy compared to heap-allocated objects. Because SwiftUI's declarative model frequently recreates the view tree during state changes, using lightweight structs avoids the overhead of reference counting and prevents state synchronization issues typical of mutable shared objects. The struct is merely a lightweight description of the UI, while SwiftUI manages the persistent rendering tree underneath.
Senior: Explain view identity in SwiftUI and its impact on state preservation and animations.
SwiftUI tracks views through either structural identity (their position in the view hierarchy code) or explicit identity (using the .id() modifier). If a view's identity remains constant across an update, SwiftUI will merely update its properties, animate any changes, and preserve its internal @State. If the view's identity changes, SwiftUI treats it as a completely new view, destroying the old one (losing its @State) and abruptly drawing the new one without an animation transition.
Mini Project (20-30 min)
Create a custom ViewModifier in SwiftUI to standardize a stylistic card appearance and apply it seamlessly.
View Solution
// Reusable Custom Modifier
struct CardModifier: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(Color.white)
.cornerRadius(10)
.shadow(radius: 5)
}
}
extension View {
func cardStyle() -> some View {
self.modifier(CardModifier())
}
}
Bigger Project (1-2 hours)
Implement an interactive, state-driven list using SwiftUI that supports deletions and integrates navigation.
View Solution
// Interactive State-driven List
struct ContentView: View {
@State private var items = ["Item 1", "Item 2", "Item 3"]
var body: some View {
NavigationView {
List {
ForEach(items, id: \.self) { item in
Text(item)
}
.onDelete { indexSet in
items.remove(atOffsets: indexSet)
}
}
.navigationTitle("My List")
.toolbar { EditButton() }
}
}
}