ViewModifier personalizzati in SwiftUI: pattern riutilizzabili per Design System
Costruisci ViewModifier personalizzati in SwiftUI per un design system coerente. Pattern, best practice ed esempi pratici per stilizzare le view iOS in modo efficiente.

I ViewModifier costituiscono la base di un design system SwiftUI robusto e manutenibile. Incapsulando stili e comportamenti in componenti riutilizzabili, garantiscono coerenza visiva e riducono drasticamente la duplicazione di codice. Questo approccio modulare trasforma il modo in cui vengono costruite le interfacce iOS moderne.
Questo articolo presenta i pattern essenziali per creare ViewModifier personalizzati efficaci, dalla struttura di base alle composizioni avanzate per un design system completo.
Anatomia di un ViewModifier
Un ViewModifier è un protocollo che definisce una trasformazione applicabile a qualsiasi view. A differenza delle estensioni di View, i ViewModifier possono mantenere uno stato interno e accettare parametri configurabili.
import SwiftUI
// Basic structure of a custom ViewModifier
struct CardModifier: ViewModifier {
// Configurable parameters
var cornerRadius: CGFloat = 12
var shadowRadius: CGFloat = 4
// The body method applies transformations
func body(content: Content) -> some View {
content
.padding()
.background(Color(.systemBackground))
.cornerRadius(cornerRadius)
.shadow(
color: .black.opacity(0.1),
radius: shadowRadius,
x: 0,
y: 2
)
}
}
// Extension for fluent syntax
extension View {
func cardStyle(
cornerRadius: CGFloat = 12,
shadowRadius: CGFloat = 4
) -> some View {
modifier(CardModifier(
cornerRadius: cornerRadius,
shadowRadius: shadowRadius
))
}
}Il pattern Content rappresenta la view a cui il modifier si applica. Questa astrazione permette allo stesso modifier di funzionare su qualsiasi tipo di view SwiftUI.
Usare il CardModifier nella pratica
Una volta creato, applicare il modifier diventa intuitivo e si legge naturalmente nel codice:
struct ProductCard: View {
let product: Product
var body: some View {
VStack(alignment: .leading, spacing: 8) {
// Product image
AsyncImage(url: product.imageURL) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
Rectangle()
.fill(Color.gray.opacity(0.2))
}
.frame(height: 150)
.clipped()
// Product information
Text(product.name)
.font(.headline)
Text(product.price, format: .currency(code: "USD"))
.foregroundStyle(.secondary)
}
.cardStyle() // Applying the modifier
}
}
struct Product: Identifiable {
let id: UUID
let name: String
let price: Decimal
let imageURL: URL?
}La chiarezza del codice migliora notevolmente la manutenibilità. Modificare lo stile di tutte le card dell'applicazione richiede una sola modifica al ViewModifier.
Modifier condizionali con @ViewBuilder
I ViewModifier possono includere logica condizionale per adattare il loro comportamento al contesto. L'attributo @ViewBuilder consente di costruire view complesse con ramificazioni.
struct BadgeModifier: ViewModifier {
let count: Int
let color: Color
let showZero: Bool
init(count: Int, color: Color = .red, showZero: Bool = false) {
self.count = count
self.color = color
self.showZero = showZero
}
// @ViewBuilder enables conditions in the body
@ViewBuilder
func body(content: Content) -> some View {
if count > 0 || showZero {
content.overlay(alignment: .topTrailing) {
badgeView
}
} else {
// Returns the view without modification
content
}
}
// Badge view extracted for readability
private var badgeView: some View {
Text(count > 99 ? "99+" : "\(count)")
.font(.caption2.bold())
.foregroundStyle(.white)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(color)
.clipShape(Capsule())
.offset(x: 8, y: -8)
}
}
extension View {
func badge(
count: Int,
color: Color = .red,
showZero: Bool = false
) -> some View {
modifier(BadgeModifier(
count: count,
color: color,
showZero: showZero
))
}
}Questo pattern consente di aggiungere badge di notifica a qualsiasi elemento dell'interfaccia in modo dichiarativo e configurabile.
SwiftUI ottimizza automaticamente i rami @ViewBuilder. La view non visualizzata non viene renderizzata, preservando le prestazioni anche con condizioni complesse.
Design System con token di stile
Un design system maturo si basa sui token: costanti semantiche che definiscono l'identità visiva dell'applicazione. I ViewModifier incapsulano perfettamente questi token.
import SwiftUI
// Spacing tokens
enum Spacing {
static let xs: CGFloat = 4
static let sm: CGFloat = 8
static let md: CGFloat = 16
static let lg: CGFloat = 24
static let xl: CGFloat = 32
}
// Radius tokens
enum Radius {
static let sm: CGFloat = 4
static let md: CGFloat = 8
static let lg: CGFloat = 16
static let full: CGFloat = 9999
}
// Typography tokens
enum Typography {
static let displayLarge = Font.system(size: 34, weight: .bold)
static let displayMedium = Font.system(size: 28, weight: .bold)
static let titleLarge = Font.system(size: 22, weight: .semibold)
static let titleMedium = Font.system(size: 17, weight: .semibold)
static let bodyLarge = Font.system(size: 17, weight: .regular)
static let bodyMedium = Font.system(size: 15, weight: .regular)
static let caption = Font.system(size: 12, weight: .regular)
}
// Semantic color tokens
enum SemanticColor {
static let primary = Color("PrimaryColor")
static let secondary = Color("SecondaryColor")
static let success = Color.green
static let warning = Color.orange
static let error = Color.red
static let surface = Color(.systemBackground)
static let surfaceVariant = Color(.secondarySystemBackground)
}ViewModifier basati sui token
Questi token si integrano naturalmente nei ViewModifier che definiscono gli stili standard dell'applicazione:
// Modifier for page titles
struct PageTitleModifier: ViewModifier {
func body(content: Content) -> some View {
content
.font(Typography.displayMedium)
.foregroundStyle(Color.primary)
}
}
// Modifier for section titles
struct SectionTitleModifier: ViewModifier {
func body(content: Content) -> some View {
content
.font(Typography.titleLarge)
.foregroundStyle(Color.primary)
}
}
// Modifier for secondary text
struct SecondaryTextModifier: ViewModifier {
func body(content: Content) -> some View {
content
.font(Typography.bodyMedium)
.foregroundStyle(.secondary)
}
}
// Fluent extensions for all text styles
extension View {
func pageTitle() -> some View {
modifier(PageTitleModifier())
}
func sectionTitle() -> some View {
modifier(SectionTitleModifier())
}
func secondaryText() -> some View {
modifier(SecondaryTextModifier())
}
}L'utilizzo diventa espressivo e autodocumentante, rendendo il codice leggibile anche per i nuovi membri del team.
Pronto a superare i tuoi colloqui su iOS?
Pratica con i nostri simulatori interattivi, flashcards e test tecnici.
Modifier interattivi con stato
I ViewModifier possono gestire il proprio stato per creare comportamenti interattivi complessi. Questo pattern si rivela potente per animazioni e feedback utente.
struct PressableModifier: ViewModifier {
// Internal modifier state
@State private var isPressed = false
// Configuration
let scale: CGFloat
let opacity: CGFloat
let animation: Animation
init(
scale: CGFloat = 0.95,
opacity: CGFloat = 0.8,
animation: Animation = .easeInOut(duration: 0.1)
) {
self.scale = scale
self.opacity = opacity
self.animation = animation
}
func body(content: Content) -> some View {
content
.scaleEffect(isPressed ? scale : 1.0)
.opacity(isPressed ? opacity : 1.0)
.animation(animation, value: isPressed)
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.onChanged { _ in
if !isPressed {
isPressed = true
}
}
.onEnded { _ in
isPressed = false
}
)
}
}
extension View {
func pressable(
scale: CGFloat = 0.95,
opacity: CGFloat = 0.8
) -> some View {
modifier(PressableModifier(scale: scale, opacity: opacity))
}
}Animazione di caricamento con effetto shimmer
Un altro esempio di stato interno: l'effetto shimmer per i placeholder di caricamento:
struct ShimmerModifier: ViewModifier {
// Continuous animation
@State private var phase: CGFloat = 0
let duration: Double
let bounce: Bool
init(duration: Double = 1.5, bounce: Bool = false) {
self.duration = duration
self.bounce = bounce
}
func body(content: Content) -> some View {
content
.overlay(
shimmerOverlay
.mask(content)
)
.onAppear {
withAnimation(
.linear(duration: duration)
.repeatForever(autoreverses: bounce)
) {
phase = 1
}
}
}
private var shimmerOverlay: some View {
GeometryReader { geometry in
LinearGradient(
colors: [
.clear,
.white.opacity(0.5),
.clear
],
startPoint: .leading,
endPoint: .trailing
)
.frame(width: geometry.size.width * 2)
.offset(x: -geometry.size.width + (geometry.size.width * 2 * phase))
}
}
}
extension View {
func shimmer(duration: Double = 1.5) -> some View {
modifier(ShimmerModifier(duration: duration))
}
}
// Usage for loading skeletons
struct SkeletonCard: View {
var body: some View {
VStack(alignment: .leading, spacing: Spacing.sm) {
RoundedRectangle(cornerRadius: Radius.md)
.fill(Color.gray.opacity(0.3))
.frame(height: 120)
RoundedRectangle(cornerRadius: Radius.sm)
.fill(Color.gray.opacity(0.3))
.frame(height: 20)
RoundedRectangle(cornerRadius: Radius.sm)
.fill(Color.gray.opacity(0.3))
.frame(width: 150, height: 16)
}
.shimmer()
}
}Composizione di ViewModifier
Il vero potere dei ViewModifier risiede nella loro composizione. Più modifier possono combinarsi per creare stili complessi a partire da blocchi semplici.
// Base modifier for primary buttons
struct PrimaryButtonStyleModifier: ViewModifier {
let isEnabled: Bool
func body(content: Content) -> some View {
content
.font(Typography.bodyLarge.weight(.semibold))
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
.padding(.vertical, Spacing.md)
.background(isEnabled ? SemanticColor.primary : Color.gray)
.cornerRadius(Radius.md)
}
}
// Modifier for secondary buttons (outline)
struct SecondaryButtonStyleModifier: ViewModifier {
let isEnabled: Bool
func body(content: Content) -> some View {
content
.font(Typography.bodyLarge.weight(.semibold))
.foregroundStyle(isEnabled ? SemanticColor.primary : .gray)
.frame(maxWidth: .infinity)
.padding(.vertical, Spacing.md)
.background(Color.clear)
.overlay(
RoundedRectangle(cornerRadius: Radius.md)
.stroke(
isEnabled ? SemanticColor.primary : Color.gray,
lineWidth: 2
)
)
}
}
// Extension combining style and interaction
extension View {
func primaryButton(isEnabled: Bool = true) -> some View {
self
.modifier(PrimaryButtonStyleModifier(isEnabled: isEnabled))
.pressable(scale: isEnabled ? 0.98 : 1.0)
.allowsHitTesting(isEnabled)
}
func secondaryButton(isEnabled: Bool = true) -> some View {
self
.modifier(SecondaryButtonStyleModifier(isEnabled: isEnabled))
.pressable(scale: isEnabled ? 0.98 : 1.0)
.allowsHitTesting(isEnabled)
}
}Questa composizione permette di creare variazioni senza duplicare codice. Ogni modifier rimane testabile e modificabile in modo indipendente.
L'ordine di applicazione dei modifier influenza il risultato visivo. padding prima di background crea spazio tra il contenuto e lo sfondo, mentre l'ordine inverso aggiunge padding intorno allo sfondo.
Modifier adattivi con Environment
I ViewModifier possono leggere i valori dell'environment SwiftUI per adattarsi al contesto: dark mode, dimensione del testo accessibile, orientamento e altro.
struct AdaptiveCardModifier: ViewModifier {
// Reading environment values
@Environment(\.colorScheme) private var colorScheme
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
func body(content: Content) -> some View {
content
.padding(adaptivePadding)
.background(backgroundColor)
.cornerRadius(Radius.lg)
.shadow(
color: shadowColor,
radius: shadowRadius,
x: 0,
y: shadowOffset
)
}
// Padding adapted to text size
private var adaptivePadding: CGFloat {
switch dynamicTypeSize {
case .xSmall, .small, .medium:
return Spacing.md
case .large, .xLarge:
return Spacing.lg
default:
return Spacing.xl
}
}
// Background color based on theme
private var backgroundColor: Color {
colorScheme == .dark
? Color(.secondarySystemBackground)
: Color(.systemBackground)
}
// More subtle shadow in dark mode
private var shadowColor: Color {
colorScheme == .dark
? .clear
: .black.opacity(0.1)
}
private var shadowRadius: CGFloat {
colorScheme == .dark ? 0 : 8
}
private var shadowOffset: CGFloat {
colorScheme == .dark ? 0 : 4
}
}
extension View {
func adaptiveCard() -> some View {
modifier(AdaptiveCardModifier())
}
}Questo modifier si adatta automaticamente alle preferenze di sistema senza richiedere codice aggiuntivo nelle view che lo utilizzano.
Organizzare un design system completo
Un design system strutturato raggruppa i ViewModifier per categoria funzionale per facilitare la scoperta e la manutenzione:
// MARK: - Namespace for the Design System
enum DS {
// MARK: Text Styles
enum Text {
static func title(_ content: some View) -> some View {
content.modifier(PageTitleModifier())
}
static func section(_ content: some View) -> some View {
content.modifier(SectionTitleModifier())
}
static func body(_ content: some View) -> some View {
content.font(Typography.bodyLarge)
}
static func caption(_ content: some View) -> some View {
content.modifier(SecondaryTextModifier())
}
}
// MARK: Container Styles
enum Container {
static func card(_ content: some View) -> some View {
content.cardStyle()
}
static func adaptiveCard(_ content: some View) -> some View {
content.adaptiveCard()
}
}
// MARK: Button Styles
enum Button {
static func primary(_ content: some View, enabled: Bool = true) -> some View {
content.primaryButton(isEnabled: enabled)
}
static func secondary(_ content: some View, enabled: Bool = true) -> some View {
content.secondaryButton(isEnabled: enabled)
}
}
// MARK: Effects
enum Effect {
static func shimmer(_ content: some View) -> some View {
content.shimmer()
}
static func pressable(_ content: some View) -> some View {
content.pressable()
}
}
}Utilizzo del design system
Questa organizzazione produce codice espressivo che documenta la propria intenzione:
struct ProfileScreen: View {
let user: User
@State private var isLoading = false
var body: some View {
ScrollView {
VStack(spacing: Spacing.lg) {
// Header with title style
Text("Profile")
.pageTitle()
// User card
UserCard(user: user)
.adaptiveCard()
// Settings section
VStack(alignment: .leading, spacing: Spacing.md) {
Text("Settings")
.sectionTitle()
SettingsRow(title: "Notifications")
SettingsRow(title: "Privacy")
SettingsRow(title: "Appearance")
}
.adaptiveCard()
// Logout button
Button("Log Out") {
// Action
}
.secondaryButton()
}
.padding(Spacing.md)
}
}
}
struct UserCard: View {
let user: User
var body: some View {
HStack(spacing: Spacing.md) {
AsyncImage(url: user.avatarURL) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
Circle()
.fill(Color.gray.opacity(0.3))
.shimmer()
}
.frame(width: 60, height: 60)
.clipShape(Circle())
VStack(alignment: .leading, spacing: Spacing.xs) {
Text(user.name)
.font(Typography.titleMedium)
Text(user.email)
.secondaryText()
}
Spacer()
}
}
}Testare i ViewModifier
I ViewModifier possono essere testati in modo indipendente per garantirne il comportamento corretto:
import XCTest
import SwiftUI
final class ViewModifierTests: XCTestCase {
func testCardModifierAppliesCorrectCornerRadius() {
// Arrange
let modifier = CardModifier(cornerRadius: 20, shadowRadius: 4)
// Assert
XCTAssertEqual(modifier.cornerRadius, 20)
}
func testBadgeModifierHidesWhenCountIsZero() {
// Badge should not display if count = 0 and showZero = false
let modifier = BadgeModifier(count: 0, showZero: false)
// Verify behavior via snapshot tests or UI tests
}
func testPressableModifierInitialState() {
// Initial state should be not pressed
// Testable via ViewInspector or UI tests
}
}La libreria ViewInspector consente di ispezionare il contenuto delle view SwiftUI nei test unitari, facilitando la validazione dei ViewModifier.
Conclusione
I ViewModifier si impongono come strumento d'elezione per costruire un design system SwiftUI coerente e manutenibile. La loro capacità di incapsulare stili, animazioni e comportamenti in componenti riutilizzabili trasforma l'architettura delle moderne applicazioni iOS.
Checklist del design system SwiftUI
- ✅ Definire i design token (spacing, radius, typography, colors)
- ✅ Creare ViewModifier atomici per ogni stile
- ✅ Usare
@ViewBuilderper i modifier condizionali - ✅ Gestire lo stato interno con
@Stateper le interazioni - ✅ Comporre i modifier per creare stili complessi
- ✅ Leggere l'environment per un adattamento automatico
- ✅ Organizzare i modifier in un namespace chiaro
- ✅ Fornire estensioni di View per una sintassi fluente
- ✅ Testare i modifier in modo indipendente
- ✅ Documentare l'utilizzo con esempi concreti
Inizia a praticare!
Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.
Tag
Condividi
Articoli correlati

Performance SwiftUI: Ottimizzazione di LazyVStack e Liste Complesse
Tecniche di ottimizzazione per LazyVStack e liste SwiftUI. Ridurre il consumo di memoria, migliorare le performance di scroll ed evitare errori comuni.

SwiftUI @Observable vs @State: Quando Usare Cosa nel 2026
Padroneggia le differenze tra @Observable e @State in SwiftUI per scegliere lo strumento di gestione dello stato giusto per le app iOS.

SwiftUI: Creare Interfacce Moderne per iOS
Una guida completa alla creazione di interfacce utente moderne con SwiftUI: sintassi dichiarativa, componenti, animazioni e best practice per iOS 18.