View Protocol เป็นโปรโตคอลพื้นฐานของ SwiftUI ที่ส่วนประกอบอินเทอร์เฟซที่มองเห็นได้ทุกตัวต้องปฏิบัติตาม ตาม Apple Developer Documentation, 2024 View กำหนดสัญญาเดียว: โครงสร้างหรือคลาสที่ใช้โปรโตคอลนี้ต้องระบุคุณสมบัติที่คำนวณได้ body ผ่านโปรโตคอลนี้ SwiftUI สร้างลำดับชั้นหน้าจอทั้งหมดตั้งแต่ป้ายข้อความธรรมดาไปจนถึงโครงสร้างการนำทางที่ซับซ้อน
ประเด็นสำคัญ
View Protocol เป็นโปรโตคอลหลักของ SwiftUI ที่กำหนดว่าองค์ประกอบที่มองเห็นได้ใดๆ อธิบายเนื้อหาของตนอย่างไร แตกต่างจาก UIKit ที่แต่ละองค์ประกอบสืบทอดจาก UIView ผ่านคลาส SwiftUI ใช้แนวทางเชิงโปรโตคอล: ชนิดใดก็ตามที่ปฏิบัติตามโปรโตคอล View สามารถแสดงบนหน้าจอได้
โปรโตคอล View ต้องการการใช้งานคุณสมบัติที่คำนวณได้เพียงอย่างเดียว body ที่ส่งคืนเนื้อหาบางส่วน อย่างไรก็ตาม เบื้องหลังความเรียบง่ายนี้มีระบบองค์ประกอบที่ทรงพลัง: body สามารถส่งคืนชนิดใดก็ได้ที่ปฏิบัติตาม View รวมถึงชนิดพื้นฐาน (Text, Image, Button) ตัวเก็บ (VStack, HStack, ZStack) และส่วนประกอบประกอบแบบกำหนดเอง
ตาม WWDC 2023 มากกว่า 95% ของหน้าจอทั้งหมดในแอปพลิเคชัน SwiftUI ถูกสร้างขึ้นผ่านองค์ประกอบของโครงสร้างที่ใช้โปรโตคอล View ทำให้ View Protocol เป็นรากฐานของสถาปัตยกรรม SwiftUI ทั้งหมด
SwiftUI กำหนดให้ View เป็น ชนิดค่า (value type) (struct) ไม่ใช่คลาส นี่คือการตัดสินใจทางสถาปัตยกรรมที่สำคัญ: ชนิดค่ามีอายุการใช้งานที่คาดเดาได้ ไม่มีสถานะที่เปลี่ยนแปลงได้ร่วมกัน และช่วยให้ SwiftUI สามารถระบุได้อย่างมีประสิทธิภาพว่าส่วนใดของลำดับชั้นเปลี่ยนแปลงและต้องการการวาดใหม่
หากคุณพยายามทำให้ View เป็นคลาส คอมไพเลอร์จะแสดงข้อผิดพลาด: โปรโตคอล View สืบทอดจากโปรโตคอล DynamicViewProperty ซึ่งต้องการความหมายของค่า (value semantics) คลาสสามารถปฏิบัติตาม View ได้ แต่สิ่งนี้ทำลายแนวทางตามสำนวนและสูญเสียประโยชน์ของการอัปเดตอัตโนมัติ
body เป็นข้อกำหนดบังคับเพียงอย่างเดียวของโปรโตคอล View เป็นคุณสมบัติที่คำนวณได้ซึ่งส่งคืนเนื้อหาที่แสดงบนหน้าจอ ชนิดที่ส่งคืนคือ some View ซึ่งหมายถึง “ชนิดที่ปฏิบัติตาม View ซึ่งจะถูกกำหนดโดยคอมไพเลอร์”
struct GreetingView: View {
var name: String
var body: some View {
VStack {
Text("Hello, \(name)!")
.font(.title)
.foregroundColor(.blue)
Button("Start") {
print("Button pressed")
}
}
}
}
body ทำงานอย่างไร: SwiftUI เรียก body ทุกครั้งที่สถานะของแอปพลิเคชันเปลี่ยนแปลงและจำเป็นต้องวาดใหม่ เฟรมเวิร์กเปรียบเทียบทรี View ใหม่กับทรีเก่าและใช้เฉพาะการเปลี่ยนแปลงที่จำเป็น (diffing) นี่เป็นแนวทางเชิงประกาศอย่างสมบูรณ์ — คุณอธิบายสิ่งที่ควรแสดง และ SwiftUI จัดการวิธีการนำไปใช้
รายละเอียดสำคัญ: body ต้องไม่มีผลข้างเคียง มันถูกเรียกหลายครั้งตลอดอายุของแอปพลิเคชัน และหาก body เปลี่ยนแปลงสถานะภายนอก — สิ่งนี้นำไปสู่พฤติกรรมที่คาดเดาไม่ได้ สำหรับผลข้างเคียง ให้ใช้ task, onChange หรือ DispatchQueue
SwiftUI กำหนดข้อจำกัด: body สามารถส่งคืนองค์ประกอบรากได้เพียงตัวเดียวเท่านั้น หากคุณต้องการแสดงหลายองค์ประกอบในระดับเดียวกัน ให้ห่อไว้ในตัวเก็บ — VStack, HStack, ZStack หรือ Group เมื่อมีการนำ @ViewBuilder มาใช้ ข้อจำกัดนี้สังเกตเห็นได้น้อยลง แต่ในเชิงแนวคิด body จะส่งคืน View เดียวเสมอ
some View เป็นไวยากรณ์ชนิดทึบแสง (opaque type) ที่นำมาใช้ใน Swift 5.1 โดยเฉพาะสำหรับ SwiftUI หมายความว่าฟังก์ชันหรือคุณสมบัติส่งคืนชนิดรูปธรรมที่ปฏิบัติตามโปรโตคอล View แต่โค้ดที่เรียกไม่ทราบและไม่จำเป็นต้องทราบว่าชนิดใดที่แน่นอนถูกส่งคืน
คอมไพเลอร์ Swift กำหนดชนิดรูปธรรมในเวลาคอมไพล์สำหรับแต่ละการใช้งาน body แต่ซ่อนไว้จากโลกภายนอก สิ่งนี้ช่วยให้ SwiftUI ปรับลำดับชั้น View ให้เหมาะสมโดยรู้ชนิดที่แน่นอนของส่วนประกอบทั้งหมด ขณะเดียวกันให้ความยืดหยุ่นแก่นักพัฒนาในการเปลี่ยนการใช้งานโดยไม่ต้องเปลี่ยนลายเซ็น
struct ContentView: View {
var body: some View {
Text("Hello, World!") // Compiler knows this is Text
}
}
ทำไม some View และไม่ใช่แค่ View? ถ้า body ส่งคืนเพียง View (เป็นโปรโตคอล) SwiftUI จะไม่สามารถระบุชนิดรูปธรรมในเวลาคอมไพล์ได้ สิ่งนี้เพิ่มค่าใช้จ่ายในการห่อในคอนเทนเนอร์เชิงوجود (existential container) some View ให้ข้อมูลเพียงพอแก่คอมไพเลอร์สำหรับการปรับแต่ง ในขณะที่รักษาความยืดหยุ่นของโปรโตคอล
ข้อจำกัดหลักคือ body ต้องส่งคืนชนิดเดียวกัน คุณไม่สามารถส่งคืน Text ในสาขาหนึ่งของเงื่อนไขและ Image ในอีกสาขาหนึ่งได้โดยไม่มีตัวห่อพิเศษ (AnyView, Group หรือ @ViewBuilder) คอมไพเลอร์ตรวจสอบสิ่งนี้ในเวลาคอมไพล์: เส้นทางส่งคืนที่เป็นไปได้ทั้งหมดต้องมีชนิดเดียวกัน
เพื่อหลีกเลี่ยงข้อจำกัดนี้ ให้ใช้ @ViewBuilder (สร้างชนิด TupleView เดียว), Group (ซึ่งส่งคืนชนิดเดียว) หรือ AnyView (ลบชนิดแต่เพิ่มค่าใช้จ่าย) AnyView ควรใช้เมื่อตัวเลือกอื่นเป็นไปไม่ได้เท่านั้น เนื่องจากมันปิดการปรับแต่งของ SwiftUI
@ViewBuilder เป็น result builder ที่คำอธิบายประกอบอนุญาตให้รวบรวมหลาย Views เป็นองค์ประกอบเดียวโดยไม่ต้องมีตัวเก็บที่ซ้อนกัน @ViewBuilder จะห่อหลายนิพจน์ใน tuple (TupleView) โดยอัตโนมัติ หรือใช้ตรรกะตามเงื่อนไข (If / else / switch) ด้วยชนิดส่งคืนที่ถูกต้อง
struct DashboardView: View {
var isLoggedIn: Bool
@ViewBuilder
var body: some View {
if isLoggedIn {
Text("Welcome!")
.font(.largeTitle)
ProfileCard()
} else {
LoginButton()
.padding()
}
}
}
@ViewBuilder ทำงานอย่างไร: คอมไพเลอร์แปลงแต่ละบล็อกโค้ดภายใน @ViewBuilder เป็นการเรียกเมธอดสแตติก buildBlock, buildEither, buildOptional ฯลฯ ถ้าบล็อกมีหลายนิพจน์ — พวกมันจะถูกห่อใน TupleView ถ้าบล็อกมีตรรกะตามเงื่อนไข — คอมไพเลอร์สร้าง ConditionalContent โดยซ่อนชนิดของสาขา
@ViewBuilder กำหนดข้อจำกัด: สูงสุด 10 องค์ประกอบ ต่อบล็อก (ข้อจำกัด TupleView) หากคุณต้องการรวบรวมมากกว่าสิบองค์ประกอบ ให้ใช้ Group, ForEach หรือแบ่งเป็นส่วนประกอบย่อย ข้อจำกัดนี้มีอยู่เพราะ Swift สร้างการโอเวอร์โหลด buildBlock แยกต่างหากสำหรับจำนวนอาร์กิวเมนต์ตั้งแต่ 1 ถึง 10
องค์ประกอบ (Composition) เป็นหลักการสำคัญของ SwiftUI: อินเทอร์เฟซที่ซับซ้อนถูกสร้างขึ้นจากส่วนประกอบ View ขนาดเล็กที่นำกลับมาใช้ใหม่ได้ แต่ละส่วนประกอบใช้โปรโตคอล View และรับผิดชอบส่วนของหน้าจอของตน ตัวปรับแต่ง (font, padding, foregroundColor) ถูกนำไปใช้กับ View และส่งคืน View ใหม่ที่มีการตั้งค่าที่เปลี่ยนแปลง
ตัวปรับแต่งใน SwiftUI ไม่ใช่การกลายพันธุ์ แต่เป็นการสร้างตัวห่อใหม่รอบ View ดั้งเดิม ตัวปรับแต่งแต่ละตัวส่งคืนชนิดใหม่ (ModifiedContent) ทำให้ SwiftUI สามารถสร้าง ทรีตัวปรับแต่ง และวาดเฉพาะส่วนที่เปลี่ยนแปลงอย่างมีประสิทธิภาพ ลำดับของการใช้ตัวปรับแต่งมีความสำคัญ: ลำดับที่แตกต่างกันให้ผลลัพธ์ทางภาพที่แตกต่างกัน
Text("Hello, SwiftUI!")
.font(.title) // ModifiedContent
.padding() // ModifiedContent<..., PaddingModifier>
.background(.yellow) // ModifiedContent<..., BackgroundModifier>
.cornerRadius(8) // ModifiedContent<..., CornerRadiusModifier>
การปรับแต่งประสิทธิภาพ: SwiftUI ไม่ได้เปรียบเทียบค่ารูปธรรมของ View แต่เปรียบเทียบเอกลักษณ์ผ่านกลไกเอกลักษณ์ (id, ForEach, เอกลักษณ์ที่เสถียรของโครงสร้าง) ถ้าโครงสร้างของ View ไม่เปลี่ยนแปลง — body จะไม่ถูกเรียก ซึ่งทำได้ผ่านการเปรียบเทียบ Equatable และกลไก PreferenceKey สำหรับส่งข้อมูลขึ้นไปในลำดับชั้น
สำหรับองค์ประกอบที่มีประสิทธิภาพ แนะนำให้แบ่งหน้าจอที่ซับซ้อนเป็น ส่วนประกอบย่อยอิสระ แต่ละส่วนมีสถานะขั้นต่ำของตัวเอง สิ่งนี้ช่วยให้ SwiftUI วาดเฉพาะส่วนที่เปลี่ยนแปลงของลำดับชั้น ไม่ใช่ทั้งหน้าจอ
คำถามที่พบบ่อย
View Protocol เป็นโปรโตคอลพื้นฐานของ SwiftUI ที่ส่วนประกอบที่แสดงใดๆ ต้องปฏิบัติตาม มันต้องการคุณสมบัติ body ที่คำนวณได้เพียงอย่างเดียวที่ส่งคืนเนื้อหา องค์ประกอบ SwiftUI มาตรฐานทั้งหมด — Text, Button, Image, VStack — ใช้โปรโตคอลนี้
SwiftUI ใช้ ความหมายของค่า (value semantics) สำหรับการอัปเดตอินเทอร์เฟซที่คาดเดาได้ โครงสร้างไม่มีสถานะที่เปลี่ยนแปลงได้ร่วมกัน ซึ่งช่วยให้ SwiftUI สามารถเปรียบเทียบลำดับชั้น View เก่าและใหม่ได้อย่างมีประสิทธิภาพและวาดเฉพาะองค์ประกอบที่เปลี่ยนแปลง คลาสทำลายการปรับแต่งนี้
body ส่งคืน some View — ชนิดทึบแสงที่ซ่อนการใช้งานรูปธรรม จริงๆ แล้วมันส่งคืนชนิดใดก็ได้ที่ปฏิบัติตาม View: Text, Image, VStack, โครงสร้างกำหนดเอง คอมไพเลอร์กำหนดชนิดรูปธรรมในเวลาคอมไพล์เพื่อการปรับแต่ง
some View เป็นชนิดทึบแสงที่มีชนิดรูปธรรมถูกกำหนดในเวลาคอมไพล์ AnyView คือการลบชนิด (type erasure) ที่ห่อ View ใดๆ ในคอนเทนเนอร์เดียว some View มีประสิทธิภาพมากกว่า AnyView เพิ่มค่าใช้จ่ายและใช้เฉพาะเมื่อต้องการการเปลี่ยนชนิดแบบไดนามิก
สูงสุด 10 องค์ประกอบ — นี่คือข้อจำกัดของ TupleView ซึ่งสร้าง buildBlock สำหรับจำนวนอาร์กิวเมนต์ตั้งแต่ 1 ถึง 10 หากคุณต้องการองค์ประกอบมากขึ้น ให้ใช้ Group, ForEach, List หรือแบ่งเป็นส่วนประกอบย่อย ข้อจำกัดนี้มีอยู่ในระดับคอมไพเลอร์ Swift
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม