SwiftUI Skill
Declarative UI framework knowledge for building modern Apple platform interfaces.
Prerequisites
- Xcode 15+ installed
- iOS 16+ / macOS 13+ deployment target recommended
- Understanding of reactive programming concepts
Parameters
parameters:
min_ios_version:
type: string
default: "16.0"
description: Minimum iOS version
platforms:
type: array
items: [iOS, macOS, watchOS, tvOS, visionOS]
default: [iOS]
observation_framework:
type: string
enum: [observation, combine, observable_object]
default: observation
description: State management approach
Topics Covered
Property Wrappers
| Wrapper |
Ownership |
Use Case |
@State |
View owns |
Local, private state |
@Binding |
Parent owns |
Two-way child connection |
@StateObject |
View creates/owns |
Observable object lifecycle |
@ObservedObject |
External owns |
Passed observable |
@EnvironmentObject |
Environment owns |
Dependency injection |
@Environment |
System provides |
System values (colorScheme, etc) |
Observation (iOS 17+)
| Feature |
Description |
@Observable |
Macro for observable classes |
@Bindable |
Create bindings from Observable |
| Automatic tracking |
No need for @Published |
Layout System
| Container |
Purpose |
VStack |
Vertical arrangement |
HStack |
Horizontal arrangement |
ZStack |
Overlapping views |
LazyVStack/HStack |
Lazy loading for lists |
Grid |
2D grid layout |
GeometryReader |
Access to size/position |
Code Examples
Observation Pattern (iOS 17+)
import SwiftUI
@Observable
final class ShoppingCart {
var items: [CartItem] = []
var couponCode: String = ""
var subtotal: Decimal {
items.reduce(0) { $0 + $1.price * Decimal($1.quantity) }
}
var total: Decimal {
let discount = applyCoupon(to: subtotal)
return subtotal - discount
}
func add(_ product: Product, quantity: Int = 1) {
if let index = items.firstIndex(where: { $0.product.id == product.id }) {
items[index].quantity += quantity
} else {
items.append(CartItem(product: product, quantity: quantity))
}
}
func remove(_ item: CartItem) {
items.removeAll { $0.id == item.id }
}
private func applyCoupon(to amount: Decimal) -> Decimal {
guard !couponCode.isEmpty else { return 0 }
// Apply coupon logic
return amount * 0.1
}
}
struct CartView: View {
@Bindable var cart: ShoppingCart
var body: some View {
List {
ForEach(cart.items) { item in
CartItemRow(item: item)
}
.onDelete { indexSet in
cart.items.remove(atOffsets: indexSet)
}
Section {
HStack {
TextField("Coupon code", text: $cart.couponCode)
Button("Apply") { }
}
LabeledContent("Subtotal", value: cart.subtotal, format: .currency(code: "USD"))
LabeledContent("Total", value: cart.total, format: .currency(code: "USD"))
.fontWeight(.bold)
}
}
.navigationTitle("Cart (\(cart.items.count))")
}
}
Custom View Modifier
struct CardStyle: ViewModifier {
let cornerRadius: CGFloat
let shadowRadius: CGFloat
func body(content: Content) -> some View {
content
.background(.background)
.clipShape(RoundedRectangle(cornerRadius: cornerRadius))
.shadow(color: .black.opacity(0.1), radius: shadowRadius, y: 2)
}
}
extension View {
func cardStyle(cornerRadius: CGFloat = 12, shadowRadius: CGFloat = 4) -> some View {
modifier(CardStyle(cornerRadius: cornerRadius, shadowRadius: shadowRadius))
}
}
// Usage
struct ProductCard: View {
let product: Product
var body: some View {
VStack(alignment: .leading, spacing: 8) {
AsyncImage(url: product.imageURL) { image in
image.resizable().aspectRatio(contentMode: .fill)
} placeholder: {
ProgressView()
}
.frame(height: 150)
.clipped()
Text(product.name)
.font(.headline)
Text(product.price, format: .currency(code: "USD"))
.foregroundStyle(.secondary)
}
.cardStyle()
}
}
Custom Animations
struct PulsingButton: View {
let title: String
let action: () -> Void
@State private var isPulsing = false
var body: some View {
Button(action: action) {
Text(title)
.font(.headline)
.foregroundStyle(.white)
.padding(.horizontal, 24)
.padding(.vertical, 12)
.background(.blue)
.clipShape(Capsule())
.scaleEffect(isPulsing ? 1.05 : 1.0)
}
.onAppear {
withAnimation(.easeInOut(duration: 0.8).repeatForever(autoreverses: true)) {
isPulsing = true
}
}
}
}
struct MatchedGeometryExample: View {
@Namespace private var animation
@State private var isExpanded = false
var body: some View {
VStack {
if isExpanded {
RoundedRectangle(cornerRadius: 20)
.fill(.blue)
.matchedGeometryEffect(id: "shape", in: animation)
.frame(height: 300)
} else {
RoundedRectangle(cornerRadius: 10)
.fill(.blue)
.matchedGeometryEffect(id: "shape", in: animation)
.frame(width: 100, height: 100)
}
}
.onTapGesture {
withAnimation(.spring(response: 0.5, dampingFraction: 0.7)) {
isExpanded.toggle()
}
}
}
}
Navigation Stack (iOS 16+)
struct NavigationExample: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
List(products) { product in
NavigationLink(value: product) {
ProductRow(product: product)
}
}
.navigationTitle("Products")
.navigationDestination(for: Product.self) { product in
ProductDetailView(product: product)
}
.navigationDestination(for: Category.self) { category in
CategoryView(category: category)
}
}
}
func navigateToProduct(_ product: Product) {
path.append(product)
}
func popToRoot() {
path.removeLast(path.count)
}
}
Troubleshooting
Common Issues
| Issue |
Cause |
Solution |
| View not updating |
Wrong property wrapper |
Check ownership: @State vs @StateObject |
| Preview crash |
Missing mock data |
Provide preview with sample data |
| Animation stutters |
Expensive body |
Extract subviews, avoid complex calculations |
| Navigation broken |
Missing NavigationStack |
Ensure view is inside NavigationStack |
| List slow |
Complex cells |
Use LazyVStack, simplify cell views |
Debug Tips
// Trace view updates
var body: some View {
let _ = Self._printChanges()
// ... view content
}
// Check if preview
#if DEBUG
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView(data: .preview)
}
}
#endif
Validation Rules
validation:
- rule: state_ownership
severity: error
check: @StateObject for views that create, @ObservedObject for passed
- rule: body_purity
severity: warning
check: No side effects in body computed property
- rule: lazy_for_lists
severity: info
check: Use LazyVStack/LazyHStack for long scrolling content
Usage
Skill("swift-swiftui")
Related Skills
swift-combine - Reactive programming
swift-uikit - UIKit interop
swift-architecture - MVVM patterns