-
Notifications
You must be signed in to change notification settings - Fork 0
Book 19 Building Custom Views And Modifiers
Part V — Advanced Techniques · Claude's Xcode 26 Swift Bible
← Book-18-Error-Handling-And-Result-Type · Chapters and Appendices · Book-20-Performance-Instruments-And-Best-Practices →
Claude's Swift Reference 26 -- Part V: Advanced Techniques
By the end of this chapter you can:
- Extract reusable SwiftUI views from copy-pasted snippets.
- Write custom
ViewModifiertypes and apply them with.modifieror a cleaner.myStyle()extension. - Use
@Environmentvalues to pass data deep into a view tree without threading it through every level. - Read child-view sizes and positions with
GeometryReader. - Share data from child to parent with
PreferenceKey. - Animate transitions between layouts with
.matchedGeometryEffect.
The first step past "all code in one big ContentView" is pulling repeated shapes into their own View types.
struct AvatarRow: View {
let name: String
let imageName: String
var body: some View {
HStack {
Image(systemName: imageName)
.resizable()
.scaledToFit()
.frame(width: 40, height: 40)
.foregroundStyle(.blue)
Text(name).font(.headline)
Spacer()
}
.padding()
}
}Used anywhere:
VStack {
AvatarRow(name: "Ada", imageName: "person.circle")
AvatarRow(name: "Max", imageName: "person.circle.fill")
AvatarRow(name: "Claude", imageName: "sparkles")
}The rule of three: the third time you write the same layout, turn it into a view.
When the thing you keep repeating is a stack of modifiers, not a layout, reach for ViewModifier instead of a new view.
struct CardStyle: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12))
.shadow(color: .black.opacity(0.12), radius: 6, y: 2)
}
}Apply it with .modifier(CardStyle()), or -- for a cleaner call site -- add a View extension:
extension View {
func card() -> some View { modifier(CardStyle()) }
}
// Usage
Text("Hello")
.card()Custom modifiers are how you ship a house style. A dozen views across the app, one definition, any future tweak happens in one place.
struct BadgedStyle: ViewModifier {
let color: Color
let count: Int
func body(content: Content) -> some View {
content.overlay(alignment: .topTrailing) {
if count > 0 {
Text("\(count)")
.font(.caption2.bold())
.padding(4)
.background(color, in: Circle())
.foregroundStyle(.white)
.offset(x: 8, y: -8)
}
}
}
}
extension View {
func badged(_ count: Int, color: Color = .red) -> some View {
modifier(BadgedStyle(color: color, count: count))
}
}
// Usage
Image(systemName: "envelope")
.font(.title)
.badged(3)When deeply nested views need the same value (theme, date formatter, API client), don't thread it through every initializer. Put it in the environment.
struct Banner: View {
@Environment(\.colorScheme) private var scheme
var body: some View {
Text("Hello")
.foregroundStyle(scheme == .dark ? .white : .black)
}
}Define a key, extend EnvironmentValues, and set it with .environment:
private struct ThemeKey: EnvironmentKey {
static let defaultValue: Color = .blue
}
extension EnvironmentValues {
var theme: Color {
get { self[ThemeKey.self] }
set { self[ThemeKey.self] = newValue }
}
}
// A view that reads it
struct ThemedButton: View {
@Environment(\.theme) private var theme
let title: String
var body: some View {
Text(title).padding().background(theme).foregroundStyle(.white)
}
}
// A view tree that sets it
VStack {
ThemedButton(title: "Save")
ThemedButton(title: "Cancel")
}
.environment(\.theme, .orange)Every ThemedButton under that .environment(...) sees orange. Change the value once and every button updates.
GeometryReader gives its child view a GeometryProxy describing the size and safe-area it has been allocated.
struct SplitHalf: View {
var body: some View {
GeometryReader { geo in
HStack(spacing: 0) {
Color.red.frame(width: geo.size.width / 2)
Color.blue.frame(width: geo.size.width / 2)
}
}
}
}GeometryReader expands to fill the space it is offered, which often surprises newcomers. If all you need is "half the width," reach for .frame(maxWidth: .infinity) and let SwiftUI's normal layout system do the work.
Use GeometryReader when you genuinely need to compute something from the runtime size -- a custom layout, a parallax effect, a shape that depends on width / height.
In iOS 18, .onGeometryChange observes a view's frame without wrapping it in GeometryReader:
Text("Hello")
.onGeometryChange(for: CGSize.self) { proxy in
proxy.size
} action: { newSize in
print("I am now", newSize)
}Cleaner than GeometryReader when you only need a measurement, not a layout container.
The environment passes data down the view tree. Preference keys pass data up.
private struct HeightKey: PreferenceKey {
static let defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
}
struct RowLikeContent: View {
var body: some View {
Text("I report my height upward")
.background(
GeometryReader { geo in
Color.clear.preference(key: HeightKey.self, value: geo.size.height)
}
)
}
}
struct Parent: View {
@State private var rowHeight: CGFloat = 0
var body: some View {
VStack {
RowLikeContent()
Text("The row above is \(Int(rowHeight)) points tall")
}
.onPreferenceChange(HeightKey.self) { rowHeight = $0 }
}
}reduce is how SwiftUI combines values from multiple children; in this case we take the largest. Use preference keys when a parent needs to size itself based on its children, or when a child needs to say something specific to an ancestor.
When the same "thing" appears in two different layouts, matchedGeometryEffect animates it between them.
struct PhotoDetail: View {
@Namespace private var ns
@State private var expanded = false
var body: some View {
VStack {
if expanded {
Image("kitten")
.resizable()
.scaledToFit()
.matchedGeometryEffect(id: "photo", in: ns)
.onTapGesture { withAnimation(.spring) { expanded.toggle() } }
} else {
HStack {
Image("kitten")
.resizable()
.scaledToFit()
.frame(width: 80, height: 80)
.matchedGeometryEffect(id: "photo", in: ns)
.onTapGesture { withAnimation(.spring) { expanded.toggle() } }
Text("Kitten").font(.headline)
}
}
}
}
}Tap the image. SwiftUI notices the two views share the id: "photo" in the same namespace, and animates position + size from the small version to the full one (and back). You don't write any of the intermediate frames.
@Namespace gives you a private namespace; reuse its value for every view pair you want matched.
Putting custom views, a custom modifier, and an environment value together:
import SwiftUI
private struct AccentKey: EnvironmentKey { static let defaultValue: Color = .blue }
extension EnvironmentValues {
var accent: Color {
get { self[AccentKey.self] }
set { self[AccentKey.self] = newValue }
}
}
struct BadgedIcon: View {
let systemName: String
let count: Int
@Environment(\.accent) private var accent
var body: some View {
Image(systemName: systemName)
.font(.title)
.foregroundStyle(accent)
.overlay(alignment: .topTrailing) {
if count > 0 {
Text("\(count)")
.font(.caption2.bold())
.padding(4)
.background(.red, in: Circle())
.foregroundStyle(.white)
.offset(x: 8, y: -8)
}
}
}
}
struct CardStyle: ViewModifier {
func body(content: Content) -> some View {
content.padding()
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12))
.shadow(color: .black.opacity(0.12), radius: 6, y: 2)
}
}
extension View { func card() -> some View { modifier(CardStyle()) } }
struct NotificationBar: View {
var body: some View {
HStack(spacing: 24) {
BadgedIcon(systemName: "envelope", count: 3)
BadgedIcon(systemName: "bell", count: 0)
BadgedIcon(systemName: "person.2", count: 1)
}
.card()
.environment(\.accent, .orange)
}
}One reusable icon view, one reusable card modifier, one environment value -- and the bar's entire accent color lives in one spot.
We've added view-shaped tools to the toolkit. Book 20 pulls back and looks at the app as a running system: Instruments, profiling, and the habits that keep a SwiftUI app smooth as it grows.
← Book-18-Error-Handling-And-Result-Type · Chapters and Appendices · Book-20-Performance-Instruments-And-Best-Practices →
Feedback: Found something off? Open an issue · Discuss it · Email Michael
Claude's X26 Swift6 Bible | GPL v3 | Built with Claude by Anthropic | Repo