some View เป็นโครงสร้างทางไวยากรณ์หลักของ Swift ที่ไม่มี SwiftUI ไม่สามารถทำงานได้ ตาม Apple Swift Book, 2024, some View เป็นชนิดไม่โปร่งใส่ (opaque type) ที่ซ่อนชนิดค่าที่มีการเพิ่งกลับมาเฉพาะเจาะจง โดยยังคงการกำหนดชนิดแบบเข้มงวดในขณะเรียนเรียบรวม โครงสร้างนี้ทำให้โปรโตคอล View มีลายเซ็งอักษร body ที่เป็นเอกภาพโดยไม่ต้องเปิดเผยรายละเอียดการนำไปใช้งาน
ข้อสำคัญ
some View เป็นไวยากรณ์ชนิดไม่โปร่งใส่ที่นำเสนอใน Swift 5.1 ใช้เป็นชนิดค่าที่ส่งคืนของคุณสมบัติ body ของโปรโตคอล View สัญลักษณ์ some View หมายถึง: “ฟังก์ชันหรือคุณสมบัติส่งคืนบางชนิดรวมที่ตรงกับโปรโตคอล View แต่โค้ดที่เรียกใช้ไม่รู้และไม่จำเป็นต้องรู้ว่าเป็นชนิดใด”
แนวคิดของ ชนิดไม่โปร่งใส่ เป็นด้านกลับของการเขียนโปรแกรมแบบทั่วไป (generics) หาก generics อนุญาตให้โค้ดที่เรียกใช้กำหนดชนิด แต่ชนิดไม่โปร่งใส่อนุญาตให้การนำไปใช้งานกำหนดชนิด โดยซ่อนมันจากผู้เรียกใช้ สิ่งนี้ทำให้นักพัฒนามีอิสระในการเปลี่ยนแปลงการนำไปใช้งานภายในโดยไม่ต้องเปลี่ยนข้อตกลง
ตาม Swift Evolution SE-0244, ชนิดไม่โปร่งใส่ถูกเพิ่มเข้ามาเพื่อสนับสนุน SwiftUI และรูปแบบของโปรโตคอลที่มีชนิดที่เกี่ยวข้องกัน (PAT) ซึ่งไม่สามารถใช้เป็นชนิดค่าที่ส่งคืนได้โดยไม่มีโครงสร้างนี้
หากไม่มี some View ลายเซ็งอักษร body จะเป็นไม่ได้: โปรโตคอล View มีชนิดที่เกี่ยวข้องกัน Body ที่ตรงกับ View หาก body ส่งคืนเพียง View (เป็นโปรโตคอล), Swift จะไม่สามารถทำงานกับโปรโตคอลที่มีข้อกำหนด Self ในตำแหน่งการส่งคืน some View แก้ปัญหานี้โดยการให้ชนิดที่เป็นรวมแต่ซ่อนอยู่
ชนิดไม่โปร่งใส่ เป็นชนิดพิเศษที่มีพฤติกรรมเป็นรวมสำหรับตัวแปลแต่เป็นนามธรรมสำหรับนักพัฒนา เมื่อตัวแปลเห็น some View, มันจะวิเคราะห์การนำไปใช้งานและกำหนดชนิดค่าที่แน่นอน ชนิดนี้ถูกกำหนดแน่นอนและใช้สำหรับการสร้างโค้ดโดยไม่มีการจัดส่งแบบไดนามิก
struct SimpleView: View {
var body: some View {
Text("สวัสดี")
}
}
// ตัวแปลเห็น: body -> Text, not some View
หลักการทำงาน: ตัวแปล Swift อนุมานชนิดรวมจากการนำไปใช้งาน ในตัวอย่างด้านบน, body มีเฉพาะ Text ดังนั้นตัวแปลรู้ว่า body ส่งคืน Text อย่างแท้จริง แม้ว่าลายเซ็งอักษรจะเขียนเป็น some View ทำให้ได้ข้อดีสองอย่าง: การเรียกโดยตรงโดยไม่ต้องมีตารางเมธอดเสมือนจริงและความเป็นไปได้ของการแทรกแทนแบบ inline
หากการนำไปใช้งานของ body เปลี่ยนไป (ตัวอย่าง, แทน Text จะส่งคืน VStack ของ Text และ Button), ตัวแปลจะกำหนดชนิดรวมใหม่ แต่สำหรับโค้ดที่เรียกใช้ (SwiftUI), ลายเซ็งอักษรยังเหมือนเดิม — some View นี้คือ ด้านกลับของ generics: โค้ดที่เรียกใช้ไม่ขึ้นอยู่กับการเปลี่ยนแปลงการนำไปใช้งาน
หนึ่งในกฎที่สำคัญที่สุดของชนิดไม่โปร่งใส่: ฟังก์ชันหรือคุณสมบัติที่ส่งคืน some View ต้องส่งคืนชนิดรวมเดียวกันเสมอ ไม่สามารถส่งคืน Text ในแขนง if หนึ่งและ Image ในอีกแขนงหนึ่ง ข้อจำกัดนี้ถูกตรวจสอบโดยตัวแปลและทำหน้าที่เป็นการรับประกันสำหรับโค้ดที่เรียกใช้
struct BadView: View {
var flag: Bool
var body: some View {
if flag {
Text("จริง") // ข้อผิดพลาด: Text vs VStack
} else {
VStack {
Text("เท็จ")
Image(systemName: "xmark")
}
}
}
}
เพื่อแก้ปัญหานี้ จึงใช้ @ViewBuilder ซึ่งห่อแขนงต่างๆ ไว้ในคอนเทนเนอร์แบบมีเงื่อนไข ConditionalContent การเพิ่มหมายเหตุ @ViewBuilder เทูเป็นประเภทมาตรฐานใน SwiftUI แม้ว่าจะเป็นแบบไม่ชัดเจนได้หาก body มีเพียงนิพจน์เดียว
AnyView เป็นชนิดที่ลบการนำไปใช้งานของ View (type erasure) โดยห่อ Any View ไว้ในตัวห่อเดียว ทำให้สามารถเก็บ Views ของชนิดต่างๆ ไว้ในคอนเทนเนอร์เดียวกัน แตกต่างจาก some View, AnyView ทำงานในขณะเรียกใช้งานและเพิ่มโอเวอร์เฮดในการห่อและแกะห่อ
| เกณฑ์ | some View | AnyView |
|---|---|---|
| เวลาแก้ไข | เรียบรวม | เรียกใช้งาน |
| ประสิทธิภาพ | เรียกโดยตรง ไม่มีโอเวอร์เฮด | ห่อใน existential container |
| ความยืดหยุ่นของชนิด | ชนิดรวมเดียว | ชนิด View ใดก็ได้ |
| การเปลี่ยนแปลงแบบไดนามิก | ไม่รองรับ | รองรับในขณะเรียกใช้งาน |
| ลำดับความสำคัญในการใช้ | เสมอเป็นไปได้ | เมื่อ some View ไม่สามารถใช้ได้ |
| รองรับโปรโตคอล PAT | ใช่ | ใช่ |
เมื่อให้ใช้ AnyView: เฉพาะในสถานการณ์ที่ some View ไม่สามารถใช้ได้เนื่องจากต้องการเปลี่ยนชนิดแบบไดนามิกในขณะเรียกใช้งาน ตัวอย่างเช่น เมื่อส่งคืน View จากพจนานุกรมหรือในโครงสร้างแบบเรียกซ้ำที่ชนิดรวมต้องเปลี่ยนแปลงในแต่ละระดับ AnyView ควรลดให้น้อยที่สุด เพราะการห่อแต่ละครั้งจะปิดการเพิ่มประสิทธิภาพของ SwiftUI
ความเข้าใจผิด: AnyView ไม่ได้แก้ปัญหาชนิดที่แตกต่างกันใน body — @ViewBuilder เป็นผู้แก้ปัญหานี้ AnyView ลบชนิดแต่ไม่ได้ช่วยให้ตัวแปลอนุมานชนิดเดียว ใช้ @ViewBuilder สำหรับลอจิกแบบมีเงื่อนไขและ AnyView สำหรับการจัดส่งแบบไดนามิกเท่านั้น
@ViewBuilder เป็น result builder ที่ออกแบบมาเพื่อทำงานกับ some View โดยเฉพาะ ช่วยให้สามารถใช้ลอจิกแบบมีเงื่อนไข (if/else, switch) และนิพจน์หลายตัวใน body โดยยังคงชนิดค่าที่ส่งคืนเดียว ViewBuilder จะห่อนิพจน์หลายตัวใน TupleView และแขนงแบบมีเงื่อนไขใน ConditionalContent โดยอัตโนมัติ
struct ProfileView: View {
let user: User?
@ViewBuilder
var body: some View {
if let user {
UserCard(user: user)
Text("ออนไลน์")
.font(.caption)
} else {
ProgressView("Loading...")
}
}
}
มันทำงานอย่างไร: @ViewBuilder วิเคราะห์บล็อกโค้ดและสร้างการเรียก buildBlock, buildOptional หรือ buildEither ที่เหมาะสม สำหรับลอจิกแบบมีเงื่อนไข, ConditionalContent จะถูกสร้างขึ้น — ชนิดทั่วไปที่ซ่อนชนิดรวมภายในแขนงแต่เองเป็นชนิดเดียวสำหรับตัวแปล ซึ่งแก้ปัญหาชนิดรวมที่แตกต่างกัน
หากไม่มี @ViewBuilder, คุณสมบัติ body ที่มีหลายนิพจน์หรือลอจิกแบบมีเงื่อนไขจะทำให้เกิดข้อผิดพลาดการคอมไพล์ ด้วยเหตุนี้ SwiftUI จึงใช้ @ViewBuilder กับ body แบบไม่ชัดเจน และสำหรับคุณสมบัติและฟังก์ชันที่กำหนดเองต้องเพิ่มอย่างชัดเจน
@ViewBuilder สามารถซ้อนกันได้: ViewBuilder หนึ่งภายในอีก ViewBuilder หนึ่ง ซึ่งช่วยให้สามารถสร้างลำดับชั้นที่ซับซ้อนโดยมีเงื่อนไขในระดับต่างๆ อย่างไรก็ตาม การซ้อนกันลึกทำให้อ่านยากขึ้น ดังนั้นจึงแนะนำให้แยกเงื่อนไขที่ซ้อนกันออกเป็นคอมโปเนนต์ View แยกต่างหาง
ตัวอย่างที่ 1: คืน View ที่กำหนดเองจากคุณสมบัติที่คำนวณ คุณสมบัติสามารถส่งคืน some View โดยซ่อนส่วนประกอบภายใน ช่วยให้สามารถจัดระเบียบโค้ดใหม่โดยไม่ต้องเปลี่ยนส่วนติดต่อสาธารณะ
struct ArticleView: View {
var body: some View {
CardView {
HeaderView()
ContentView()
FooterView()
}
}
}
struct CardView<Content: View>: View {
let content: Content
var body: some View {
content
.padding(16)
.background(.white)
.cornerRadius(12)
.shadow(radius: 4)
}
}
ตัวอย่างที่ 2: การส่งต่อ View เป็นคลอเจอร์ผ่าน @ViewBuilder รูปแบบนี้ใช้ในคอนเทนเนอร์มาตรฐานของ SwiftUI (VStack, HStack, List) และสามารถนำไปใช้ในคอมโปเนนต์ที่กำหนดเอง
struct CustomContainer<Content: View>: View {
@ViewBuilder let content: () -> Content
var body: some View {
VStack(alignment: .leading) {
content()
}
.padding(20)
}
}
ตัวอย่างที่ 3: ฟังก์ชันไฟเฟอร์ที่ส่งคืน some View ช่วยให้สามารถสร้าง Views ตามพารามิเตอร์โดยไม่ต้องเปิดเผยการนำไปใช้งาน มีประโยชน์สำหรับไลบรารีและคอมโปเนนต์ที่ใช้ซ้ำได้
func makeIcon(for status: Status) -> some View {
switch status {
case .success:
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
case .error:
Image(systemName: "xmark.circle.fill")
.foregroundColor(.red)
case .pending:
ProgressView()
}
}
คำถามที่พบบ่อย
some View เป็นชนิดไม่โปร่งใส่ หมายความว่าจะส่งคืนบางชนิดรวมที่ตรงกับโปรโตคอล View โดยชนิดรวมจะถูกกำหนดโดยตัวแปลแต่ซ่อนจากโค้ดที่เรียกใช้ ทำให้มีการกำหนดชนิดอย่างเข้มงวดโดยไม่ต้องเปิดเผยรายละเอียดการนำไปใช้งาน
some View ถูกแก้ไขในขณะเรียนเรียบรวมโดยไม่มีโอเวอร์เฮด AnyView ใช้ type erasure ในขณะเรียกใช้งานโดยมีค่าใช้จ่ายเพิ่มสำหรับการห่อใน existential container ใช้ some View เมื่อพ็องจะทำได้, AnyView เฉพาะในกรณีที่ต้องเปลี่ยนชนิดแบบไดนามิก
ชนิดไม่โปร่งใส่ต้องการ ชนิดรวมเดียว สำหรับเส้นทางการส่งคืนทั้งหมด if/else กับชนิดที่แตกต่างกันละเมิดข้อกำหนดนี้ @ViewBuilder แก้ปัณหาโดยการห่อแขนงใน ConditionalContent — ชนิดเดียวที่ซ่อนความแตกต่างของการนำไปใช้งานจริง
some View ไม่ทำให้ประสิทธิภาพลดลง — ตัวแปลรู้จักชนิดที่แน่นอนและสร้างโค้ดโดยตรง ในทางกลับกัน, any View (เป็นโปรโตคอล) จะต้องใช้การจัดส่งแบบไดนามิก some View เป็นกลไกการเพิ่มประสิทธิภาพที่ติดตั้งอยู่ในการออกแบบ SwiftUI
ได้, some เป็นโครงสร้างทั่วไปของ Swift 5.1 ที่ไม่ผูกมัดกับ SwiftUI สามารถใช้กับโปรโตคอลใดก็ได้: some Equatable, some Codable, some Collection มีประโยชน์สำหรับการซ่อนชนิดซ้อนกันที่ซับซ้อนเช่น [String: [Int]]
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม