ViewModifier แบบกำหนดเองใน SwiftUI: รูปแบบที่นำกลับมาใช้ใหม่ได้สำหรับ Design System
สร้าง ViewModifier แบบกำหนดเองใน SwiftUI สำหรับ design system ที่สอดคล้องกัน รูปแบบ แนวทางปฏิบัติที่ดีที่สุด และตัวอย่างที่ใช้งานได้จริงสำหรับการจัดสไตล์ view ของ iOS อย่างมีประสิทธิภาพ

ViewModifier เป็นรากฐานของ design system SwiftUI ที่แข็งแกร่งและบำรุงรักษาได้ง่าย ด้วยการห่อหุ้มสไตล์และพฤติกรรมไว้ในคอมโพเนนต์ที่นำกลับมาใช้ใหม่ได้ ViewModifier จึงรับประกันความสอดคล้องเชิงทัศนะและลดการซ้ำซ้อนของโค้ดได้อย่างมาก แนวทางแบบโมดูลาร์นี้เปลี่ยนแปลงวิธีการสร้างอินเทอร์เฟซ iOS สมัยใหม่
บทความนี้นำเสนอรูปแบบที่จำเป็นสำหรับการสร้าง ViewModifier แบบกำหนดเองที่มีประสิทธิภาพ ตั้งแต่โครงสร้างพื้นฐานไปจนถึงการประกอบขั้นสูงสำหรับ design system ที่สมบูรณ์
กายวิภาคของ ViewModifier
ViewModifier คือ protocol ที่นิยามการแปลงที่สามารถนำไปใช้กับ view ใดก็ได้ ซึ่งแตกต่างจาก extension ของ View ตรงที่ ViewModifier สามารถเก็บสถานะภายในและรับพารามิเตอร์ที่กำหนดค่าได้
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 จะกลายเป็นเรื่องง่ายและอ่านได้อย่างเป็นธรรมชาติในโค้ด:
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 ที่ซับซ้อนพร้อมการแยกสาขาได้
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 ห่อหุ้มโทเคนเหล่านี้ได้อย่างสมบูรณ์แบบ
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 ที่นิยามสไตล์มาตรฐานของแอปพลิเคชันได้อย่างเป็นธรรมชาติ:
// 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())
}
}การใช้งานกลายเป็นการแสดงออกที่ชัดเจนและบันทึกตัวเองได้ ทำให้โค้ดอ่านง่ายแม้สำหรับสมาชิกใหม่ของทีม
พร้อมที่จะพิชิตการสัมภาษณ์ iOS แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
Modifier เชิงโต้ตอบที่มีสถานะ
ViewModifier สามารถจัดการสถานะของตนเองเพื่อสร้างพฤติกรรมเชิงโต้ตอบที่ซับซ้อนได้ รูปแบบนี้แสดงให้เห็นถึงพลังในการสร้างแอนิเมชันและการตอบสนองของผู้ใช้
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 ในการโหลด:
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 หลายตัวสามารถรวมกันเพื่อสร้างสไตล์ที่ซับซ้อนจากบล็อกพื้นฐาน
// 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 ไปใช้ส่งผลต่อผลลัพธ์เชิงทัศนะ padding ก่อน background จะสร้างพื้นที่ระหว่างเนื้อหาและพื้นหลัง ในขณะที่ลำดับตรงกันข้ามจะเพิ่ม padding รอบพื้นหลัง
Modifier ปรับตัวด้วย Environment
ViewModifier สามารถอ่านค่า environment ของ SwiftUI เพื่อปรับให้เข้ากับบริบท: โหมดมืด ขนาดข้อความที่เข้าถึงได้ การวางแนว และอื่นๆ
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 ตามหมวดหมู่การใช้งานเพื่อความสะดวกในการค้นหาและบำรุงรักษา:
// 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
การจัดระเบียบนี้สร้างโค้ดที่แสดงออกได้ดีและบันทึกเจตนาของตนเอง:
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 สามารถทดสอบได้อย่างอิสระเพื่อรับประกันพฤติกรรมที่ถูกต้อง:
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 ช่วยให้สามารถตรวจสอบเนื้อหาของ 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 อย่างอิสระ
- ✅ บันทึกการใช้งานด้วยตัวอย่างที่เป็นรูปธรรม
เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
แท็ก
แชร์
บทความที่เกี่ยวข้อง

ประสิทธิภาพ SwiftUI: การปรับแต่ง LazyVStack และรายการที่ซับซ้อน
เทคนิคการปรับแต่งสำหรับ LazyVStack และรายการ SwiftUI ลดการใช้หน่วยความจำ ปรับปรุงประสิทธิภาพการเลื่อน และหลีกเลี่ยงข้อผิดพลาดที่พบบ่อย

SwiftUI @Observable vs @State: ใช้ตัวไหนเมื่อไหร่ในปี 2026
เข้าใจความแตกต่างระหว่าง @Observable และ @State ใน SwiftUI เพื่อเลือกเครื่องมือจัดการ state ที่เหมาะกับแอป iOS

SwiftUI: การสร้างอินเทอร์เฟซที่ทันสมัยสำหรับ iOS
คู่มือการสร้างอินเทอร์เฟซที่ทันสมัยด้วย SwiftUI: ไวยากรณ์แบบ declarative, คอมโพเนนต์, แอนิเมชัน และแนวปฏิบัติที่ดีที่สุดสำหรับ iOS 18