@ScaledMetric คือ property wrapper ของ SwiftUI ที่ปรับขนาดค่าตัวเลขโดยอัตโนมัติตามการตั้งค่า Dynamic Type ของผู้ใช้ ค่าจะถูกห่อหุ้มใน @ScaledMetric และคำนวณใหม่เมื่อขนาดฟอนต์ของระบบเปลี่ยนแปลง ซึ่งรับประกันการเข้าถึงอินเทอร์เฟซสำหรับผู้ที่มีความบกพร่องทางการมองเห็น ตาม เอกสารสำหรับนักพัฒนา Apple (2026) @ScaledMetric ใช้สเกล UIFontMetrics เพื่อคำนวณสเกลสัมพัทธ์ตาม preferred content size category เรียนรู้เพิ่มเติมเกี่ยวกับการเข้าถึงได้ใน บทความเกี่ยวกับการเข้าถึง SwiftUI
ประเด็นสำคัญ
@ScaledMetric คือ property wrapper ของ SwiftUI ที่เพิ่มใน iOS 14 ซึ่งปรับขนาดค่าตัวเลข (CGFloat, Int, Double) โดยอัตโนมัติให้เข้ากับขนาดฟอนต์ Dynamic Type ปัจจุบัน ต่างจาก .font(.body) สำหรับฟอนต์ @ScaledMetric ปรับขนาดพารามิเตอร์ตัวเลขใด ๆ: padding, spacing, cornerRadius, iconSize — ทุกอย่างที่ควรเพิ่มขึ้นตามสัดส่วนเมื่อข้อความใหญ่ขึ้น
วัตถุประสงค์หลักของ @ScaledMetric คือการปรับขนาดการเข้าถึงสำหรับองค์ประกอบอินเทอร์เฟซที่ไม่ใช่ข้อความ เมื่อผู้ใช้เพิ่มขนาดฟอนต์ในการตั้งค่า iOS ปุ่ม ไอคอน และระยะห่างควรปรับขนาดตามสัดส่วนเพื่อให้อินเทอร์เฟซสมดุล @ScaledMetric จัดการสิ่งนี้โดยอัตโนมัติโดยไม่ต้องคำนวณตัวคูณด้วยตนเอง
ไวยากรณ์พื้นฐาน ของ @ScaledMetric ใช้ค่าเริ่มต้นและพารามิเตอร์ relativeTo ที่ไม่บังคับ หากระบุ relativeTo การปรับขนาดจะผูกกับสไตล์ข้อความที่ระบุ (UIFontTextStyle) หากไม่ระบุ จะใช้สเกล .body
struct AccessibleButton: View {
@ScaledMetric private var padding: CGFloat = 12
@ScaledMetric(relativeTo: .title) private var iconSize: CGFloat = 24
var body: some View {
Label("Submit", systemImage: "checkmark.circle.fill")
.font(.body)
.padding(padding)
.imageScale(.init(rawValue: iconSize / 24) ?? .medium)
}
}
padding จะปรับขนาดสัมพัทธ์กับ .body (ค่าเริ่มต้น) iconSize สัมพัทธ์กับ .title เมื่อข้อความใหญ่ขึ้น ระยะห่างและไอคอนจะเพิ่มขึ้นตามสัดส่วน หากไม่มี @ScaledMetric padding จะคงที่ 12 pt โดยไม่คำนึงถึงขนาดฟอนต์ ทำให้เกิดความไม่สมดุลทางสายตา
กลไกของ @ScaledMetric อาศัย UIFontMetrics จาก UIKit เมื่อ SwiftUI สร้างอินสแตนซ์ @ScaledMetric มันจะคำนวณตัวคูณตาม preferred content size category ปัจจุบัน (UIContentSizeCategory) ค่าฐานจะถูกคูณด้วย scaledValue จาก UIFontMetrics สำหรับสไตล์ข้อความที่ระบุ
ทางคณิตศาสตร์: ScaledMetricValue = baseValue × UIFontMetrics.scaledValue(for: relativeTo) หากไม่ระบุ relativeTo จะใช้ UIFontMetrics.default ที่ผูกกับ .body เมื่อ Dynamic Type เปลี่ยนแปลง SwiftUI จะสร้าง View body ใหม่ @ScaledMetric คำนวณ scaledValue ใหม่ และ UI จะอัปเดตโดยอัตโนมัติผ่านกลไก PropertyWrappers ที่คล้าย @State
สเกลของ iOS มี 11 ขนาด: ตั้งแต่ .extraSmall (5 pt) ถึง .accessibilityExtraExtraExtraLarge (77 pt สำหรับ .body) ปัจจัยการปรับขนาดสำหรับ .body อยู่ระหว่าง 0.85 (XS) ถึง 1.71 (XXXL) เทียบกับค่าฐาน @ScaledMetric ใช้สเกลนี้อย่างแม่นยำ ดังนั้นค่า padding 12 pt อาจกลายเป็น ~20 pt ที่ขนาดการเข้าถึงสูงสุด
| หมวดหมู่ขนาดเนื้อหา | ปัจจัย (body) | ตัวอย่าง @ScaledMetric(12) |
|---|---|---|
| extraSmall | 0.85 | ~10 pt |
| small | 0.93 | ~11 pt |
| medium (default) | 1.00 | 12 pt |
| large | 1.07 | ~13 pt |
| extraLarge | 1.15 | ~14 pt |
| extraExtraLarge | 1.28 | ~15 pt |
| accessibilityExtraLarge | 1.47 | ~18 pt |
| accessibilityXXXL | 1.71 | ~20 pt |
การเลือก relativeTo: ใช้ .body สำหรับค่าที่เกี่ยวข้องกับข้อความเนื้อหา (padding, spacing ในรายการ) .title สำหรับองค์ประกอบขนาดใหญ่ (iconSize, imageSize) .caption สำหรับองค์ประกอบขนาดเล็ก (ขนาด badge) ซึ่งช่วยให้องค์ประกอบปรับขนาดสอดคล้องกับข้อความโดยรอบ
Dynamic Type คือฟีเจอร์ iOS ที่ให้ผู้ใช้ปรับขนาดฟอนต์ระบบในการตั้งค่า → จอภาพและความสว่าง → ขนาดข้อความ การเปลี่ยนแปลงมีผลทั่วโลกกับทุกแอป @ScaledMetric ตอบสนองต่อการเปลี่ยนแปลงนี้โดยอัตโนมัติ: SwiftUI อัปเดตตัวแปร @ScaledMetric ทั้งหมดเมื่อ UIContentSizeCategory เปลี่ยนแปลง
สำคัญ: @ScaledMetric ปรับขนาดเฉพาะค่าตัวเลขเท่านั้น ไม่ได้จัดการฟอนต์โดยตรง สำหรับฟอนต์ ให้ใช้ .font() กับสไตล์ข้อความ (.body, .title, .headline) — SwiftUI จะปรับขนาดฟอนต์โดยอัตโนมัติ @ScaledMetric เสริมการปรับขนาดฟอนต์สำหรับ padding, spacing และขนาดองค์ประกอบ
Canvas Preview รองรับ Dynamic Type: แถบเครื่องมือ Canvas มีแถบเลื่อนขนาดข้อความ (A–A) สำหรับทดสอบ UI ที่ขนาดฟอนต์ต่าง ๆ ใช้ร่วมกับ @ScaledMetric เพื่อตรวจสอบว่าระยะห่างและขนาดปรับตัวอย่างถูกต้อง
struct CardView: View {
@ScaledMetric private var cornerRadius: CGFloat = 16
@ScaledMetric private var spacing: CGFloat = 8
var body: some View {
VStack(spacing: spacing) {
Text("Card Title")
.font(.headline)
Text("Description with dynamic type support")
.font(.body)
}
.padding(spacing * 2)
.background(.regularMaterial)
.cornerRadius(cornerRadius)
}
}
struct CardView_Previews: PreviewProvider {
static var previews: some View {
CardView()
.dynamicTypeSize(.large)
.previewDisplayName("Large")
CardView()
.dynamicTypeSize(.accessibility5)
.previewDisplayName("Accessibility 5")
}
}
cornerRadius ปรับขนาดจาก 16 pt เป็น ~27 pt ที่ขนาดการเข้าถึงสูงสุด spacing — จาก 8 เป็น ~14 pt ซึ่งช่วยให้การ์ดคงความสมดุลทางสายตาไม่ว่าขนาดฟอนต์จะเป็นเท่าใด
ตัวอย่าง: ไอคอนที่รองรับ Dynamic Type ขนาดไอคอน Image(systemName:) ไม่ปรับขนาดตาม Dynamic Type โดยค่าเริ่มต้น @ScaledMetric แก้ปัญหานี้โดยเปลี่ยน imageScale หรือขนาดเฟรมตามปัจจัยสเกลปัจจุบัน
struct IconLabel: View {
let title: String
let icon: String
@ScaledMetric private var iconDimension: CGFloat = 28
@ScaledMetric(relativeTo: .body) private var spacing: CGFloat = 6
var body: some View {
HStack(spacing: spacing) {
Image(systemName: icon)
.resizable()
.frame(width: iconDimension, height: iconDimension)
Text(title)
.font(.body)
}
}
}
ตัวอย่าง: คอมโพเนนต์ badge ที่เข้าถึงได้ badge ที่มีตัวเลขควรปรับขนาดตามสัดส่วนของข้อความ @ScaledMetric สำหรับขนาด badge ขั้นต่ำช่วยให้ badge วงกลมยังคงมองเห็นได้เมื่อใช้ข้อความขนาดใหญ่
struct BadgeView: View {
let count: Int
@ScaledMetric(relativeTo: .caption) private var badgeSize: CGFloat = 20
@ScaledMetric(relativeTo: .caption) private var fontScale: CGFloat = 1
var body: some View {
ZStack {
Circle()
.fill(.red)
.frame(width: badgeSize, height: badgeSize)
Text("\(count)")
.font(.caption)
.foregroundColor(.white)
.scaleEffect(fontScale)
}
.fixedSize()
}
}
fontScale ปรับขนาดเนื้อหา Circle เพิ่มเติมเพื่อให้ตรงกับ badgeSize ที่ขยายใหญ่ขึ้น หากไม่มี fontScale ข้อความภายใน badge อาจไม่พอดีเมื่อใช้ข้อความขนาดใหญ่
@ScaledMetric และ @State ต่างก็เป็น property wrappers ที่ติดตามการเปลี่ยนแปลง แต่มีแหล่งที่มาของการอัปเดตต่างกัน @State อัปเดตค่าเมื่อมีการเปลี่ยนแปลงเชิงโปรแกรม (ผ่าน $stateBinding) @ScaledMetric อัปเดตค่าโดยอัตโนมัติเมื่อ Dynamic Type ของระบบเปลี่ยนแปลง แต่ไม่อนุญาตให้เปลี่ยนค่าโดยตรงจากโค้ด
ความแตกต่างสำคัญ: @ScaledMetric เป็นแบบอ่านอย่างเดียวสำหรับนักพัฒนาและเขียนอย่างเดียวสำหรับระบบ คุณไม่สามารถเปลี่ยน scaledValue ผ่าน setter — มันถูกคำนวณโดย SwiftUI ตามค่าฐานและ Dynamic Type ปัจจุบัน ในทางกลับกัน @State ถูกควบคุมโดยนักพัฒนาอย่างสมบูรณ์ หากคุณต้องการค่าที่ทั้งปรับขนาดตาม Dynamic Type และเปลี่ยนแปลงได้เชิงโปรแกรม — ให้รวม @ScaledMetric กับ @State หรือใช้ computed property
| ลักษณะ | @ScaledMetric | @State |
|---|---|---|
| แหล่งอัปเดต | Dynamic Type (ระบบ) | เชิงโปรแกรม (นักพัฒนา) |
| ประเภทค่า | CGFloat, Int, Double | ใดก็ได้ |
| การเปลี่ยนจากโค้ด | ไม่อนุญาต | อนุญาตผ่าน binding |
| การวาด View ใหม่ | เมื่อ Dynamic Type เปลี่ยน | เมื่อค่าเปลี่ยน |
| เวอร์ชัน iOS | iOS 14+ | iOS 13+ |
รูปแบบรวม: หากคุณต้องการเปลี่ยน padding เชิงโปรแกรม (เช่น แอนิเมชันการแตะ) ขณะปรับขนาดตาม Dynamic Type ให้สร้าง @ScaledMetric สำหรับค่าปรับขนาดฐานและ @State สำหรับตัวคูณแอนิเมชัน ค่าสุดท้าย = scaledValue × animationMultiplier
ข้อผิดพลาด 1: การใช้ @ScaledMetric สำหรับฟอนต์ @ScaledMetric ปรับขนาดตัวเลข ไม่ใช่ฟอนต์ สำหรับฟอนต์ ให้ใช้ .font(.body) — SwiftUI ใช้ Dynamic Type โดยอัตโนมัติ อย่าใช้ @ScaledMetric กับ font(.system(size: scaledSize)) — ซึ่งจะทำลายการเข้าถึงของระบบ
ข้อผิดพลาด 2: ขาด relativeTo สำหรับองค์ประกอบที่ต่างกัน หากคุณมี padding (ผูกกับ .body) และ iconSize (ผูกกับ .title) ให้ระบุ relativeTe ที่ถูกต้องสำหรับแต่ละตัว หากไม่มี relativeTo ทั้งสองจะปรับขนาดตาม .body ทำให้ไอคอนปรับขนาดไม่สมดุลเมื่อเทียบกับบริบทข้อความ
ข้อผิดพลาด 3: @ScaledMetric ใน ViewModel/@ObservableObject @ScaledMetric เป็น property wrapper ของ SwiftUI ที่ทำงานภายใน View เท่านั้น ไม่สามารถใช้ใน ViewModel หรือบริการ สำหรับการปรับขนาดใน ViewModel ให้ส่งค่าที่ปรับขนาดแล้วจาก View เป็นพารามิเตอร์หรือใช้ @Environment(\.sizeCategory) ใน View
@Environment(\.sizeCategory) เป็นวิธีอื่นในการรับ Dynamic Type ปัจจุบันใน View ใช้เมื่อคุณต้องการควบคุมมากขึ้น: คำนวณตัวคูณแบบกำหนดเอง ส่ง sizeCategory ไปยัง ViewModel หรือรวมกับ @ScaledMetric เพื่อการปรับขนาดที่ยืดหยุ่น
struct CustomScaledView: View {
@Environment(\.sizeCategory) private var sizeCategory
@ScaledMetric private var basePadding: CGFloat = 12
private var extraPadding: CGFloat {
if sizeCategory >= .accessibilityLarge {
return basePadding * 0.5
}
return 0
}
var body: some View {
Text("Custom scaled content")
.font(.body)
.padding(basePadding + extraPadding)
}
}
Padding เพิ่มเติม extraPadding จะถูกเพิ่มเฉพาะในขนาดการเข้าถึงเท่านั้น ให้พื้นที่มากขึ้นสำหรับข้อความขนาดใหญ่โดยไม่เปลี่ยนลอจิก @ScaledMetric พื้นฐาน
คำถามที่พบบ่อย
@ScaledMetric เป็น property wrapper ทางการของ SwiftUI สำหรับปรับขนาดตัวเลข @ScaledFont ไม่มีเป็น API มาตรฐาน — เป็น wrapper ที่กำหนดเองซึ่งสร้างโดยชุมชน สำหรับฟอนต์ ให้ใช้ .font() ในตัวกับสไตล์ข้อความ (.body, .title) เสมอ และใช้ @ScaledMetric สำหรับ padding, spacing และขนาด
@ScaledMetric พร้อมใช้งานบน iOS 14+, watchOS 7+, tvOS 14+ และ macOS 11+ บน watchOS Dynamic Type ถูกจำกัดช่วงที่เล็กลง — ขนาดตั้งแต่ .extraSmall ถึง .extraLarge โดยไม่มีขนาดการเข้าถึง บน tvOS ไม่มี Dynamic Type — @ScaledMetric จะส่งคืนค่าฐานเสมอ
ได้ ในการทดสอบ @ScaledMetric ให้สร้าง View ที่มี @ScaledMetric และส่งค่า environment .sizeCategory ผ่าน .environment(\.sizeCategory, .extraExtraLarge) จากนั้นรับขนาดองค์ประกอบผ่าน GeometryReader หรือ SwiftUI Inspector หรือทดสอบลอจิกการปรับขนาดผ่าน UIFontMetrics ในโมดูลแยกต่างหาก
.dynamicTypeSize คือตัวปรับแต่ง View ที่จำกัด Dynamic Type สูงสุดสำหรับลำดับชั้น (เช่น .dynamicTypeSize(...large)) @ScaledMetric เคารพข้อจำกัดนี้: หากตั้งค่า .dynamicTypeSize ค่าที่ปรับขนาดแล้วจะไม่เกินขนาดที่สอดคล้องกัน รวมทั้งสอง API เพื่อการควบคุมที่แม่นยำ
ตรวจสอบว่า View ใช้ @ScaledMetric ภายใน (ไม่ใช่ใน ViewModel) ตรวจสอบว่า View สมัครสมาชิก Dynamic Type: @ScaledMetric จะ trigger การรีเฟรช body โดยอัตโนมัติ แต่ถ้า View ใช้ .equatable() หรือ .id() กลไกอาจพัง ใช้ @Environment(\.sizeCategory) เป็นตัวสำรอง
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม