# 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. - Published: 2026-03-01 - Updated: 2026-04-29 - Author: SharpSkill - Tags: swiftui, ios, viewmodifier, design-system, swift - Reading time: 12 min --- 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. > **Cosa copre questo articolo** > > 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. ```swift // CardModifier.swift 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: ```swift // CardUsageExample.swift 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. ```swift // ConditionalBadgeModifier.swift 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. > **Performance dei condizionali** > > 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. ```swift // DesignTokens.swift 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: ```swift // TextStyleModifiers.swift // 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. ## 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. ```swift // PressableModifier.swift 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: ```swift // ShimmerModifier.swift 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. ```swift // ComposedModifiers.swift // 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 dei modifier conta** > > 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. ```swift // AdaptiveCardModifier.swift 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: ```swift // DesignSystem.swift // 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: ```swift // DesignSystemUsage.swift 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: ```swift // ViewModifierTests.swift 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 } } ``` > **ViewInspector per i test** > > 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 `@ViewBuilder` per i modifier condizionali - ✅ Gestire lo stato interno con `@State` per 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 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/it/blog/ios/swiftui-viewmodifiers-patterns-design-system