com um clique
swiftui-patterns-developer
// SwiftUI view structure, composition, and best practices. Use when refactoring SwiftUI views, organizing view files, or extracting subviews.
// SwiftUI view structure, composition, and best practices. Use when refactoring SwiftUI views, organizing view files, or extracting subviews.
Triage Sentry crashes for an iOS release. Pulls unresolved fatal events from sentry.anytype.io, investigates each fingerprint cluster against the source, creates one Linear ticket per cluster with a root-cause hypothesis (no proposed fix - the implementer figures that out with full context), then archives the Sentry issue (status `ignored`) so it stops cluttering the inbox. Activate on "triage Sentry crashes", "triage fatal errors", "investigate crashes in release", "fatal errors in 0.X.Y", or any time the user wants to turn a release's Sentry inbox into actionable Linear tickets. The slash entry is `/do-sentry-triage`.
Expert guidance on Swift concurrency using the Office Building mental model. Use when working with actors, isolation, Sendable, TaskGroups, or fixing concurrency warnings and data race issues.
Context-aware routing to iOS 26 Liquid Glass implementation patterns. Use when working with glass effects, GlassEffectContainer, morphing transitions, or iOS 26 visual effects.
Decompose a large Linear task into independent subtasks with a master plan. Analyzes issue, project, related tasks, PRs, and codebase to create PLAN.md and Linear sub-issues.
Context-aware routing to Swift/iOS development patterns, architecture, and best practices. Use when working with .swift files, ViewModels, Coordinators, refactoring, or discussing Swift/SwiftUI patterns.
Audit and improve SwiftUI runtime performance through code review and Instruments guidance. Use for diagnosing slow rendering, janky scrolling, excessive view updates, or layout thrash in SwiftUI apps.
| name | swiftui-patterns-developer |
| description | SwiftUI view structure, composition, and best practices. Use when refactoring SwiftUI views, organizing view files, or extracting subviews. |
Apply consistent structure and patterns to SwiftUI views, with focus on ordering, subview extraction, and proper composition.
Understanding these fundamentals helps you write better SwiftUI code:
1. Declarative - Describe what you want, not how to create it:
// ✅ Declarative - describe the result
List(pets) { pet in
HStack {
Text(pet.name)
Spacer()
Text(pet.species)
}
}
// No need to add/remove rows manually - SwiftUI handles it
2. Compositional - Build complex UIs from simple building blocks:
// ViewBuilder closures define children of containers
HStack { // Container view
Image(...) // Child 1
VStack { // Child 2 (also a container)
Text(...) // Nested child
Text(...) // Nested child
}
Spacer() // Child 3
}
3. State-Driven - UI automatically updates when state changes:
// SwiftUI tracks dependencies and updates views automatically
@State private var count = 0
var body: some View {
Button("Count: \(count)") { // Dependency on `count`
count += 1 // State change triggers re-render
}
}
Key insight: Views are VALUE TYPES (structs), not long-lived objects. They are descriptions of current UI state, not objects that receive commands over time. SwiftUI maintains the actual UI behind the scenes.
Follow Anytype's property organization from IOS_DEVELOPMENT_GUIDE.md:
struct ExampleView: View {
// 1. Property wrappers (@State, @Injected, @Environment)
@State private var model: ExampleViewModel
@Injected(\.settingsService) private var settingsService
@Environment(\.dismiss) private var dismiss
// 2. Public properties (let/var)
let title: String
// 3. Private properties
private var cancellables = Set<AnyCancellable>()
// 4. Computed properties
private var hasItems: Bool { !model.items.isEmpty }
// 5. init (if needed)
init(title: String) {
self.title = title
_model = State(wrappedValue: ExampleViewModel(title: title))
}
// 6. body
var body: some View {
content
.task { await model.startSubscriptions() }
}
// 7. Computed view builders
private var content: some View { ... }
// 8. Helper / async functions
private func handleTap() { ... }
}
Anytype uses MVVM with ViewModels. Always use ViewModels for business logic:
// View - lightweight, UI only
struct ChatView: View {
@State private var model: ChatViewModel
init(spaceId: String, chatId: String) {
_model = State(wrappedValue: ChatViewModel(spaceId: spaceId, chatId: chatId))
}
var body: some View {
content
.task { await model.startSubscriptions() }
}
private var content: some View {
List(model.messages) { message in
MessageRow(message: message)
}
}
}
// ViewModel - handles business logic
@MainActor
@Observable
final class ChatViewModel {
var messages: [Message] = []
@ObservationIgnored
@Injected(\.chatService) private var chatService
func startSubscriptions() async {
// Heavy work here, not in init
}
func sendMessage(_ text: String) async {
// Business logic
}
}
Key points:
@State private var model: ViewModel in viewsinit with _model = State(wrappedValue:).task@Observable macro (not ObservableObject)@MainActorUnderstanding why @Observable works helps you use it correctly.
Property Access Tracking:
body evaluationbody don't cause re-renders (unlike @Published)@Observable
final class SettingsViewModel {
var userName: String = "" // Accessed in body → triggers update
var isLoading: Bool = false // Accessed in body → triggers update
var analyticsData: Data = Data() // NOT accessed in body → no update
}
struct SettingsView: View {
@State private var model: SettingsViewModel
var body: some View {
// SwiftUI tracks: "this view reads userName and isLoading"
VStack {
Text(model.userName) // ✓ Tracked
if model.isLoading { // ✓ Tracked
ProgressView()
}
// model.analyticsData not read → changes won't invalidate this view
}
}
}
Per-Instance Tracking:
@Observable objects work efficientlyidentifiable tricks with observation@Observable
final class MessageViewModel {
var text: String
var isRead: Bool = false
}
// Each MessageRow only updates when ITS message changes
List(model.messages) { message in
MessageRow(message: message) // Only this row updates when message.isRead changes
}
Computed Properties Just Work:
@Observable
final class CartViewModel {
var items: [Item] = []
var discount: Double = 0
// Computed → tracks both `items` and `discount`
var totalPrice: Double {
items.reduce(0) { $0 + $1.price } - discount
}
}
Performance Benefit:
With @Observable, views only update when properties they actually read change. This is more efficient than ObservableObject where ANY @Published change triggers objectWillChange for ALL subscribers.
When to use which wrapper with @Observable:
| Scenario | Wrapper | Why |
|---|---|---|
| View owns model lifecycle | @State | View creates and manages the model |
| Model shared app-wide | @Environment | Injected at app root, read anywhere |
| Just need bindings ($syntax) | @Bindable | Pass to TextField, Toggle, etc. |
| Just reading the model | Nothing | Direct property access triggers tracking |
// View OWNS the model (creates it)
struct ChatView: View {
@State private var model: ChatViewModel // ← @State
init(chatId: String) {
_model = State(wrappedValue: ChatViewModel(chatId: chatId))
}
}
// Model passed from parent, need bindings
struct MessageEditor: View {
@Bindable var draft: DraftMessage // ← @Bindable for $draft.text
var body: some View {
TextField("Message", text: $draft.text)
}
}
// Just reading, no bindings needed
struct MessageRow: View {
let message: MessageViewModel // ← Nothing! Just read properties
var body: some View {
Text(message.text)
Image(systemName: message.isRead ? "checkmark.circle.fill" : "circle")
}
}
Migration from ObservableObject:
| Old | New |
|---|---|
@StateObject | @State |
@ObservedObject | @Bindable or nothing |
@EnvironmentObject | @Environment |
Step-by-step conversion from legacy ObservableObject:
Before (ObservableObject):
class SettingsViewModel: ObservableObject {
@Published var userName: String = ""
@Published var notifications: Bool = true
private var cancellables = Set<AnyCancellable>()
}
struct SettingsView: View {
@StateObject private var model = SettingsViewModel()
var body: some View {
TextField("Name", text: $model.userName)
Toggle("Notifications", isOn: $model.notifications)
}
}
After (@Observable):
@Observable
final class SettingsViewModel {
var userName: String = ""
var notifications: Bool = true
@ObservationIgnored
private var cancellables = Set<AnyCancellable>()
}
struct SettingsView: View {
@State private var model = SettingsViewModel()
var body: some View {
@Bindable var model = model // Local binding for $ syntax
TextField("Name", text: $model.userName)
Toggle("Notifications", isOn: $model.notifications)
}
}
Migration Steps:
ObservableObject conformance, add @Observable macro@Published from all properties (observation is automatic)@ObservationIgnored to properties that shouldn't trigger updates@StateObject → @State in views$ binding syntax, use @Bindable var model = model in body@EnvironmentObject with @EnvironmentNote: Anytype already uses @Observable - this section is for understanding legacy code during migrations.
Anytype uses Factory DI, not SwiftUI Environment for services:
// ✅ CORRECT - Factory DI
@Injected(\.chatService) private var chatService
// ❌ WRONG - Environment for services
@Environment(ChatService.self) private var chatService
Environment is for:
@Environment(\.dismiss), @Environment(\.colorScheme)@Injected is for:
@Injected(\.chatService)@Injected(\.userRepository)View modifiers create a hierarchical structure. Order matters - modifiers are applied sequentially:
// Each modifier wraps the previous result
Image("whiskers")
.clipShape(Circle()) // 1. Clip to circle first
.shadow(radius: 4) // 2. Add shadow to clipped shape
.overlay( // 3. Overlay on top of shadow
Circle().stroke(.green, lineWidth: 2)
)
The hierarchy and order of effect is defined by the exact order of modifiers. Chaining modifiers makes it clear how results are produced and how to customize them.
SwiftUI views describe purpose, not exact visual construction. This enables adaptation:
Buttons - Same purpose (labeled action), different contexts:
// Adapts to: borderless, bordered, prominent styles
// Adapts to: swipe actions, menus, forms
Button("Edit", action: handleEdit)
// In swipe actions
.swipeActions {
Button("Delete", role: .destructive) { delete() }
Button("Archive") { archive() }
}
Toggles - Switch, checkbox, or toggle button depending on context:
// Automatically shows appropriate style for platform/context
Toggle("Notifications", isOn: $notificationsEnabled)
Searchable - Describes capability, SwiftUI handles idiomatic presentation:
// iOS: overlay list, macOS: dropdown menu
List(filteredItems) { ... }
.searchable(text: $searchText)
.searchSuggestions {
ForEach(suggestions) { Text($0) }
}
If body grows beyond a screen, split into smaller subviews:
// Computed view properties (same file)
var body: some View {
List {
header
filters
results
}
}
private var header: some View { ... }
private var filters: some View { ... }
private var results: some View { ... }
// Extracted subview (reusable or complex)
struct HeaderSection: View {
let title: String
let subtitle: String?
var body: some View {
VStack(alignment: .leading, spacing: 4) {
AnytypeText(title, style: .heading)
if let subtitle {
AnytypeText(subtitle, style: .bodyRegular)
}
}
}
}
For views with loading/error/loaded states:
enum ViewState {
case loading
case error(String)
case loaded
}
@MainActor
@Observable
final class FeedViewModel {
var viewState: ViewState = .loading
var posts: [Post] = []
func loadPosts() async {
do {
posts = try await feedService.getFeed()
viewState = .loaded
} catch {
viewState = .error(error.localizedDescription)
}
}
}
struct FeedView: View {
@State private var model: FeedViewModel
var body: some View {
content
.task { await model.loadPosts() }
}
@ViewBuilder
private var content: some View {
switch model.viewState {
case .loading:
ProgressView()
case .error(let message):
ErrorView(message: message, retry: { Task { await model.loadPosts() } })
case .loaded:
List(model.posts) { post in
PostRow(post: post)
}
}
}
}
@State creates internal source of data for a view:
struct RatingView: View {
@State private var rating = 0 // View owns this state
var body: some View {
HStack {
Text("\(rating)")
Button("+") { rating += 1 }
Button("-") { rating -= 1 }
}
}
}
@Binding creates two-way reference to state owned elsewhere:
struct RatingContainerView: View {
@State private var rating = 0 // Single source of truth
var body: some View {
VStack {
Gauge(value: Double(rating), in: 0...10) {}
RatingEditor(rating: $rating) // Pass binding
}
}
}
struct RatingEditor: View {
@Binding var rating: Int // Two-way reference to container's state
var body: some View {
Button("+") { rating += 1 } // Updates container's state
}
}
Key principle: One source of truth. When multiple views need the same data, lift state up to common ancestor and pass bindings down.
Wrap state changes with withAnimation to animate resulting view updates:
Button("Rate") {
withAnimation {
rating += 1 // State change inside animation block
}
}
Customize transitions for specific views:
Text("\(rating)")
.contentTransition(.numericText()) // Smooth number transition
Animations in SwiftUI build on the same data-driven updates - when state changes, views update, and withAnimation makes those updates animate.
// Initial load
.task {
await model.startSubscriptions()
}
// React to state changes
.task(id: searchText) {
guard !searchText.isEmpty else { return }
await model.search(query: searchText)
}
.onChange(of: selectedTab) { oldValue, newValue in
// Handle tab change
}
When file exceeds ~300 lines:
struct LargeView: View {
// Properties and body here
}
// MARK: - Subviews
private extension LargeView {
var header: some View { ... }
var content: some View { ... }
}
// MARK: - Actions
private extension LargeView {
func loadData() async { ... }
func handleTap() { ... }
}
SwiftUI provides seamless interop with UIKit and AppKit - no expectation that an app needs to be entirely SwiftUI.
Embed UIKit in SwiftUI - Use UIViewRepresentable:
struct MapView: UIViewRepresentable {
func makeUIView(context: Context) -> MKMapView {
MKMapView()
}
func updateUIView(_ uiView: MKMapView, context: Context) {
// Update the view with new data
}
}
// Use like any SwiftUI view
var body: some View {
VStack {
MapView()
Button("Center") { ... }
}
}
Embed SwiftUI in UIKit - Use UIHostingController:
// In a UIKit view controller
let swiftUIView = ProfileView(user: user)
let hostingController = UIHostingController(rootView: swiftUIView)
addChild(hostingController)
view.addSubview(hostingController.view)
Incremental Adoption Philosophy: Apple's own apps use these tools to adopt SwiftUI incrementally - whether bringing SwiftUI into existing apps or incorporating UIKit views into new SwiftUI apps. All are valid approaches.
// ❌ WRONG - Environment for app services
@Environment(FeedService.self) private var feedService
// ✅ CORRECT - Factory DI
@Injected(\.feedService) private var feedService
// ❌ WRONG - Business logic in view
struct FeedView: View {
@State private var posts: [Post] = []
private func loadPosts() async {
// Business logic directly in view
}
}
// ✅ CORRECT - ViewModel handles business logic
struct FeedView: View {
@State private var model: FeedViewModel
// ViewModel handles loadPosts()
}
// ❌ WRONG - onAppear can fire multiple times
var body: some View {
Group {
if model.isLoading {
ProgressView()
} else {
content
}
}
.onAppear { model.onAppear() }
}
// ✅ CORRECT - Use @ViewBuilder
var body: some View {
loadingContent
.onAppear { model.onAppear() }
}
@ViewBuilder
private var loadingContent: some View {
if model.isLoading {
ProgressView()
} else {
content
}
}
Navigation: This skill provides SwiftUI structure patterns. For full architecture guidance, see IOS_DEVELOPMENT_GUIDE.md.
Attribution: View structure patterns adapted from Dimillian/Skills, aligned with Anytype MVVM architecture. WWDC24 insights from "SwiftUI Essentials" session.