# ViewModifier แบบกำหนดเองใน SwiftUI: รูปแบบที่นำกลับมาใช้ใหม่ได้สำหรับ Design System > สร้าง ViewModifier แบบกำหนดเองใน SwiftUI สำหรับ design system ที่สอดคล้องกัน รูปแบบ แนวทางปฏิบัติที่ดีที่สุด และตัวอย่างที่ใช้งานได้จริงสำหรับการจัดสไตล์ view ของ iOS อย่างมีประสิทธิภาพ - Published: 2026-03-01 - Updated: 2026-04-29 - Author: SharpSkill - Tags: swiftui, ios, viewmodifier, design-system, swift - Reading time: 12 min --- ViewModifier เป็นรากฐานของ design system SwiftUI ที่แข็งแกร่งและบำรุงรักษาได้ง่าย ด้วยการห่อหุ้มสไตล์และพฤติกรรมไว้ในคอมโพเนนต์ที่นำกลับมาใช้ใหม่ได้ ViewModifier จึงรับประกันความสอดคล้องเชิงทัศนะและลดการซ้ำซ้อนของโค้ดได้อย่างมาก แนวทางแบบโมดูลาร์นี้เปลี่ยนแปลงวิธีการสร้างอินเทอร์เฟซ iOS สมัยใหม่ > **สิ่งที่บทความนี้ครอบคลุม** > > บทความนี้นำเสนอรูปแบบที่จำเป็นสำหรับการสร้าง ViewModifier แบบกำหนดเองที่มีประสิทธิภาพ ตั้งแต่โครงสร้างพื้นฐานไปจนถึงการประกอบขั้นสูงสำหรับ design system ที่สมบูรณ์ ## กายวิภาคของ ViewModifier ViewModifier คือ protocol ที่นิยามการแปลงที่สามารถนำไปใช้กับ view ใดก็ได้ ซึ่งแตกต่างจาก extension ของ View ตรงที่ ViewModifier สามารถเก็บสถานะภายในและรับพารามิเตอร์ที่กำหนดค่าได้ ```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 )) } } ``` รูปแบบ `Content` แสดงถึง view ที่ modifier ถูกนำไปใช้ การทำให้เป็นนามธรรมนี้ทำให้ modifier เดียวกันสามารถทำงานกับ view SwiftUI ประเภทใดก็ได้ ### การใช้ CardModifier ในทางปฏิบัติ เมื่อสร้างเสร็จแล้ว การใช้ modifier จะกลายเป็นเรื่องง่ายและอ่านได้อย่างเป็นธรรมชาติในโค้ด: ```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? } ``` ความชัดเจนของโค้ดช่วยปรับปรุงความสามารถในการบำรุงรักษาอย่างมีนัยสำคัญ การเปลี่ยนสไตล์ของการ์ดทั้งหมดในแอปพลิเคชันต้องการเพียงการเปลี่ยนแปลงเดียวใน ViewModifier ## Modifier แบบมีเงื่อนไขด้วย @ViewBuilder ViewModifier สามารถรวมตรรกะแบบมีเงื่อนไขเพื่อปรับพฤติกรรมตามบริบท แอตทริบิวต์ `@ViewBuilder` ช่วยให้สร้าง view ที่ซับซ้อนพร้อมการแยกสาขาได้ ```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 )) } } ``` รูปแบบนี้ช่วยให้สามารถเพิ่มตราสัญลักษณ์การแจ้งเตือนให้กับองค์ประกอบอินเทอร์เฟซใดก็ได้ในลักษณะที่เป็นการประกาศและปรับแต่งได้ > **ประสิทธิภาพของเงื่อนไข** > > SwiftUI ปรับปรุงสาขา `@ViewBuilder` ให้เหมาะสมโดยอัตโนมัติ View ที่ไม่ได้แสดงผลจะไม่ถูกเรนเดอร์ จึงรักษาประสิทธิภาพไว้ได้แม้ในเงื่อนไขที่ซับซ้อน ## Design System ด้วยโทเคนสไตล์ Design system ที่สมบูรณ์อาศัยโทเคน: ค่าคงที่เชิงความหมายที่นิยามอัตลักษณ์ทางทัศนะของแอปพลิเคชัน ViewModifier ห่อหุ้มโทเคนเหล่านี้ได้อย่างสมบูรณ์แบบ ```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 ที่อิงโทเคน โทเคนเหล่านี้ผสานเข้ากับ ViewModifier ที่นิยามสไตล์มาตรฐานของแอปพลิเคชันได้อย่างเป็นธรรมชาติ: ```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()) } } ``` การใช้งานกลายเป็นการแสดงออกที่ชัดเจนและบันทึกตัวเองได้ ทำให้โค้ดอ่านง่ายแม้สำหรับสมาชิกใหม่ของทีม ## Modifier เชิงโต้ตอบที่มีสถานะ ViewModifier สามารถจัดการสถานะของตนเองเพื่อสร้างพฤติกรรมเชิงโต้ตอบที่ซับซ้อนได้ รูปแบบนี้แสดงให้เห็นถึงพลังในการสร้างแอนิเมชันและการตอบสนองของผู้ใช้ ```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)) } } ``` ### แอนิเมชันการโหลดด้วยเอฟเฟกต์ shimmer อีกตัวอย่างหนึ่งของสถานะภายใน: เอฟเฟกต์ shimmer สำหรับ placeholder ในการโหลด: ```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() } } ``` ## การประกอบ ViewModifier พลังที่แท้จริงของ ViewModifier อยู่ที่การประกอบเข้าด้วยกัน Modifier หลายตัวสามารถรวมกันเพื่อสร้างสไตล์ที่ซับซ้อนจากบล็อกพื้นฐาน ```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) } } ``` การประกอบนี้ช่วยให้สร้างรูปแบบต่างๆ ได้โดยไม่ต้องทำซ้ำโค้ด แต่ละ modifier ยังคงทดสอบและปรับเปลี่ยนได้อย่างอิสระ > **ลำดับของ modifier มีความสำคัญ** > > ลำดับการนำ modifier ไปใช้ส่งผลต่อผลลัพธ์เชิงทัศนะ `padding` ก่อน `background` จะสร้างพื้นที่ระหว่างเนื้อหาและพื้นหลัง ในขณะที่ลำดับตรงกันข้ามจะเพิ่ม padding รอบพื้นหลัง ## Modifier ปรับตัวด้วย Environment ViewModifier สามารถอ่านค่า environment ของ SwiftUI เพื่อปรับให้เข้ากับบริบท: โหมดมืด ขนาดข้อความที่เข้าถึงได้ การวางแนว และอื่นๆ ```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()) } } ``` Modifier นี้ปรับตัวเองให้เข้ากับการตั้งค่าระบบโดยอัตโนมัติโดยไม่ต้องเพิ่มโค้ดใน view ที่ใช้งาน ## การจัดระเบียบ Design System ที่สมบูรณ์ Design system ที่มีโครงสร้างจะจัดกลุ่ม ViewModifier ตามหมวดหมู่การใช้งานเพื่อความสะดวกในการค้นหาและบำรุงรักษา: ```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() } } } ``` ### การใช้ Design System การจัดระเบียบนี้สร้างโค้ดที่แสดงออกได้ดีและบันทึกเจตนาของตนเอง: ```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() } } } ``` ## การทดสอบ ViewModifier ViewModifier สามารถทดสอบได้อย่างอิสระเพื่อรับประกันพฤติกรรมที่ถูกต้อง: ```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 สำหรับการทดสอบ** > > ไลบรารี ViewInspector ช่วยให้สามารถตรวจสอบเนื้อหาของ view SwiftUI ในการทดสอบหน่วย ทำให้การตรวจสอบ ViewModifier ง่ายขึ้น ## บทสรุป ViewModifier ก้าวขึ้นมาเป็นเครื่องมือที่ได้รับความนิยมในการสร้าง design system SwiftUI ที่สอดคล้องและบำรุงรักษาได้ ความสามารถในการห่อหุ้มสไตล์ แอนิเมชัน และพฤติกรรมไว้ในคอมโพเนนต์ที่นำกลับมาใช้ใหม่ได้นั้นเปลี่ยนแปลงสถาปัตยกรรมของแอป iOS สมัยใหม่ ### รายการตรวจสอบ Design System SwiftUI - ✅ กำหนดโทเคนการออกแบบ (spacing, radius, typography, colors) - ✅ สร้าง ViewModifier แบบอะตอมิกสำหรับแต่ละสไตล์ - ✅ ใช้ `@ViewBuilder` สำหรับ modifier แบบมีเงื่อนไข - ✅ จัดการสถานะภายในด้วย `@State` สำหรับการโต้ตอบ - ✅ ประกอบ modifier เพื่อสร้างสไตล์ที่ซับซ้อน - ✅ อ่าน environment เพื่อการปรับตัวอัตโนมัติ - ✅ จัดระเบียบ modifier ใน namespace ที่ชัดเจน - ✅ จัดเตรียม extension ของ View สำหรับไวยากรณ์ที่ลื่นไหล - ✅ ทดสอบ modifier อย่างอิสระ - ✅ บันทึกการใช้งานด้วยตัวอย่างที่เป็นรูปธรรม --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/ios/swiftui-viewmodifiers-patterns-design-system