SwiftUI Fundamentals
View protocol, modifiers, layout containers, navigation, lists
- 01
What is SwiftUI and how does it differ from UIKit?
EasySwiftUI is Apple's declarative UI framework, introduced in iOS 13 (2019).
import SwiftUI struct GreetingView: View { let name: String var body: some View { VStack(spacing: 12) { … - 02
Explain stack containers in SwiftUI: VStack, HStack, ZStack, LazyVStack/HStack.
EasyStacks are how SwiftUI lays out children along an axis, and picking one is mostly a question of direction plus whether the content is long enough to need laziness.
// Vertical stack VStack(alignment: .leading, spacing: 8) { Text("Title").font(.headline) Text("Subtitle").foregroundStyle(.secondary) } … - 03
How do view modifiers work in SwiftUI?
MediumA modifier is a method on View that returns a new view wrapping the original with extra behavior (padding, background, foreground, gesture).
// Built-in modifiers Text("Hello") .font(.title) .foregroundStyle(.blue) .padding() .background(.yellow) … - 04
How does navigation work in SwiftUI? NavigationStack and NavigationSplitView.
MediumSwiftUI navigation is state-driven: you push by appending a value to a path, not by calling a method on a navigation controller.
// Typed route stack struct Route: Hashable { let userId: String } struct RootView: View { @State private var path: [Route] = [] var body: some View { … - 05
How do you build a List in SwiftUI, and why does row identity matter?
MediumA List gives you a scrolling, platform-styled container with separators, selection, swipe actions and pull-to-refresh for the cost of one view, and
ForEachfills it from a collection.struct Todo: Identifiable { let id: UUID; var title: String; var done: Bool } struct TodoListView: View { @State var todos: [Todo] = [] @State var search = "" var body: some View { … - 06
How do you write SwiftUI previews for a view with several states or a service dependency?
MediumPreviews render a view inside Xcode without launching the app, which turns the iteration loop from a build-and-navigate into a couple of seconds.
struct ProfileCard: View { let user: User var body: some View { VStack(alignment: .leading) { Text(user.name).font(.title) Text(user.bio).foregroundStyle(.secondary) … - 07
Walk through how SwiftUI decides a view's size — who has the final say, the parent or the child?
MediumSwiftUI layout is a three-step negotiation: the parent proposes a size, the child chooses its own size, and the parent then places the child inside itself.
struct Badge: View { var body: some View { Text("Sale") .padding(8) // proposes (offered size - 16) to the Text .background(.pink) // sizes itself around the padded text .frame(width: 200, height: 60) // a NEW parent 200x60; the badge centres inside it … - 08
What is
@ViewBuilderdoing behind the scenes, and why is returningAnyViewa bad habit?Medium@ViewBuilderis a result builder that folds a list of statements into one nested generic view type, which is howbodycan contain several views and anifwithout you ever writing that type out.struct Header: View { let isEditing: Bool var body: some View { // body is @ViewBuilder, so several statements are allowed HStack { Text("Inbox").font(.headline) … - 09
How do you present a sheet or an alert in SwiftUI, and when do you use the
item:form instead ofisPresented:?EasyPresentation is a modifier bound to state: you flip a
Boolor set an optional item, and the system presents or dismisses to match.struct InboxView: View { @State private var isShowingSettings = false @State private var editing: Message? // nil means no sheet @State private var pendingDelete: Message? @State private var isConfirmingDelete = false … - 10
What is the difference between
withAnimationand the.animationmodifier, and why was the single-argument.animation(_:)deprecated?MediumwithAnimationanimates every change that results from the state mutation inside its closure, while theanimation(_:value:)modifier animates one view's reaction to a single named value.struct Panel: View { @State private var isExpanded = false @State private var count = 0 var body: some View { VStack { … - 11
When do you use a List, and when a ScrollView with a LazyVStack?
EasyList gives you recycled rows, separators, swipe actions, selection and platform styling for free, while ScrollView plus LazyVStack gives you a blank canvas and full control of the row layout.
// List: recycled rows, separators, swipe actions, selection — the choice for long data List { ForEach(messages) { message in Row(message: message) .swipeActions { Button("Delete", role: .destructive) { delete(message) } } } … - 12
What do you actually change on a SwiftUI screen to make it work with VoiceOver and Dynamic Type?
MediumStandard SwiftUI controls are accessible by default, so the work is supplying what the framework cannot infer and making the layout survive very large text.
struct MessageRow: View { let message: Message @Environment(\.dynamicTypeSize) private var typeSize var body: some View { let layout = typeSize.isAccessibilitySize … - 13
Your floating Save button covers the last row of a list, and the keyboard shoves the whole screen up — which safe-area APIs fix each?
EasyThe button belongs in
safeAreaInsetso the scroll view insets its content behind it, and the layout that must not move needsignoresSafeArea(.keyboard).struct InboxView: View { @State private var draft = "" var body: some View { List(messages) { MessageRow($0) } // Right: the bar joins the safe area, so the last row scrolls clear of it … - 14
You wrapped a Text in
.frame(maxWidth: .infinity)and it stayed centred — how do the different alignments in SwiftUI actually differ?Easy.framecentres its child by default, so you have to passalignment: .leading, and even that only positions the whole text block — the wrapped lines inside it are governed separately bymultilineTextAlignment.// Wrong: the frame stretches, the text stays centred inside it Text("Title").frame(maxWidth: .infinity) // Right: tell the frame where to put the child Text("Title").frame(maxWidth: .infinity, alignment: .leading) … - 15
A bundled image blows out the layout at full size, and a remote one flickers back to its placeholder while you scroll — what is going on?
EasyImageignores the size proposed to it until you call.resizable(), andAsyncImagerestarts its request whenever the row is rebuilt because it keeps no decoded-image cache of its own.// Wrong: a bundled image ignores the proposed size until it is resizable Image("cover").frame(width: 120, height: 120) // still full size, just overflowing // Right: resizable, then a content mode, then clip what .fill spills Image("cover") .resizable() … - 16
A detail screen fires its request again every time you navigate back to it, and list rows re-fetch as you scroll — where does that come from?
MediumonAppearruns on every appearance, including a pop back onto the screen, and.taskis bound to that same appear/disappear pair — so work started there restarts whenever the view returns and is cancelled the moment it leaves.struct ItemDetail: View { let id: Item.ID @State private var model = DetailModel() var body: some View { Content(state: model.state) … - 17
You need a photo grid that shows two columns on iPhone and four on iPad, plus a comparison table whose columns line up — what does SwiftUI give you?
MediumLazyVGridwith an adaptiveGridItemhandles the flow-and-wrap case,Gridhandles the table case where cells in different rows must share column widths, andViewThatFitschooses between whole layouts.// Flow layout: as many columns as the width allows, built lazily ScrollView { LazyVGrid(columns: [GridItem(.adaptive(minimum: 120), spacing: 8)], spacing: 8) { ForEach(photos) { photo in Thumbnail(photo).aspectRatio(1, contentMode: .fill) } … - 18
A child needs to tell its parent how tall it is, and the parent cannot wrap everything in a GeometryReader — how does data travel up the tree?
MediumEnvironment values flow down and preferences flow up: a child writes a value with
.preference(key:value:), SwiftUI reduces every value in the subtree into one, and an ancestor reads the result with.onPreferenceChangeor.overlayPreferenceValue.// A key is a default plus a rule for merging siblings struct MaxHeightKey: PreferenceKey { static let defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = max(value, nextValue()) } … - 19
A list row has a star button and a delete button, and tapping either one runs both plus the row's navigation — what is happening?
MediumInside a
Listrow the default button style hands the whole row's tap region to every button in it, so each button needs an explicit.buttonStyle(.borderless)or.plainto own its own bounds.// Wrong: inside a List row, .automatic buttons inherit the row's tap region, // so a single tap runs both actions and the row's navigation. List(messages) { message in HStack { Text(message.title) Spacer() … - 20
After saving on the fourth pushed screen you must land back on the root, and the same flow also runs inside a sheet — how do you build that?
MediumPop-to-root is one data change — clearing the array that drives the
NavigationStack— so the deep screen needs access to that state, because@Environment(\.dismiss)only ever undoes one level.@Observable @MainActor final class Router { var path: [Route] = [] func popToRoot() { path.removeAll() } } …