I'm heavily involved in making reusable components for specific features, as well as generic ones as part of a design system / component library.
Does Apple have any guidance doing this? There is a lot to be learned from the decisions around the 'style' APIs, as well as the overloaded constrained initializers of views like Label. The design system should be opinionated, but allow a degree of flexibility.
I liked Sarah's talk about incorporating brand which is certainly important when building components for 10+ apps, internal and external. The focus on purposeful design, but there isn't much at a technical level for designing (coding) reusable components and embracing strategies using the Environment, making custom EnvironmentValues, leveraging built-in system constructs, etc.
Any resources and guidance here would be welcomed!
A few WWDC sessions worth watching if you haven't already:
- "The craft of SwiftUI API design: Progressive disclosure" (WWDC22, Matt Ricketson)
- "Demystify SwiftUI containers" (WWDC24) — covers
ContainerValues - "Compose custom layouts with SwiftUI" (WWDC22)
- "Demystify SwiftUI" (WWDC21) — for the identity model that underpins environment-based theming
Swift Charts is a useful reference for API design: it combines a result-builder DSL, constrained ...Mark initializers, and a family of ...Style protocols that compose through the environment.
The most important principle: design the call site first, then work backward to the implementation.
A few patterns that tend to produce reusable, evolvable components:
-
Style protocol. Define
protocol BadgeStyle { func makeBody(configuration:) -> some View }with aConfigurationthat exposes only semantic inputs. Ship.automatic, two or three named built-ins, and the protocol itself. Propagate via the environment so.badgeStyle(.prominent)cascades down a subtree. -
Progressive-disclosure initializers (the
Labelpattern). A single generic struct, plus constrained extension whereHeader == Textoverloads for the common cases. Convenience for the most cases; the fully generic init covers the rest. -
Environmentas the public configuration surface. Anything an ancestor might want to override belongs in the environment, not in initializer parameters. With the@Entrythe client side looks like this:
@Entry var brandTone: BrandTone = .neutral
-
Pair it with a
.brandTone(_:)modifier and you have a theming surface that scales across apps. -
Container values for components that take arbitrary content and need per-subview metadata (priority, badge, section). Set with
containerValue(\.cardPriority, .high)and read inside aForEach(subviews:)viasubview.containerValues.cardPriority. -
PreferenceKeyfor child-to-parent data and configuration flow. Environment for the opposite direction.
Julia V