| name | SwiftUI |
| description | SwiftUI patterns, view composition, styling, and reusable UI elements |
SwiftUI
You are a SwiftUI expert. Apply these patterns when building views and components.
Component Design Principles
Single Responsibility
Each component should do one thing well. Split complex views into smaller, focused components.
Configurable via Initializer
Expose configuration through init parameters, not environment or external state.
Preview-Friendly
Components should be easily previewable in isolation with sensible defaults.
Trailing Closure Style
Always prefer trailing closure syntax for better readability and consistency:
Button {
saveDocument()
} label: {
Label("Save", systemImage: "square.and.arrow.down")
}
Section {
TextField("Name", text: $name)
TextField("Email", text: $email)
} header: {
Text("Contact Info")
} footer: {
Text("We'll never share your email")
}
NavigationLink {
DetailView(item: item)
} label: {
ItemRow(item: item)
}
Button("Save") { saveDocument() }
Section("Header") { content }
NavigationLink("Details", destination: DetailView())
View Composition
Container Components
struct Card<Content: View>: View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
content
.padding()
.background(.background.secondary)
.clipShape(RoundedRectangle(cornerRadius: 12))
.shadow(color: .black.opacity(0.1), radius: 4, y: 2)
}
}
Card {
VStack {
Text("Title")
Text("Subtitle")
}
}
Slot-Based Components
struct ListRow<Leading: View, Trailing: View>: View {
let title: String
let leading: Leading
let trailing: Trailing
init(
title: String,
@ViewBuilder leading: () -> Leading = { EmptyView() },
@ViewBuilder trailing: () -> Trailing = { EmptyView() }
) {
self.title = title
self.leading = leading()
self.trailing = trailing()
}
var body: some View {
HStack {
leading
Text(title)
Spacer()
trailing
}
}
}
ListRow(title: "Settings") {
Image(systemName: "gear")
} trailing: {
Image(systemName: "chevron.right")
}
Common Patterns
Loading States
struct AsyncContent<Content: View, Placeholder: View>: View {
let isLoading: Bool
let content: Content
let placeholder: Placeholder
init(
isLoading: Bool,
@ViewBuilder content: () -> Content,
@ViewBuilder placeholder: () -> Placeholder = { ProgressView() }
) {
self.isLoading = isLoading
self.content = content()
self.placeholder = placeholder()
}
var body: some View {
if isLoading {
placeholder
} else {
content
}
}
}
Empty States
struct EmptyState: View {
let icon: String
let title: String
let message: String
var action: (() -> Void)?
var actionTitle: String?
var body: some View {
ContentUnavailableView {
Label(title, systemImage: icon)
} description: {
Text(message)
} actions: {
if let action, let actionTitle {
Button(actionTitle, action: action)
.buttonStyle(.borderedProminent)
}
}
}
}
EmptyState(
icon: "doc.text",
title: "No Documents",
message: "Create your first document to get started",
action: { createDocument() },
actionTitle: "Create Document"
)
Error States
struct ErrorView: View {
let error: Error
let retry: (() -> Void)?
var body: some View {
ContentUnavailableView {
Label("Something went wrong", systemImage: "exclamationmark.triangle")
} description: {
Text(error.localizedDescription)
} actions: {
if let retry {
Button("Try Again", action: retry)
.buttonStyle(.bordered)
}
}
}
}
Form Components
Validated Text Field
struct ValidatedField: View {
let title: String
@Binding var text: String
let validation: (String) -> String?
@State private var error: String?
@FocusState private var isFocused: Bool
var body: some View {
VStack(alignment: .leading, spacing: 4) {
TextField(title, text: $text)
.focused($isFocused)
.onChange(of: isFocused) { _, focused in
if !focused {
error = validation(text)
}
}
if let error {
Text(error)
.font(.caption)
.foregroundStyle(.red)
}
}
}
}
ValidatedField(title: "Email", text: $email) { value in
value.contains("@") ? nil : "Invalid email"
}
Option Picker
struct OptionPicker<T: Hashable>: View {
let options: [T]
@Binding var selection: T
let label: (T) -> String
var body: some View {
HStack(spacing: 8) {
ForEach(options, id: \.self) { option in
Button {
selection = option
} label: {
Text(label(option))
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(selection == option ? Color.accentColor : Color.secondary.opacity(0.2))
.foregroundStyle(selection == option ? .white : .primary)
.clipShape(Capsule())
}
.buttonStyle(.plain)
}
}
}
}
Interactive Components
Swipe Actions
struct SwipeableRow<Content: View>: View {
let content: Content
let onDelete: () -> Void
let onArchive: (() -> Void)?
init(
@ViewBuilder content: () -> Content,
onDelete: @escaping () -> Void,
onArchive: (() -> Void)? = nil
) {
self.content = content()
self.onDelete = onDelete
self.onArchive = onArchive
}
var body: some View {
content
.swipeActions(edge: .trailing) {
Button(role: .destructive, action: onDelete) {
Label("Delete", systemImage: "trash")
}
if let onArchive {
Button(action: onArchive) {
Label("Archive", systemImage: "archivebox")
}
.tint(.orange)
}
}
}
}
Expandable Section
struct ExpandableSection<Content: View>: View {
let title: String
let content: Content
@State private var isExpanded = false
init(title: String, @ViewBuilder content: () -> Content) {
self.title = title
self.content = content()
}
var body: some View {
VStack(alignment: .leading, spacing: 0) {
Button {
withAnimation(.snappy) {
isExpanded.toggle()
}
} label: {
HStack {
Text(title)
.font(.headline)
Spacer()
Image(systemName: "chevron.right")
.rotationEffect(.degrees(isExpanded ? 90 : 0))
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
if isExpanded {
content
.padding(.top, 12)
}
}
}
}
Data Display
Stat Card
struct StatCard: View {
let title: String
let value: String
let trend: Trend?
let icon: String?
enum Trend {
case up(String)
case down(String)
case neutral
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text(title)
.font(.subheadline)
.foregroundStyle(.secondary)
Spacer()
if let icon {
Image(systemName: icon)
.foregroundStyle(.secondary)
}
}
Text(value)
.font(.title)
.fontWeight(.semibold)
if let trend {
trendView(trend)
}
}
.padding()
.background(.background.secondary)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
@ViewBuilder
private func trendView(_ trend: Trend) -> some View {
switch trend {
case .up(let value):
Label(value, systemImage: "arrow.up")
.font(.caption)
.foregroundStyle(.green)
case .down(let value):
Label(value, systemImage: "arrow.down")
.font(.caption)
.foregroundStyle(.red)
case .neutral:
EmptyView()
}
}
}
Avatar
struct Avatar: View {
let name: String
let imageURL: URL?
let size: CGFloat
init(name: String, imageURL: URL? = nil, size: CGFloat = 40) {
self.name = name
self.imageURL = imageURL
self.size = size
}
var body: some View {
Group {
if let imageURL {
AsyncImage(url: imageURL) { image in
image.resizable().scaledToFill()
} placeholder: {
initialsView
}
} else {
initialsView
}
}
.frame(width: size, height: size)
.clipShape(Circle())
}
private var initialsView: some View {
Text(initials)
.font(.system(size: size * 0.4, weight: .medium))
.foregroundStyle(.white)
.frame(width: size, height: size)
.background(color)
}
private var initials: String {
name.split(separator: " ")
.prefix(2)
.compactMap { $0.first }
.map(String.init)
.joined()
.uppercased()
}
private var color: Color {
let colors: [Color] = [.blue, .purple, .orange, .green, .pink]
let index = abs(name.hashValue) % colors.count
return colors[index]
}
}
Layout Components
Adaptive Grid
struct AdaptiveGrid<Content: View, Item: Identifiable>: View {
let items: [Item]
let minWidth: CGFloat
let spacing: CGFloat
let content: (Item) -> Content
init(
items: [Item],
minWidth: CGFloat = 150,
spacing: CGFloat = 16,
@ViewBuilder content: @escaping (Item) -> Content
) {
self.items = items
self.minWidth = minWidth
self.spacing = spacing
self.content = content
}
var body: some View {
LazyVGrid(
columns: [GridItem(.adaptive(minimum: minWidth), spacing: spacing)],
spacing: spacing
) {
ForEach(items) { item in
content(item)
}
}
}
}
Aspect Ratio Container
struct AspectRatioContainer<Content: View>: View {
let aspectRatio: CGFloat
let content: Content
init(
_ aspectRatio: CGFloat = 1,
@ViewBuilder content: () -> Content
) {
self.aspectRatio = aspectRatio
self.content = content()
}
var body: some View {
GeometryReader { geometry in
content
.frame(
width: geometry.size.width,
height: geometry.size.width / aspectRatio
)
}
.aspectRatio(aspectRatio, contentMode: .fit)
}
}
Animation Components
Shimmer Loading
struct ShimmerView: View {
@State private var phase: CGFloat = 0
var body: some View {
LinearGradient(
colors: [
.gray.opacity(0.3),
.gray.opacity(0.1),
.gray.opacity(0.3)
],
startPoint: .leading,
endPoint: .trailing
)
.mask(Rectangle())
.offset(x: phase)
.onAppear {
withAnimation(.linear(duration: 1.5).repeatForever(autoreverses: false)) {
phase = 200
}
}
}
}
struct ShimmerModifier: ViewModifier {
let isLoading: Bool
func body(content: Content) -> some View {
if isLoading {
content.redacted(reason: .placeholder)
.overlay(ShimmerView())
} else {
content
}
}
}
extension View {
func shimmer(isLoading: Bool) -> some View {
modifier(ShimmerModifier(isLoading: isLoading))
}
}
Pulse Animation
struct PulseView: View {
@State private var isPulsing = false
var body: some View {
Circle()
.fill(.blue)
.frame(width: 20, height: 20)
.overlay(
Circle()
.stroke(.blue, lineWidth: 2)
.scaleEffect(isPulsing ? 2 : 1)
.opacity(isPulsing ? 0 : 1)
)
.onAppear {
withAnimation(.easeOut(duration: 1).repeatForever(autoreverses: false)) {
isPulsing = true
}
}
}
}
Accessibility
Always Include
struct AccessibleButton: View {
let title: String
let icon: String
let action: () -> Void
var body: some View {
Button(action: action) {
Label(title, systemImage: icon)
}
.accessibilityLabel(title)
.accessibilityHint("Double tap to \(title.lowercased())")
}
}
Dynamic Type Support
struct AdaptiveStack<Content: View>: View {
@Environment(\.dynamicTypeSize) var dynamicTypeSize
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
if dynamicTypeSize >= .accessibility1 {
VStack(alignment: .leading, content: { content })
} else {
HStack(content: { content })
}
}
}
Preview Organization
#Preview("Default") {
Avatar(name: "John Doe")
}
#Preview("With Image") {
Avatar(name: "Jane Smith", imageURL: URL(string: "https://example.com/avatar.jpg"))
}
#Preview("Large") {
Avatar(name: "Bob Wilson", size: 80)
}
#Preview("Grid") {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 60))]) {
ForEach(["Alice", "Bob", "Charlie", "Diana"], id: \.self) { name in
Avatar(name: name)
}
}
.padding()
}
Colors and Styling
Hierarchical Colors
SwiftUI provides semantic foreground styles that automatically adapt to context:
VStack(alignment: .leading) {
Text("Primary text")
.foregroundStyle(.primary)
Text("Secondary text")
.foregroundStyle(.secondary)
Text("Tertiary text")
.foregroundStyle(.tertiary)
Text("Quaternary text")
.foregroundStyle(.quaternary)
Text("Quinary text")
.foregroundStyle(.quinary)
}
Use cases:
.primary - Titles, main content, important information
.secondary - Subtitles, captions, supporting details
.tertiary - Placeholders, hints, timestamps
.quaternary - Dividers, subtle backgrounds
.quinary - Decorative elements, barely visible accents
Hierarchical SF Symbols
Image(systemName: "square.stack.3d.up.fill")
.symbolRenderingMode(.hierarchical)
.foregroundStyle(.blue)
Image(systemName: "person.crop.circle.badge.checkmark")
.symbolRenderingMode(.palette)
.foregroundStyle(.primary, .green)
Color Gradients
Apply gradients directly to colors for smooth visual effects:
Text("Gradient Text")
.font(.largeTitle)
.foregroundStyle(.blue.gradient)
Circle()
.fill(.orange.gradient)
RoundedRectangle(cornerRadius: 12)
.fill(.purple.gradient)
Text("Card")
.padding()
.background(.indigo.gradient)
Custom Gradients
LinearGradient(
colors: [.blue, .purple],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
RadialGradient(
colors: [.white, .blue],
center: .center,
startRadius: 0,
endRadius: 100
)
AngularGradient(
colors: [.red, .yellow, .green, .blue, .purple, .red],
center: .center
)
MeshGradient(
width: 3,
height: 3,
points: [
[0, 0], [0.5, 0], [1, 0],
[0, 0.5], [0.5, 0.5], [1, 0.5],
[0, 1], [0.5, 1], [1, 1]
],
colors: [
.red, .orange, .yellow,
.green, .blue, .purple,
.pink, .mint, .cyan
]
)
Background Materials
Text("Blurred background")
.padding()
.background(.ultraThinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 12))
Shadow Styles
Text("Shadowed")
.shadow(color: .black.opacity(0.2), radius: 4, x: 0, y: 2)
RoundedRectangle(cornerRadius: 12)
.fill(.white)
.overlay {
RoundedRectangle(cornerRadius: 12)
.stroke(.black.opacity(0.1), lineWidth: 1)
}
Best Practices
- Use @ViewBuilder for flexible content composition
- Provide sensible defaults for optional parameters
- Support Dynamic Type and accessibility
- Keep components focused - one responsibility per component
- Use semantic colors (.primary, .secondary, .tertiary, .quaternary)
- Use .gradient on colors for subtle depth
- Test with previews in various configurations
- Document complex components with usage examples