Design systems — collections of components that can be reused across applications — are important for making apps consistent even when developed by large teams. However, building and maintaining a design system can be really difficult. Code needs to be clear and easy for everyone to understand. Styles also need to stay the same without limiting customization options. On top of that, systems must adapt to changing requirements over time.
On iOS development, compared with UIKit, SwiftUI uses declarative syntax, which means the code directly corresponds to the visual appearance. This makes the code self-documenting. SwiftUI also provides built-in tools that ensure consistent application of styles without sacrificing flexibility.
In this post, we'll look at how SwiftUI's declarative syntax, built-in consistency features, and other qualities streamline the process of building a design system. The goal is to show how SwiftUI aligns well with best practices for maintainable, complex design systems at scale.
Declarative syntax will be more readable and easier to implement
First, in terms of implementation and maintenance costs, SwiftUI is different from Apple's existing UIKit and AppKit. SwiftUI adopts declarative syntax to build UI. Because the declarative syntax focuses more on describing the final effect of the UI than the specific implementation, the UI components written by declarative syntax are more readable, which helps teams better collaborate to implement a design system.
// SwiftUI
VStack(spacing: 20) {
Text("Hello")
.font(.largeTitle)
Text("World")
.font(.largeTitle)
}
.foregroundColor(.blue)
// UIKit
let stackView = UIStackView()
stackView.axis = .vertical
stackView.spacing = 20
let helloLabel = UILabel()
helloLabel.text = "Hello"
helloLabel.font = UIFont.systemFont(ofSize: 34)
stackView.addArrangedSubview(helloLabel)
let worldLabel = UILabel()
worldLabel.text = "World"
worldLabel.font = UIFont.systemFont(ofSize: 34)
stackView.addArrangedSubview(worldLabel)
stackView.backgroundColor = .blue
Besides the difference in the amount of code, we can also feel the declarative syntax code directly corresponds to the visual appearance.
Built-in consistency and uniformity of expression
Through code, we can see the preset design specifications of SwiftUI, which makes it easier for developers to implement and ensure the consistency of a design system. In SwiftUI, we can achieve a unified expression of UI components through View Modifiers.
Yes, developers can also wrap UIKit's UI components to achieve a similar effect to SwiftUI View Modifier. However, SwiftUI's View Modifier is intrinsically coupled and naturally has better consistency. On the contrary, while creating a wrapping function in UIKit, developers still need to conscientiously follow design specifications. Negligence in doing so can inevitably impact the consistency of the final UI.
// SwiftUI
Button("Primary") {
// action
}
.buttonStyle(PrimaryButtonStyle())
Button("Secondary") {
// action
}
.buttonStyle(SecondaryButtonStyle()) // When the Primary or Secondary style changes, all buttons using these two styles will be automatically updated.
// UIKit
let primaryButton = UIButton()
primaryButton.setTitle("Primary", for: .normal)
primaryButton.titleLabel?.font = UIFont.systemFont(ofSize: 16)
primaryButton.backgroundColor = UIColor.blue
primaryButton.layer.cornerRadius = 5
let secondaryButton = UIButton()
secondaryButton.setTitle("Secondary", for: .normal)
secondaryButton.titleLabel?.font = UIFont.systemFont(ofSize: 16)
secondaryButton.backgroundColor = UIColor.gray
secondaryButton.layer.cornerRadius = 5
Predictability brought by unidirectional data flow
The UI components written in SwiftUI are more predictable. This is reflected in its adherence to the principle of unidirectional data flow, compared to UIKit's various techniques and patterns like delegation, notifications, or target-action for coordinating state updates between different components. Unidirectional data flow can reduce UI errors caused by the complexity of mutable state management. At the same time, the styles of UI components are created through higher-order functions or combinations, rather than through side effects or implicit rules, making it easier for developers to identify which styles the component will have and where they come from.
Compared to UIKit as an imperativeUI framework, its UI rendering process is relatively complex and unpredictable.
State changes can lead to hard-to-track side effects, making the UI more prone to errors and difficult to debug.
Style rules are defined and applied by developers themselves, with a large degree of discretion, making it more difficult to read and understand.
The source and application of styles are not as clear as SwiftUI, and developers need to clarify the relationship between various style rules to ensure the correctness of the UI.
Modular components mirror design system principles
The modular components and functional programming approach advocated in SwiftUI is consistent with the principles of atomic components and combinations in Design System.
Both tend to build small, self-contained units, and then combine them to create more complex structures. Views and Modifiers in SwiftUI are an example of such units and combinations.
Components and functions can be either general or specific, reusable or composable. In SwiftUI, we can build general Modifiers and Views, as well as custom Modifiers for a specific View.
When changing the base unit (such as font size, spacing, etc.), all combinations will be affected in a controllable way. If we change the behavior or attribute of a Modifier, all views that use the Modifier will change accordingly. This makes it easier to predict and control the scope of adjustments when fine-tuning the design system.
In contrast, UIKit — as an imperative UI framework — is relatively scattered and casual in how it constructs a UI:
When building a UI component, developers decide for themselves which controls to assemble and which properties to set, making it difficult to follow a unified composition rule.
Without a similar mechanism to View Modifier, the style of each view needs to be set separately, so it cannot be reused in multiple places. This makes it difficult to efficiently apply design changes when adjusting the style, as each view needs to be modified individually.
The change of attributes does not necessarily affect all related views, and developers need to make modifications to the views of the application one by one. This increases the difficulty of maintaining the design system.
Evaluating SwiftUI and UIKit
As we’ve seen, SwiftUI has a number of advantages over UIKit when implementing a Design System for mobile platforms. However, it’s worth noting that in some complex UX scenarios that require smooth animation and stable frame rates, UIKit — which is an imperative UI framework — may be more suitable. Also, because the state and behavior of SwiftUI components are data-driven, it can lead to performance issues such as a large number of data bindings and updates, which will cause UI stuttering and delays. In contrast, UIKit is close to the underlying rendering mechanism and may have better performance. However, it is easier to implement cross-platform requirements using SwiftUI than UIKit.
So, if you’re pursuing a performance edge or want high levels of customization, UIKit's flexibility and performance may be advantageous. However, in most cases, SwiftUI's declarative syntax and composition features make it naturally more suitable for building a unified and maintainable design system.
Disclaimer: The statements and opinions expressed in this article are those of the author(s) and do not necessarily reflect the positions of Thoughtworks.