Styling Custom Views in SwiftUI

Yasir
3 min readJan 6, 2025

--

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:

  1. Create a View Style Protocol
  2. Create a Style Configuration
  3. Define New Styles
  4. Setup the Style Environment
  5. Add a View Modifier for Styles
  6. Create a Card View
  7. 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!

--

--

Yasir
Yasir

Written by Yasir

Exploring Swift, SwiftUI, and Apple Universe.

No responses yet