-
Notifications
You must be signed in to change notification settings - Fork 0
Book 02 Introducing SwiftUI Views
Part I — Introduction · Claude's Xcode 26 Swift Bible
← Book-01-Introducing-Swift-And-Xcode · Chapters and Appendices · Book-03-Introducing-Scenes-And-Windows →
Claude's Xcode 26 Swift Bible -- Part I: Introduction
Every shipping app alongside this book is built with SwiftUI, but Inkwell is the most useful one to point at while reading the introductory chapter. Inkwell is the iPad reader app that renders these very pages — every paragraph, every code block, every Live Reference callout you're reading right now is drawn by a SwiftUI
Viewsomewhere in the Inkwell source. Tap the Under the Hood tab in this app's toolbar; the file list you see is every SwiftUI view that makes the app.ContentView.swiftis the root view.VaultWebView.swiftis the WKWebView wrapper that paints the HTML you're looking at. Source: github.com/fluhartyml/Claudes-X26-Swift6-Bible. See Build-Along 00 (titled "Meta") for the full architecture and the recursive-self-reference framing.
Open any iOS / iPadOS / macOS / tvOS app built against the X26 SDK and you'll see it: a new dynamic material running through the controls and navigation. Apple calls it Liquid Glass, and Apple's one-line definition covers the feature:
"Liquid Glass forms a distinct functional layer for controls and navigation elements. It affects how the interface looks, feels, and moves, adapting in response to a variety of factors to help bring focus to the underlying content."[[lg1]]
Liquid Glass is a material, not a style. It reacts in real time to two inputs: whether other elements are overlapping it, and whether it currently has focus. Visually it borrows refraction, reflection, and blur from real glass, then layers fluid morphing animations on top.[[lg1]]
The practical headline: rebuild against the latest SDK and your standard components inherit the new material for free. Bars, sheets, popovers, sliders, toggles, and buttons all switch over without a single line of code from you. Overlap behavior and focus response come along automatically.[[lg1]]
One catch: any custom backgrounds you've put behind NavigationStack, NavigationSplitView, titleBar, or toolbar(content:) have to come off. They sit in the same layer as the new material and the system's scroll edge effect, so they fight for the same pixels. Let the system paint the background.[[lg1]]
Sometimes you need to ship a build against the new SDK before you've finished the Liquid Glass redesign work. The UIDesignRequiresCompatibility key in Info.plist is the lever for that — with it set, the app runs on the new platforms but renders the old way. Treat it as a bridge, not a hiding place.[[lg1]]
If you've built a custom control that needs to read as part of the same surface family, reach for the SwiftUI modifier glassEffect(_:in:). Use it sparingly — the material is supposed to draw the eye to whatever the control is sitting on top of, so spreading it across the whole interface defeats the point. Pick the one or two elements that genuinely matter.[[lg1]]
Multiple custom Liquid Glass elements that animate between each other belong inside a GlassEffectContainer. The container is what makes the morphing look right and keeps rendering cost down.[[lg1]]
The two things that make Liquid Glass feel like Liquid Glass — translucency and morphing — both back off when the user asks them to. Settings ship a preferred-look toggle for the material itself, and the existing Reduce Transparency and Reduce Motion switches dampen or remove pieces of the effect. Stock components respect all of this on their own. Anything you've drawn yourself — custom views, custom palette, custom animation — needs a pass through both accessibility settings before you ship.[[lg1]]
The full Liquid Glass surface area touches multiple chapters in this book. New control APIs (.glass, .glassProminent, ConcentricRectangle) live in Book 6. Toolbar grouping with ToolbarSpacer is in Book 5. Tab(role: .search), tabBarMinimizeBehavior, and sidebarAdaptable are in Book 7. List and form metrics + title-style section headers are in Book 8. Sheet and action sheet changes are in Book 12. backgroundExtensionEffect for split views is in Book 13. glassEffect deep-dive for custom views is in Book 19.
[lg1] Apple Developer Documentation, *Adopting Liquid Glass*. [developer.apple.com/documentation/TechnologyOverviews/adopting-liquid-glass](https://developer.apple.com/documentation/TechnologyOverviews/adopting-liquid-glass) — verified 2026-04-29.
SwiftUI is Apple's declarative UI framework. You describe what you want the interface to look like, and SwiftUI figures out how to render it and when to update it.
It shipped in 2019 and has matured significantly. As of Swift 6 / Xcode 26, SwiftUI is the primary way to build Apple apps. UIKit still exists and works, but new projects should start with SwiftUI.
UIKit (imperative): You create a button, configure it, add it to a view, write code to handle layout changes, and manually update it when data changes.
// UIKit way -- you manage everything yourself
let label = UILabel()
label.text = "Score: 0"
label.font = .systemFont(ofSize: 18)
view.addSubview(label)
// ...later, when score changes:
label.text = "Score: \(newScore)"SwiftUI (declarative): You describe the view once. SwiftUI watches your data and re-renders automatically when it changes.
// SwiftUI way -- describe it, SwiftUI handles the rest
Text("Score: \(score)")
.font(.system(size: 18))When score changes, SwiftUI updates the Text automatically. You never manually set the text.
Every piece of UI in SwiftUI conforms to the View protocol. The protocol has one requirement: a computed property called body that returns some other View.
struct ScoreCard: View {
var body: some View {
Text("Hello, Michael")
.font(.system(size: 18, weight: .bold))
}
}That is a complete SwiftUI view. A few things to notice:
- struct, not class. SwiftUI views are value types. They are lightweight and frequently recreated. Do not put heavy logic in a view's initializer.
- some View. This is an "opaque return type." It means "I return a specific View type, but I am not telling you which one." The compiler figures it out.
- body is computed. It has no stored value. SwiftUI calls it whenever it needs to know what this view looks like right now.
- The
bodyproperty must return exactly one root view. If you need multiple views, wrap them in a container (VStack,HStack,ZStack,Group). - Do not put side effects (network calls, file writes, print statements) inside
body. It can be called many times. Use.onAppear,.task, or event handlers instead.
SwiftUI is built on composition. You build small views and combine them into larger ones.
struct PlayerRow: View {
let name: String
let score: Int
var body: some View {
HStack {
Text(name)
.font(.system(size: 18))
Spacer()
Text("\(score)")
.font(.system(size: 18, weight: .bold))
.foregroundStyle(.secondary)
}
.padding()
}
}
struct Scoreboard: View {
var body: some View {
VStack(alignment: .leading, spacing: 12) {
PlayerRow(name: "Michael", score: 42)
PlayerRow(name: "Claude", score: 38)
}
.padding()
}
}There is no limit to nesting depth. Break views into separate structs whenever a body gets hard to read -- generally past 30-40 lines.
SwiftUI provides three primary layout containers:
HStack(spacing: 12) {
Image(systemName: "star.fill")
Text("Favorites")
.font(.system(size: 18))
}VStack(alignment: .leading, spacing: 8) {
Text("Title")
.font(.system(size: 24, weight: .bold))
Text("Subtitle")
.font(.system(size: 18))
.foregroundStyle(.secondary)
}ZStack {
Color.blue
Text("Overlay")
.font(.system(size: 18))
.foregroundStyle(.white)
}Later children in a ZStack render on top of earlier ones.
Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 8) {
GridRow {
Text("Name")
.font(.system(size: 18, weight: .bold))
Text("Score")
.font(.system(size: 18, weight: .bold))
}
GridRow {
Text("Michael")
.font(.system(size: 18))
Text("42")
.font(.system(size: 18))
}
}-
VStackandHStackhave a 10-child limit in their@ViewBuilderclosure. If you need more, wrap groups inGroup {}or useForEach. -
Spacer()in anHStackpushes content to the edges. It is the most common way to achieve "left-align this, right-align that" layouts.
By default, view structs are immutable. If you need a value that changes and triggers re-rendering, mark it @State.
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack(spacing: 16) {
Text("Count: \(count)")
.font(.system(size: 24, weight: .bold))
Button("Add One") {
count += 1
}
.font(.system(size: 18))
}
}
}When count changes, SwiftUI re-evaluates body and updates only the parts that changed (the Text in this case).
- Always mark
@Statepropertiesprivate. They belong to this view and no one else. -
@Statesurvives view re-creation. SwiftUI stores the actual value separately from the struct. - Initialize
@Statewith a default value. Do not try to set it from outside the view (use@Bindingor@Observablefor that).
-
@Stateis for simple, view-local values: a toggle boolean, a text field string, a counter. If multiple views need to share the same data, use@Observable(see below). - Mutating
@Stateinsidebody(outside of a closure like a button action) causes an infinite loop. SwiftUI re-evaluatesbody, which changes state, which re-evaluatesbody, forever.
A @Binding creates a two-way connection to someone else's @State. The child view can read and write the value, and changes flow back to the owner.
struct ToggleRow: View {
let label: String
@Binding var isOn: Bool
var body: some View {
Toggle(label, isOn: $isOn)
.font(.system(size: 18))
}
}
struct SettingsView: View {
@State private var darkMode = false
@State private var notifications = true
var body: some View {
VStack(spacing: 12) {
ToggleRow(label: "Dark Mode", isOn: $darkMode)
ToggleRow(label: "Notifications", isOn: $notifications)
}
.padding()
}
}The $ prefix creates a binding from a @State property. $darkMode is a Binding<Bool> that reads and writes the underlying darkMode state.
- You cannot create a
@Bindingout of thin air. It must connect to a source of truth (@State,@Observableproperty, etc.). - For previews and testing, use
.constant():ToggleRow(label: "Test", isOn: .constant(true)).
@Observable is the modern way to share data across multiple views. It replaces the older ObservableObject / @Published / @ObservedObject pattern.
@Observable
class GameState {
var score = 0
var playerName = "Michael"
var isPlaying = false
}That is it. No @Published wrappers. The @Observable macro automatically tracks which properties each view reads, and only re-renders views that actually use changed properties.
struct GameView: View {
var game: GameState
var body: some View {
VStack(spacing: 16) {
Text("\(game.playerName)'s Score: \(game.score)")
.font(.system(size: 24, weight: .bold))
Button("Score Point") {
game.score += 1
}
.font(.system(size: 18))
}
}
}
struct ContentView: View {
@State private var game = GameState()
var body: some View {
GameView(game: game)
}
}The top-level view owns the GameState as @State. Child views receive it as a plain property. SwiftUI tracks dependencies automatically.
If you need to pass an @Observable object deep into the view hierarchy without threading it through every intermediate view:
// In the top-level view:
GameView()
.environment(game)
// In any descendant view:
struct DeepChildView: View {
@Environment(GameState.self) private var game
var body: some View {
Text("Score: \(game.score)")
.font(.system(size: 18))
}
}-
@Observablerequires iOS 17 / macOS 14 minimum. If you need to support older OS versions, useObservableObjectwith@Publishedproperties. -
@Observableclasses are reference types (classes, not structs). Multiple views holding the same instance see the same data. This is the point. - If an
@Observableproperty changes but no view reads it, no re-render happens. This is efficient and intentional.
View modifiers are methods you chain onto views to change their appearance or behavior. Each modifier returns a new view wrapping the original.
Text("Important Notice")
.font(.system(size: 20, weight: .bold))
.foregroundStyle(.red)
.padding()
.background(.yellow.opacity(0.3))
.clipShape(RoundedRectangle(cornerRadius: 8))Modifiers apply from inside out.
// Padding THEN background: background includes the padding
Text("Hello")
.padding()
.background(.blue)
// Background THEN padding: background is tight to the text, padding is outside
Text("Hello")
.background(.blue)
.padding()These produce visually different results. The first has a blue box with padding inside. The second has a tight blue background with empty space around it.
| Modifier | What It Does | |----------|-------------| | .font(.system(size: 18)) | Sets font size (use 18+ for accessibility) | | .foregroundStyle(.primary) | Text/icon color | | .padding() | Adds space around the view | | .padding(.horizontal, 16) | Adds space on specific edges | | .frame(width:height:alignment:) | Constrains or expands the view's size | | .background(...) | Puts something behind the view | | .overlay(...) | Puts something on top of the view | | .clipShape(...) | Clips to a shape (circle, rounded rect) | | .opacity(0.5) | Transparency | | .hidden() | Hides the view (still takes up space) | | .disabled(true) | Grays out and disables interaction | | .accessibilityLabel("...") | VoiceOver label |
For reusable styling, create a ViewModifier:
struct CardStyle: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(.background)
.clipShape(RoundedRectangle(cornerRadius: 12))
.shadow(radius: 2)
}
}
extension View {
func cardStyle() -> some View {
modifier(CardStyle())
}
}
// Usage:
Text("Hello")
.font(.system(size: 18))
.cardStyle()SwiftUI Previews let you see your views in Xcode without running the app. They update in real-time as you type.
#Preview {
ContentView()
}
// Named preview:
#Preview("Dark Mode") {
ContentView()
.preferredColorScheme(.dark)
}
// Multiple previews:
#Preview("Large Text") {
ContentView()
.environment(\.dynamicTypeSize, .accessibility3)
}- Wrap previews in a
NavigationStackif your view expects to be inside one. - Pass mock data to previews, not real network calls.
- If previews refuse to load, try: Resume (click the "Resume" button), clean build (Cmd+Shift+K), or restart Xcode.
- Previews run in Debug configuration. They can be slow with complex views.
- Previews crash if your view requires an
@Environmentvalue that is not provided. Always supply required environment values in your preview. - Previews do not support all device features (camera, Bluetooth, etc.).
- If previews show "Cannot preview in this file," there is usually a compile error somewhere. Check the build log.
SwiftUI builds a tree of views. Understanding this tree helps you reason about layout, updates, and performance.
App
WindowGroup
ContentView
NavigationStack
VStack
Text("Title")
List
ForEach
PlayerRow
HStack
Text(name)
Spacer
Text(score)- A
@Stateor@Observableproperty changes - SwiftUI identifies which views read that property
- Those views have their
bodyre-evaluated - SwiftUI diffs the old and new view trees
- Only the actual differences are rendered on screen
This is why you should keep body cheap. It may be called frequently, but SwiftUI only does real rendering work when the output actually changes.
SwiftUI uses structural identity (position in the view tree) to track views across updates. If you use ForEach with an id parameter, that id is how SwiftUI knows which item is which.
ForEach(players, id: \.name) { player in
PlayerRow(player: player)
}If two views swap positions but keep their IDs, SwiftUI animates the swap rather than destroying and recreating them.
- Using
ForEach(0..<array.count)instead ofForEach(array, id: \.self)(or a properIdentifiableconformance) breaks animations and can cause visual glitches. Always give SwiftUI stable identifiers. - Deeply nested view hierarchies are fine for readability but can slow down the compiler. If Xcode says "expression too complex," break the view into smaller sub-views.
Sometimes you need a UIKit view in SwiftUI (for features SwiftUI does not cover yet). Use UIViewRepresentable:
import UIKit
import SwiftUI
struct ActivityIndicator: UIViewRepresentable {
func makeUIView(context: Context) -> UIActivityIndicatorView {
let indicator = UIActivityIndicatorView(style: .large)
indicator.startAnimating()
return indicator
}
func updateUIView(_ uiView: UIActivityIndicatorView, context: Context) {
// Update the view if needed when SwiftUI state changes
}
}There is also UIViewControllerRepresentable for wrapping full UIKit view controllers. Use these as bridges, not as a primary development strategy.
SwiftUI has strong accessibility support built in, but you need to be intentional about it.
Use Dynamic Type sizes or explicit minimums. Michael's 18pt minimum means:
// Good -- explicit minimum size
Text("Readable text")
.font(.system(size: 18))
// Also good -- use a text style that scales
Text("Readable text")
.font(.title3) // 20pt default, scales with Dynamic TypeFont sizes below .title3 (which is 20pt) drop below 18pt at default Dynamic Type. The safe "always 18pt+" text styles are: .largeTitle, .title, .title2, .title3.
If you use .body (17pt default), it drops below 18pt at default settings. Either use .system(size: 18) explicitly or use .title3 and up.
Image(systemName: "star.fill")
.accessibilityLabel("Favorite")
// Group related content for VoiceOver:
HStack {
Text("Score:")
Text("\(score)")
}
.accessibilityElement(children: .combine)
// Hide decorative elements:
Image("decorative-line")
.accessibilityHidden(true)- Do not use
.font(.caption)or.font(.footnote)for anything the user actually needs to read. They are 12pt and 13pt respectively -- too small. - Always test with VoiceOver at least once before shipping. Simulator > Settings > Accessibility > VoiceOver.
- Color alone should never be the only way to convey information. Pair it with text, icons, or shapes.
-
Start with the data, then build the view. Define your model types first, then write the views that display them. SwiftUI works best when your data flow is clear.
-
One source of truth. Every piece of data should have exactly one owner. Other views get a binding or a reference, never a copy they also mutate.
-
Extract early, extract often. When a view body gets past 30 lines, pull part of it into a separate struct. This is free in SwiftUI -- small structs cost nothing.
-
Use
@Observablefor shared state,@Statefor local state. If only one view cares about a value, it is@State. If multiple views share it, it is@Observable. -
Previews are your rapid feedback loop. Set up multiple previews (light mode, dark mode, large text) so you catch layout issues without running the app.
-
When something looks wrong, check modifier order. Swapping
.padding()and.background()changes the result. Read modifiers bottom-to-top to understand layering.
Claude's Xcode 26 Swift Bible -- Book 2 By Michael Fluharty. Swift 6, Xcode 26.
← Book-01-Introducing-Swift-And-Xcode · Chapters and Appendices · Book-03-Introducing-Scenes-And-Windows →
Feedback: Found something off? Open an issue · Discuss it · Email Michael
Claude's X26 Swift6 Bible | GPL v3 | Built with Claude by Anthropic | Repo