Official guidance and documentation for creating reusable components and design systems

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!

Answered by Frameworks Engineer in 892442022

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:

  1. Style protocol. Define protocol BadgeStyle { func makeBody(configuration:) -> some View } with a Configuration that 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.

  2. Progressive-disclosure initializers (the Label pattern). A single generic struct, plus constrained extension where Header == Text overloads for the common cases. Convenience for the most cases; the fully generic init covers the rest.

  3. Environment as the public configuration surface. Anything an ancestor might want to override belongs in the environment, not in initializer parameters. With the @Entry the client side looks like this:

@Entry var brandTone: BrandTone = .neutral
  1. Pair it with a .brandTone(_:) modifier and you have a theming surface that scales across apps.

  2. Container values for components that take arbitrary content and need per-subview metadata (priority, badge, section). Set with containerValue(\.cardPriority, .high) and read inside a ForEach(subviews:) via subview.containerValues.cardPriority.

  3. PreferenceKey for child-to-parent data and configuration flow. Environment for the opposite direction.

Julia V

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:

  1. Style protocol. Define protocol BadgeStyle { func makeBody(configuration:) -> some View } with a Configuration that 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.

  2. Progressive-disclosure initializers (the Label pattern). A single generic struct, plus constrained extension where Header == Text overloads for the common cases. Convenience for the most cases; the fully generic init covers the rest.

  3. Environment as the public configuration surface. Anything an ancestor might want to override belongs in the environment, not in initializer parameters. With the @Entry the client side looks like this:

@Entry var brandTone: BrandTone = .neutral
  1. Pair it with a .brandTone(_:) modifier and you have a theming surface that scales across apps.

  2. Container values for components that take arbitrary content and need per-subview metadata (priority, badge, section). Set with containerValue(\.cardPriority, .high) and read inside a ForEach(subviews:) via subview.containerValues.cardPriority.

  3. PreferenceKey for child-to-parent data and configuration flow. Environment for the opposite direction.

Julia V

Official guidance and documentation for creating reusable components and design systems
 
 
Q