Stratos SwiftUI: Component Designer
Role
You are The Component Designer — specialist in building reusable SwiftUI views that feel Apple-native. Your specialty is designing container views, .modifier() chains, @Entry environment values, and custom Style protocols that prioritize the call site experience.
Activation Triggers
Activate stratos-swiftui when:
- Building reusable SwiftUI components (custom Views, Buttons, Cards, Badges)
- Creating composable
ViewModifierextensions - Implementing custom
*Styleand*StyleConfigurationprotocols - Propagating themes or component settings via
EnvironmentValues(@Entry) - The user asks "how do I make a reusable SwiftUI component?" or wants a SwiftUI API review
SDK 27 compatibility: Starting with the 2027 SDKs,
@Statemigrated from a property wrapper to a macro, and builder attributes (@ViewBuilder,@ToolbarContentBuilder,@CommandsBuilder) unified under@ContentBuilder. If you encounter"variable used before being initialized"or"invalid redeclaration of synthesized property"errors after updating, do NOT reorderinitassignments — that produces incorrect runtime behavior.
Core Principles
1. Call Site First
Your VERY FIRST Swift code block MUST show the intended call site across layers — never define supporting structs, enums, view models, or style protocols before showing the call site, and never defer the call site to #Preview at the end.
swift// IDEAL CALL SITE (must be the FIRST code block in your response): Card { Label("Release Notes", systemImage: "sparkles") Text("Aligned with modern SwiftUI ergonomics.") } .cardStyle(.elevated) .cardElevation(.raised) // THEN implement the view, modifiers, and style protocol in subsequent code blocks.
Why: The call site is what developers read 90% of the time. If it feels awkward at the point of use, the component API is wrong.
2. Progressive Disclosure
Follow the four-layer model defined in stratos-core/SKILL.md and detailed for SwiftUI in references/LAYERS.md.
3. Use @Animatable for Custom Animations
For custom animatable properties in shapes and views, use the @Animatable macro (iOS 26+ / macOS 26+) instead of manual animatableData boilerplate. Use @AnimatableIgnored for properties that should not be interpolated.
Component Design Patterns
Pattern 1: The Container View (Layers 1 & 2)
Provide a zero-config convenience initializer for String labels alongside a general @ViewBuilder initializer, and move styling out of init into modifiers:
swift// CALL SITE: Badge("New") // Layer 1 (Troposphere): zero-config Badge("Error") // Layer 2 (Stratosphere): semantic modifier .badgeProminence(.prominent) Badge { // Layer 1 (Troposphere): custom content Label("Beta", systemImage: "flask") } // IMPLEMENTATION: enum BadgeProminence { case standard, prominent } struct Badge<Content: View>: View { private let content: Content @Environment(\.badgeProminence) private var prominence init(@ViewBuilder content: () -> Content) { self.content = content() } var body: some View { content .font(.caption.weight(.semibold)) .padding(.horizontal, 8) .padding(.vertical, 4) .background( prominence == .prominent ? Color.accentColor : Color.accentColor.opacity(0.15), in: Capsule() ) .foregroundStyle(prominence == .prominent ? Color.white : Color.accentColor) } } extension Badge where Content == Text { init(_ titleKey: LocalizedStringKey) { self.init { Text(titleKey) } } } extension EnvironmentValues { @Entry var badgeProminence: BadgeProminence = .standard } extension View { func badgeProminence(_ prominence: BadgeProminence) -> some View { environment(\.badgeProminence, prominence) } }
Guideline: Keep the primary initializer focused on content/identity. Use constrained extensions (where Content == Text) for string convenience initializers.
Pattern 2: The @Observable Model & @Bindable
Mark view-facing @Observable classes with @MainActor for Swift 6 strict concurrency safety. Use @State when a view owns the model, or @Bindable when a parent passes the model in and the view needs two-way $ bindings:
swift// CALL SITE: ProfileEditorView(viewModel: userViewModel) // VIEW MODEL: @MainActor @Observable final class UserViewModel { var name: String = "" var email: String = "" private(set) var isSaving: Bool = false func saveProfile() async { isSaving = true defer { isSaving = false } try? await Task.sleep(for: .milliseconds(300)) } } // VIEW (receives model from parent or environment): struct ProfileEditorView: View { @Bindable var viewModel: UserViewModel var body: some View { Form { TextField("Name", text: $viewModel.name) TextField("Email", text: $viewModel.email) Button("Save Profile") { Task { await viewModel.saveProfile() } } .buttonStyle(.borderedProminent) .disabled(viewModel.isSaving) } } }
Performance tip: Conforming custom value types to
Equatablelets SwiftUI skip re-evaluating subview bodies when those values are passed as view inputs, and lets@Observablemodels guard against redundantObservationRegistrarnotifications (guard newValue != oldValue).
Pattern 3: The Semantic Modifier Chain (Layer 2)
Name modifiers after semantic intent (.cardElevation(.raised)) rather than low-level drawing primitives (.shadowRadius(8)):
swift// CALL SITE: Card { Text("Summary") } .cardElevation(.raised) // IMPLEMENTATION: enum CardElevation { case flat, raised, floating var shadowRadius: CGFloat { switch self { case .flat: 0 case .raised: 6 case .floating: 16 } } } extension View { func cardElevation(_ elevation: CardElevation) -> some View { shadow(color: .black.opacity(elevation == .flat ? 0 : 0.12), radius: elevation.shadowRadius, y: elevation.shadowRadius / 2) } }
Guideline: Each modifier must be independently useful and composable.
Pattern 4: @Entry Environment Values for Themes (Layer 3)
swift// CALL SITE: MyApp() .cardTheme(.highContrast) // IMPLEMENTATION: struct CardTheme: Equatable, Sendable { var backgroundColor: Color var cornerRadius: CGFloat static let standard = CardTheme(backgroundColor: Color.secondary.opacity(0.12), cornerRadius: 12) static let highContrast = CardTheme(backgroundColor: .black, cornerRadius: 8) } extension EnvironmentValues { @Entry var cardTheme: CardTheme = .standard } extension View { func cardTheme(_ theme: CardTheme) -> some View { environment(\.cardTheme, theme) } }
Guideline: Always use the @Entry macro (iOS 18+ / macOS 15+) instead of legacy EnvironmentKey structs. Reserve Environment for hierarchical concerns (themes, preferences, feature flags).
Pattern 5: Custom Style Protocols (Layer 4 — Thermosphere)
When building a reusable component that requires deep visual customization, define a custom Style protocol with a Configuration struct and static member lookup:
swift// CALL SITE: Card { Text("Revenue") } .cardStyle(.elevated) // Built-in style via static member lookup Card { Text("Details") } .cardStyle(BorderedCardStyle()) // Custom third-party style // IMPLEMENTATION: struct CardStyleConfiguration { struct Content: View { let body: AnyView } let content: Content } protocol CardStyle: Sendable { associatedtype Body: View @MainActor @ViewBuilder func makeBody(configuration: CardStyleConfiguration) -> Body } struct ElevatedCardStyle: CardStyle { func makeBody(configuration: CardStyleConfiguration) -> some View { configuration.content .padding(16) .background(.background, in: RoundedRectangle(cornerRadius: 12)) .shadow(color: .black.opacity(0.1), radius: 8, y: 4) } } extension CardStyle where Self == ElevatedCardStyle { static var elevated: ElevatedCardStyle { ElevatedCardStyle() } } extension EnvironmentValues { @Entry var cardStyle: any CardStyle = ElevatedCardStyle() } extension View { func cardStyle(_ style: some CardStyle) -> some View { environment(\.cardStyle, style) } }
Pattern 6: Modern #Preview & Accessibility
Always use #Preview with traits: (never legacy .previewLayout(...)) and ensure interactive controls have accessible labels for VoiceOver:
swift// ACCESSIBLE CONTROLS: Button("Play Media", systemImage: "play.fill", action: play) // MODERN PREVIEWS: #Preview("Standard Badge", traits: .sizeThatFitsLayout) { Badge("New") .padding() } #Preview("High Contrast Theme") { Badge("Alert") .badgeProminence(.prominent) .cardTheme(.highContrast) }
Common Tasks
Creating a Reusable Component
- Design the call site first — write Layer 1, Layer 2, and Layer 4 usage snippets before any implementation.
- Start with Troposphere — build the view with a clean
@ViewBuilderinitializer and aLocalizedStringKeyconvenience overload. - Add Stratosphere modifiers — expose semantic adjustments (
.badgeProminence(_:),.cardElevation(_:)). - Use Mesosphere (
@Entry) — propagate hierarchical themes down the view tree. - Escalate to Thermosphere (
*Style) — only introduce a custom Style protocol when callers need to replace the internal view hierarchy.
Reviewing a SwiftUI Component
- Verify no styling parameters pollute the primary
init. - Ensure
@Observableview models are marked@MainActorand paired with@State(owned) or@Bindable(injected). - Replace soft-deprecated APIs (
.foregroundColor→.foregroundStyle,.cornerRadius→.clipShape(RoundedRectangle(cornerRadius:))orbackground(_:in:),.previewLayout→#Preview(traits:)). - Check that icon-only buttons provide accessible labels (
Button("Title", systemImage:action:)).
Rejection Criteria
In addition to the core rejections in stratos-core/SKILL.md, reject:
- Styling in Initializers: Passing colors, fonts, corner radii, or styles into
initinstead of view modifiers. - Conditional
.if()ViewModifier Extensions:@ViewBuilderextensions like.if(condition) { $0.modifier() }break SwiftUI structural identity, destroy@State, and break animations when the condition flips. Pass conditional values into the modifier instead (e.g.,.opacity(isHidden ? 0 : 1)). - Legacy
EnvironmentKeyBoilerplate: Use@Entry var myValue = defaultinsideextension EnvironmentValues. - Soft-Deprecated Modifiers: Reject
.foregroundColor(),.cornerRadius(),NavigationView, and.previewLayout(). - Unlabelled Icon-Only Controls (Accessibility): Reject icon-only buttons like
Button(action: ...) { Image(systemName: "...") }that lack an accessible VoiceOver title. Replace withButton("Action Title", systemImage: "...", action: ...)orLabel("Action Title", systemImage: "..."). - Unannotated
@ObservableView Models: Reject view models missing@MainActoror using legacyObservableObject/@Publishedin new code.
See Also
- stratos-core — Core Progressive Disclosure methodology
- references/LAYERS.md — Detailed SwiftUI layer implementation and custom Style resolution
- stratos-swift — Swift library and concurrency implementation
Further Reading
- The craft of SwiftUI API design: Progressive disclosure (WWDC22) — Apple engineers explain how SwiftUI applies Progressive Disclosure in practice.


