13. SwiftUI Standards
SwiftUI-first, Golden Gate only
Fichero’s frontend is SwiftUI-first, targeting macOS 26 only — no back-deployment, no if #available guards; adopt current APIs directly. Default to SwiftUI for everything; drop to AppKit only for a documented gap, behind a contained NSViewRepresentable / NSViewControllerRepresentable bridge (below). Read from services/HTTP — never local file paths; the engine may be remote.
State management: Observation-first
For ANY new view-model or store:
@Observable(Observation framework) — NOTObservableObject/@Published/ Combine. The pre-2026@StateObject/ObservableObjectpattern is obsolete for new code.@Stateto own an@Observable;@Bindablefor two-way bindings;@Environment(Type.self)for dependency injection. Use lazy@Stateinit when construction is costly.- Migrate existing view-local
ObservableObjectview-models to@Observableas you touch each surface, updating consumers from@StateObject/@ObservedObjectto@State/@Bindable. Exception: the app-wide god objects (DocumentStore,LibraryManager,AppState, and widely-consumed service wrappers) have huge blast radius — migrate them only in a deliberate, dedicated pass. - Replace
NotificationCenter-as-mutation-bus with an observed store. Posting notifications to fan mutations across views is the anti-pattern. - State lives in stores/services injected through the environment; views stay thin, render, and collect input — they never call the API directly. Stores are the only endpoint accessors, and they update one item in place rather than re-rendering whole lists.
Modern SwiftUI to prefer: native List/Section selection (NSTableView underneath → free macOS selection emphasis and context-menu target ring), \.appearsActive / \.isEmphasized for focus loss, List/Grid/Section content-reordering APIs, toolbar visibility-priority and auto-minimizing, AsyncImage caching, Liquid Glass / .regularMaterial for chrome, SF Symbols for glyphs, and swift-collections (OrderedSet / OrderedDictionary) where ordering/dedup matters.
Swift 6 concurrency (mandatory)
Strict concurrency checking is on.
@MainActorfor UI-related classes; the actor provides serialization — never add dispatch queues inside a@MainActorclass, and neverDispatchQueue.main.asyncanywhere.- From non-isolated closures, hop with
Task { @MainActor in ... }. - Every
.task {}block checksTask.isCancelled— otherwise work keeps running after the view disappears. - Types that implement their own thread safety (a lock) conform to
@unchecked Sendable. - No concurrency warnings in the build.
File, size, and naming rules
- File size: 400 lines recommended limit; 1,000 lines hard limit (requires split). Type bodies \< 250 lines; functions \< 50 lines; cyclomatic complexity \< 10; lines \< 120 characters.
- Split by component (extract sub-views), by responsibility, or by feature.
- Files: PascalCase, descriptive (
DocumentRow, notRow), suffixed by purpose where useful (…View,…Model,…Service). - Variables: descriptive camelCase — never
x,y,i,dx. Functions: verb-first (loadDocument(),handleFileDropOnLibrary()). - Standard in-file order: imports, logger, properties, body, subviews as
@ViewBuildercomputed properties, actions, helpers, supporting types, preview. - OSLog only — a
Logger(subsystem: "com.tubb.Fichero", category: …)per file; neverNSLogorprint. - Semantic system fonts (
.title,.body) — never.system(size:). - Cache expensive computations in
@Stateand rebuild on.onChange; never rebuild hierarchies or create service objects insidebody. - Use
@FocusedValuefor menu commands, neverNotificationCenter. - SwiftLint is required before committing:
swiftlint lint fichero/fichero/(auto-fix whitespace withswiftlint --fix --format). - New
.swiftfiles must be registered:ruby scripts/add-swift-file.rb <path>(chapter 10).
AppKit interop policy
Two sanctioned reasons to bridge, both with the same containment discipline (isolate behind an NSViewRepresentable, documented, never AppKit sprinkled through view code):
- Capability gap — SwiftUI literally cannot do it. The ~8 shipped bridges: PDFKit rendering and zoom, the image magnifier / cursor tracking, scroll-wheel zoom, Quick Look, rich/plain-text editors, and an
NSEventswipe monitor. - Behavioral-fidelity gap — SwiftUI renders it but cannot match a decades-old Mac interaction a power user feels the absence of: selection emphasis on focus loss, the context-menu target focus ring, drag-session visibility, type-in-search with arrow-through-results, precise toolbar placement across a three-pane split.
A fidelity bridge folds into the existing list/inspector/reading stack — no parallel AppKit inspector. If List already gives the behavior, use List. Before any new bridge: check current Apple documentation for a native SwiftUI answer first (SwiftUI 2026 closed several old gaps natively), then bridge only a confirmed gap and add it to the sanctioned list. A bridge changes presentation/interaction, never business logic; Swift 6 concurrency rules apply inside it.
Control choice per surface: List for single-column item lists (the inspector’s entities/claims/notes/annotations, sidebar nav) — it supports multi-select, hierarchy (Section/OutlineGroup), and drag-to-reorder; Table for multi-column sortable tabular data (the library browser); a bridged NSOutlineView/NSTableView only if those cannot express a needed combination. Swipe actions are not Mac-normal — on macOS, row actions go in the context menu, toolbar, and keyboard, never a swipe. Editing is navigation, not modal: inline TextField for rename, or push a detail/edit view inside the inspector with a Back button; confirmations may be alerts, editing never is.
Before declaring SwiftUI work done, the three-leg check in order: swiftlint clean, the build succeeds, the tests pass. A build log alone is not done; a green test run alone is not done.