Once you can build screens, the next skill is building parts: small, reusable pieces that give a whole app one consistent look and make each screen shorter. This lesson shows the SwiftUI tools for that, then two features that make apps feel polished: shared-element animation and charts. All of them need iOS 16 or newer, and most need nothing beyond what you already know.
ViewModifier: reuse a style
If you repeat the same chain of modifiers, such as padding, background, corners and shadow, wrap it in a ViewModifier. Add a small extension so the call reads like a built-in modifier.
import SwiftUI
struct CardStyle: ViewModifier {
func body(content: Content) -> some View {
content
.padding()
.background(.background, in: RoundedRectangle(cornerRadius: 16))
.shadow(color: .black.opacity(0.1), radius: 6, y: 2)
}
}
extension View {
func card() -> some View { modifier(CardStyle()) }
}
// use
Text("Hello").card()
ViewBuilder: containers that accept views
A view that takes other views as its content uses a `@ViewBuilder` property. This is how VStack { ... } works, and you can write your own.
struct TitledCard<Content: View>: View {
let title: String
@ViewBuilder var content: Content
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(title).font(.headline)
content
}
.frame(maxWidth: .infinity, alignment: .leading)
.card()
}
}
// use
TitledCard(title: "This month") {
Text("Food: 120")
Text("Travel: 60")
}
A custom Layout
The Layout protocol (iOS 16 and up) lets you decide exactly how children are measured and placed, when stacks and grids are not enough. You write two methods: one that reports the total size, and one that places each child. This example makes every child as wide as the widest one.
struct EqualWidthHStack: Layout {
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let sizes = subviews.map { $0.sizeThatFits(.unspecified) }
let width = sizes.map(\.width).max() ?? 0
let height = sizes.map(\.height).max() ?? 0
return CGSize(width: width * CGFloat(subviews.count), height: height)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
let width = subviews.map { $0.sizeThatFits(.unspecified).width }.max() ?? 0
for (index, subview) in subviews.enumerated() {
subview.place(
at: CGPoint(x: bounds.minX + CGFloat(index) * width, y: bounds.minY),
proposal: ProposedViewSize(width: width, height: bounds.height)
)
}
}
}
// use
EqualWidthHStack {
Button("OK") {}
Button("Cancel, maybe later") {}
}
matchedGeometryEffect: shared-element animation
matchedGeometryEffect tells SwiftUI that two views in different states are *the same thing*, so it moves and resizes one into the other. The classic use is a selection highlight that slides between tabs.
struct PeriodPicker: View {
@Namespace private var namespace
@State private var selected = 0
private let titles = ["Day", "Week", "Month"]
var body: some View {
HStack {
ForEach(titles.indices, id: \.self) { index in
Text(titles[index])
.padding(.horizontal, 14)
.padding(.vertical, 8)
.background {
if selected == index {
Capsule()
.fill(.blue.opacity(0.2))
.matchedGeometryEffect(id: "highlight", in: namespace)
}
}
.onTapGesture {
withAnimation(.easeInOut) { selected = index }
}
}
}
}
}
Only one capsule exists at a time, with the same id and namespace, so when selected changes SwiftUI animates it from the old place to the new one. The same idea can grow a thumbnail into a full-screen image.
Charts
Apple's Charts framework draws bar, line, area and other charts from your data in a few lines, with accessibility and Dark Mode built in. Each data point becomes a *mark*, and you say which values map to which axis.
import Charts
import SwiftUI
struct CategoryTotal: Identifiable {
let id = UUID()
let name: String
let amount: Double
}
struct SpendingChart: View {
let totals = [
CategoryTotal(name: "Food", amount: 120),
CategoryTotal(name: "Travel", amount: 60),
CategoryTotal(name: "Fun", amount: 45)
]
var body: some View {
Chart(totals) { item in
BarMark(
x: .value("Category", item.name),
y: .value("Amount", item.amount)
)
.foregroundStyle(by: .value("Category", item.name))
}
.frame(height: 220)
}
}
Performance habits for SwiftUI
- Cheap bodies. The
bodyruns often. Do not sort big arrays, format many dates or decode JSON inside it. Compute once, and keep the result in state or the view model. - Stable identity. Give
ForEacha stableid. An id that changes each time makes SwiftUI throw away and rebuild rows. - Lazy containers. Use
LazyVStackorListfor long content, and an ordinaryVStackfor a few views. - Small views. When one value changes, only the views that read it are redrawn. Splitting a big body into small views keeps redraws small.
- Measure, do not guess. Xcode has a SwiftUI template in Instruments that shows how often views update.
Accessibility and Dark Mode in reusable parts
The great advantage of reusable parts is that you fix a problem once. Build accessibility into them from the start. In CardStyle, use semantic colours such as .background and .primary, so Dark Mode works without extra code. In a custom control, add .accessibilityLabel("...") and, where it makes sense, .accessibilityAddTraits(.isButton). Text that uses .font(.body) or another text style grows when people choose larger text in Settings, so avoid fixed point sizes. Preview your parts in several settings at once, for example Dark Mode with the largest text size, using the Xcode preview variants. Ten minutes spent here saves many support emails later, and it is a point interviewers like to hear.
Common mistakes
- Copying the same five modifiers on every screen instead of making a
ViewModifier. - A
matchedGeometryEffectwhere the same id appears twice at once, which confuses the animation. - Using
GeometryReaderfor everything. It takes all the space it is given, and often a simpler tool is better.
Your task
- Make
CardStyleandTitledCardand use them on two screens of your app. - Write
EqualWidthHStackand compare it with a normalHStackof two buttons with different text lengths. - Build the
PeriodPickerand check that the highlight slides. - Draw the spending chart from your own expense data, grouped by category.
How to check it worked
- Changing
CardStylein one place changes every card. - The highlight moves smoothly between Day, Week and Month.
- You can name three habits that keep a SwiftUI screen fast.