In this article, we’ll see an example of designing a flexible system for custom views with customizable styles using protocols, environment keys, and view modifiers.
Goal:
Writing different styles that can be applied to a card view containing only text. We have followed the practices that built-in styling in SwiftUI follows for button styles, menu styles, progress styles, etc.
Steps:
- Create a View Style Protocol
- Create a Style Configuration
- Define New Styles
- Setup the Style Environment
- Add a View Modifier for Styles
- Create a Card View
- Preview with New Styles
1. Create a View Style Protocol
The foundation of our approach is a CardStyle
protocol that defines how to construct a styled card. Here’s the protocol:
protocol CardStyle {
associatedtype Body: View
typealias Configuration = CardStyleConfiguration
func makeBody(configuration: Self.Configuration) -> Self.Body
}
The protocol uses an associated type to allow for flexibility in the returned View
. The makeBody(configuration:)
method generates the styled view based on the provided configuration.
2. Create a Style Configuration
To enable flexibility, we define a CardStyleConfiguration
. It encapsulates the content of the card:
struct CardStyleConfiguration {
struct Label: View {
init<Content: View>(content: Content) {
body = AnyView(content)
}
var body: AnyView
}
let label: CardStyleConfiguration.Label
}
The Label
struct wraps the card’s content, enabling any view to be used as the label.
3. Define New Styles
Using the CardStyle
protocol, we define several styles:
Wave Card Style
struct WaveCardStyle: CardStyle {
var color: Color
func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(.headline)
.padding()
.background(
ZStack {
color.opacity(0.6).blur(radius: 10)
color.opacity(0.4)
.clipShape(RoundedRectangle(cornerRadius: 30))
}
)
.clipShape(RoundedRectangle(cornerRadius: 20))
}
}
Dotted Border Style
struct DottedBorderStyle: CardStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(.body)
.padding()
.background(
RoundedRectangle(cornerRadius: 16)
.strokeBorder(style: StrokeStyle(lineWidth: 2, dash: [5]))
.foregroundColor(.blue)
)
}
}
Default Style
struct DefaultCardStyle: CardStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(.title2)
.padding()
.background(Color.gray.opacity(0.2))
.cornerRadius(12)
}
}
4. Setup the Style Environment
To make the styles dynamic, we use an environment key:
struct AnyCardStyle: CardStyle {
private var _makeBody: (Configuration) -> AnyView
init<S: CardStyle>(style: S) {
_makeBody = { configuration in
AnyView(style.makeBody(configuration: configuration))
}
}
func makeBody(configuration: Configuration) -> some View {
_makeBody(configuration)
}
}
struct CardStyleKey: EnvironmentKey {
static let defaultValue = AnyCardStyle(style: DefaultCardStyle())
}
extension EnvironmentValues {
var cardStyle: AnyCardStyle {
get { self[CardStyleKey.self] }
set { self[CardStyleKey.self] = newValue }
}
}
5. Add a View Modifier for Styles
The following modifier simplifies applying a custom style to any card:
extension View {
func cardStyle<S: CardStyle>(_ style: S) -> some View {
environment(\.[cardStyle], AnyCardStyle(style: style))
}
}
6. Create a Card View
The Card
view dynamically adopts the style from the environment:
struct Card<Content: View>: View {
@Environment(\.cardStyle) private var style
var content: () -> Content
var body: some View {
style.makeBody(configuration: CardStyleConfiguration(label: CardStyleConfiguration.Label(content: content())))
}
}
7. Preview with New Styles
Here’s how to use the custom styles in a sample view:
struct ContentView: View {
var body: some View {
VStack {
Card {
Text("Wave Style")
}
.cardStyle(WaveCardStyle(color: .blue))
Card {
Text("Dotted Border Style")
}
.cardStyle(DottedBorderStyle())
Card {
Text("Default Style")
}
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I hope you found it helpful. Thank you!