# SwiftUI: การสร้างอินเทอร์เฟซที่ทันสมัยสำหรับ iOS > คู่มือการสร้างอินเทอร์เฟซที่ทันสมัยด้วย SwiftUI: ไวยากรณ์แบบ declarative, คอมโพเนนต์, แอนิเมชัน และแนวปฏิบัติที่ดีที่สุดสำหรับ iOS 18 - Published: 2026-01-05 - Updated: 2026-04-02 - Author: Anthony Fillion-Maillet - Tags: swiftui, ios, swift, ui, apple - Reading time: 12 min --- SwiftUI ได้เปลี่ยนแปลงวิธีการพัฒนาอินเทอร์เฟซบนแพลตฟอร์ม Apple อย่างสิ้นเชิง ด้วยไวยากรณ์แบบ declarative และการผสานกับระบบ native เฟรมเวิร์กนี้ช่วยให้สร้างแอปพลิเคชันที่สวยงามด้วยโค้ดที่น้อยกว่าที่เคย iOS 18 นำเสนอการปรับปรุงประสิทธิภาพที่สำคัญพร้อมความสามารถใหม่หลายประการ > **Why SwiftUI in 2026?** > > SwiftUI มีความสมบูรณ์แล้วใน iOS 18 โดยมีการ render ที่ถูกปรับให้เหมาะสม การจัดการหน่วยความจำที่ดีขึ้น และการผสาน UIKit ที่ง่ายขึ้น ถือเป็นมาตรฐานสำหรับแอปพลิเคชัน iOS ใหม่ ## ทำความเข้าใจรูปแบบ Declarative ก่อนเริ่มเขียนโค้ด ควรทำความเข้าใจว่า SwiftUI แตกต่างจากเฟรมเวิร์กเดิมอย่างไร ใน UIKit (เฟรมเวิร์กรุ่นก่อน) นักพัฒนาต้องบอก iOS **วิธีการ**สร้างอินเทอร์เฟซทีละขั้นตอน เช่น "สร้าง label วางไว้ตรงนี้ เปลี่ยนสีเมื่อผู้ใช้กดปุ่ม" นี่คือรูปแบบ **imperative** SwiftUI ทำงานแตกต่างออกไป นักพัฒนาจะอธิบาย**สิ่งที่**ต้องการแสดงผล แล้วเฟรมเวิร์กจะจัดการส่วนที่เหลือเอง ความแตกต่างนี้เปรียบได้กับการนำทาง GPS: รูปแบบ imperative ให้คำแนะนำเลี้ยวทีละจุด ส่วนรูปแบบ declarative เพียงระบุจุดหมายปลายทาง ### View แรก ใน SwiftUI ทุกองค์ประกอบของอินเทอร์เฟซคือ `View` โดย View เป็น struct ที่อธิบายสิ่งที่ควรแสดงบนหน้าจอ ต่อไปนี้คือหน้าจอแรกที่มีหัวข้อ หัวข้อย่อย และปุ่ม: ```swift // ContentView.swift import SwiftUI // Each screen is a struct that implements the View protocol struct ContentView: View { // The "body" property describes what the view displays // "some View" means "a type of View, but Swift figures it out automatically" var body: some View { // VStack = "Vertical Stack": stacks elements vertically // spacing: 20 = 20 points of space between each element VStack(spacing: 20) { // A simple text with style modifiers Text("Welcome to SharpSkill") .font(.largeTitle) // Large title font .fontWeight(.bold) // Bold text Text("Prepare for your iOS interviews") .font(.subheadline) // Smaller font .foregroundColor(.secondary) // Gray secondary color // Button with an action (closure) and a label Button("Get Started") { print("Button tapped!") } .buttonStyle(.borderedProminent) // Filled blue button style } .padding() // Adds margins around the VStack } } ``` สังเกตโครงสร้าง: เริ่มด้วยการประกาศสิ่งที่ต้องการ (ข้อความ ปุ่ม) จากนั้นจัดเรียงองค์ประกอบในแนวตั้ง (VStack) และใช้สไตล์ผ่าน **modifier** (`.font()`, `.padding()`) > **ประเด็นสำคัญ**: ใน SwiftUI ไม่มีการ "สร้าง" วัตถุ UI ด้วยตนเอง แต่จะ**อธิบาย**อินเทอร์เฟซที่ต้องการ SwiftUI จะจัดการการสร้าง อัปเดต และทำลายองค์ประกอบจริงเอง ### Modifier: การแปลง View Modifier คือเมธอดที่เรียกต่อกันเป็นลูกโซ่หลัง view เพื่อแปลงรูปแบบ สามารถเปรียบได้กับฟิลเตอร์ที่ใส่ทับซ้อนกันทีละชั้น **ลำดับมีความสำคัญ**เพราะ modifier แต่ละตัวจะสร้าง view ใหม่ที่ครอบ view เดิม ตัวอย่างที่แสดงให้เห็นว่าลำดับสำคัญอย่างไร: ```swift // ModifiersOrder.swift // Example 1: padding THEN background Text("SwiftUI") .padding() // 1. Adds 16pt of space around the text .background(.blue) // 2. Blue background covers text + padding .foregroundColor(.white) // Result: white text on blue rectangle with margins // Example 2: background THEN padding (reversed order!) Text("SwiftUI") .background(.blue) // 1. Tight blue background around text only .padding() // 2. Transparent padding around blue background .foregroundColor(.white) // Result: white text on small blue rectangle, surrounded by empty space ``` ความแตกต่างคือ: ในกรณีแรก padding อยู่ "ภายใน" background ส่วนกรณีที่สอง padding อยู่ "ภายนอก" รายละเอียดเล็กน้อยแต่สำคัญมากสำหรับการจัดวางเลย์เอาต์ ## การจัดระเบียบอินเทอร์เฟซด้วย Stacks SwiftUI มี container หลักสามแบบสำหรับจัดระเบียบ view ลองนึกภาพว่าเป็นกล่องที่จัดเรียงเนื้อหาในรูปแบบที่แตกต่างกัน ### VStack, HStack และ ZStack - **VStack** (Vertical Stack): จัดเรียงองค์ประกอบจากบนลงล่าง - **HStack** (Horizontal Stack): จัดเรียงองค์ประกอบจากซ้ายไปขวา - **ZStack** (Z-axis Stack): วางองค์ประกอบซ้อนทับกัน มาสร้างการ์ดโปรไฟล์ผู้ใช้ที่รวม stack ทั้งสามแบบ เป้าหมายคือแสดงรูปภาพพร้อมตรายืนยันตัวตน ชื่อ และตำแหน่งของผู้ใช้: ```swift // ProfileCard.swift struct ProfileCard: View { var body: some View { // Main HStack: photo on left, info on right HStack(spacing: 16) { // ZStack to overlay the badge on the photo ZStack(alignment: .bottomTrailing) { // Profile image (circle) Image("avatar") .resizable() .frame(width: 60, height: 60) .clipShape(Circle()) // Green "verified" badge at bottom right Image(systemName: "checkmark.circle.fill") .foregroundColor(.green) .background(Circle().fill(.white)) // White circle behind } // VStack to stack name and role vertically VStack(alignment: .leading, spacing: 4) { Text("Marie Dupont") .font(.headline) Text("iOS Developer") .font(.subheadline) .foregroundColor(.secondary) } // Spacer pushes everything to the left Spacer() // Chevron on right indicates it's tappable Image(systemName: "chevron.right") .foregroundColor(.gray) } .padding() .background(Color(.systemGray6)) .cornerRadius(12) } } ``` เทคนิคสำคัญคือ `Spacer()` ซึ่งเป็นองค์ประกอบที่มองไม่เห็นแต่กินพื้นที่ว่างทั้งหมด หากไม่มี Spacer องค์ประกอบจะอยู่ตรงกลาง แต่เมื่อมี Spacer เนื้อหาจะถูกดันไปทางซ้ายและ chevron จะอยู่ทางขวา > **Xcode Tip** > > ใช้ `⌘ + click` บน view ใดก็ได้ใน Xcode เพื่อเปิด visual inspector สามารถเพิ่ม modifier ได้โดยไม่ต้องพิมพ์โค้ดเอง ## การจัดการ State ด้วย @State และ @Binding การจัดการ state เป็นหัวใจสำคัญของ SwiftUI โดย state คือข้อมูลใดก็ตามที่สามารถเปลี่ยนแปลงได้และต้องอัปเดตอินเทอร์เฟซ เมื่อ state เปลี่ยน SwiftUI จะคำนวณ view ที่ได้รับผลกระทบใหม่โดยอัตโนมัติ ### @State: State ภายในของ View `@State` คือ property wrapper ที่บอก SwiftUI ว่า "เฝ้าดูตัวแปรนี้ และรีเฟรช view เมื่อค่าเปลี่ยน" เหมาะสำหรับ state ภายในของ view เดียว มาสร้างตัวนับแบบโต้ตอบเพื่อทำความเข้าใจกลไก: ```swift // CounterView.swift struct CounterView: View { // @State creates a "source of truth" for this view // private because state shouldn't be modified from outside @State private var count = 0 var body: some View { VStack(spacing: 30) { // This Text updates automatically when count changes Text("\(count)") .font(.system(size: 72, weight: .bold)) HStack(spacing: 40) { // Decrement button Button(action: { count -= 1 // Modifies state → view refreshes }) { Image(systemName: "minus.circle.fill") .font(.largeTitle) } // Increment button Button(action: { count += 1 }) { Image(systemName: "plus.circle.fill") .font(.largeTitle) } } } } } ``` เมื่อกดปุ่ม ค่า `count` จะเปลี่ยน SwiftUI ตรวจจับการเปลี่ยนแปลงนี้และเรียก `body` ใหม่เพื่ออัปเดตการแสดงผล ไม่จำเป็นต้องอัปเดต label ด้วยตนเอง ทุกอย่างเป็นอัตโนมัติ ### @Binding: การแชร์ State ระหว่าง View บางครั้ง view ลูกจำเป็นต้องแก้ไข state ของ view แม่ นี่คือจุดที่ `@Binding` เข้ามา โดยสร้างการเชื่อมต่อแบบสองทางกับ `@State` ที่มีอยู่ ตัวอย่างที่เป็นรูปธรรม: ช่องกรอกชื่อผู้ใช้พร้อมการตรวจสอบแบบเรียลไทม์ ```swift // UsernameValidation.swift // Parent view: owns the state struct SignupForm: View { @State private var username = "" // Source of truth @State private var isValid = false // Validation state var body: some View { VStack(spacing: 20) { // We pass BINDINGS (with $) to the child view UsernameField(username: $username, isValid: $isValid) Button("Create Account") { // Submit the form } .disabled(!isValid) // Disabled if invalid .buttonStyle(.borderedProminent) } .padding() } } ``` View ลูกรับ binding และสามารถแก้ไขค่าได้: ```swift // Child view: receives and modifies state via @Binding struct UsernameField: View { @Binding var username: String // Connection to parent's @State @Binding var isValid: Bool var body: some View { VStack(alignment: .leading, spacing: 8) { TextField("Username", text: $username) .textFieldStyle(.roundedBorder) .onChange(of: username) { oldValue, newValue in // Validation: at least 3 characters isValid = newValue.count >= 3 } // Visual feedback HStack { Image(systemName: isValid ? "checkmark.circle" : "xmark.circle") Text("Minimum 3 characters") } .font(.caption) .foregroundColor(isValid ? .green : .red) } } } ``` เมื่อผู้ใช้พิมพ์ใน TextField ค่า `username` จะถูกแก้ไขผ่าน binding View แม่เห็นการเปลี่ยนแปลงนี้และสามารถนำไปใช้ได้ นี่คือการสื่อสารแบบสองทางที่ชัดเจน > **Warning** > > ควรใช้ `@State` สำหรับ state ที่เรียบง่ายและอยู่ภายในเท่านั้น สำหรับข้อมูลที่แชร์ระหว่างหลายหน้าจอหรือลอจิกที่ซับซ้อน แนะนำให้ใช้ `@Observable` (iOS 17+) หรือรูปแบบ MVVM ## การแสดงรายการแบบไดนามิก รายการปรากฏอยู่ทุกที่ในแอปพลิเคชันมือถือ SwiftUI มี `List` สำหรับแสดงคอลเลกชันข้อมูลพร้อมสไตล์ native ของ iOS (เส้นแบ่ง การดำเนินการเมื่อปัด เป็นต้น) ### การสร้างรายการแบบง่าย การแสดงรายการต้องมีสองสิ่ง: ข้อมูลและวิธีระบุตัวตน โปรโตคอล `Identifiable` ช่วยให้ SwiftUI ทราบว่า view ใดสอดคล้องกับข้อมูลใด เริ่มจากการกำหนดโมเดลข้อมูล: ```swift // Models/Interview.swift // Identifiable lets SwiftUI track each element struct Interview: Identifiable { let id = UUID() // Auto-generated unique identifier let technology: String let difficulty: String let questionCount: Int } ``` จากนั้นสร้างรายการ แนวคิดคือวนซ้ำข้อมูลด้วย `ForEach` และสร้างแถวสำหรับแต่ละรายการ: ```swift // InterviewListView.swift struct InterviewListView: View { // Data to display (in reality, this would come from an API) @State private var interviews = [ Interview(technology: "iOS", difficulty: "Intermediate", questionCount: 25), Interview(technology: "Android", difficulty: "Advanced", questionCount: 30), Interview(technology: "React", difficulty: "Beginner", questionCount: 20) ] var body: some View { // NavigationStack enables the navigation bar NavigationStack { List { // ForEach iterates over each interview // Thanks to Identifiable, no need to specify id: ForEach(interviews) { interview in InterviewRow(interview: interview) } } .navigationTitle("My Interviews") } } } ``` และนี่คือ view สำหรับแต่ละแถว ซึ่งแยกออกมาเป็นคอมโพเนนต์ต่างหากเพื่อความอ่านง่าย: ```swift // InterviewRow.swift struct InterviewRow: View { let interview: Interview var body: some View { HStack { // Left column: title and subtitle VStack(alignment: .leading, spacing: 4) { Text(interview.technology) .font(.headline) Text(interview.difficulty) .font(.subheadline) .foregroundColor(.secondary) } Spacer() // Badge with question count Text("\(interview.questionCount) Q") .font(.caption) .fontWeight(.medium) .padding(.horizontal, 8) .padding(.vertical, 4) .background(Color.blue.opacity(0.1)) .cornerRadius(8) } .padding(.vertical, 4) } } ``` การแยก `InterviewRow` ออกเป็น struct ต่างหากทำให้โค้ดอ่านง่ายขึ้นและนำกลับมาใช้ซ้ำได้ ถือเป็นแนวปฏิบัติที่ดีของ SwiftUI ### การเพิ่มการดำเนินการ: ลบและจัดเรียงใหม่ รายการใน iOS รองรับการลบ (ปัดซ้าย) และการจัดเรียงใหม่ (ลากและวาง) โดย native SwiftUI ทำให้สิ่งนี้ง่ายมากด้วย `.onDelete` และ `.onMove`: ```swift // InterviewListView.swift (enhanced version) struct InterviewListView: View { @State private var interviews = [/* ... data ... */] var body: some View { NavigationStack { List { ForEach(interviews) { interview in InterviewRow(interview: interview) } // Swipe to delete .onDelete(perform: deleteInterview) // Drag and drop to reorder .onMove(perform: moveInterview) } .navigationTitle("My Interviews") .toolbar { // "Edit" button that activates edit mode EditButton() } } } // Deletes elements at specified indices private func deleteInterview(at offsets: IndexSet) { interviews.remove(atOffsets: offsets) } // Moves elements from one position to another private func moveInterview(from source: IndexSet, to destination: Int) { interviews.move(fromOffsets: source, toOffset: destination) } } ``` เพียง 4 บรรทัดโค้ดเพิ่มเติม (`.onDelete`, `.onMove`, `EditButton` และสองฟังก์ชัน) ก็ทำให้รายการโต้ตอบได้อย่างเต็มรูปแบบ นี่คือพลังของ SwiftUI ## การสร้างแอนิเมชัน SwiftUI มีความเชี่ยวชาญด้านแอนิเมชัน ต่างจาก UIKit ที่ต้องเขียนโค้ดจำนวนมากสำหรับแอนิเมชัน ในที่นี้ทุกอย่างเป็นแบบ declarative: อธิบาย state สุดท้ายแล้ว SwiftUI จะสร้างแอนิเมชันการเปลี่ยนผ่านให้ ### แอนิเมชันโดยนัยด้วย withAnimation วิธีที่ง่ายที่สุดในการสร้างแอนิเมชันคือครอบการเปลี่ยนแปลง state ด้วย `withAnimation` โดย SwiftUI จะตรวจจับสิ่งที่เปลี่ยนแปลงและสร้างแอนิเมชันให้คุณสมบัติที่ได้รับผลกระทบโดยอัตโนมัติ ```swift // AnimatedCard.swift struct AnimatedCard: View { @State private var isExpanded = false var body: some View { VStack { RoundedRectangle(cornerRadius: 20) .fill(.blue) // Dimensions change based on state .frame( width: isExpanded ? 300 : 150, height: isExpanded ? 200 : 100 ) Button(isExpanded ? "Collapse" : "Expand") { // withAnimation animates ALL visual changes // resulting from this state change withAnimation(.spring(response: 0.5, dampingFraction: 0.7)) { isExpanded.toggle() } } .padding(.top) } } } ``` เมื่อกดปุ่ม `isExpanded` จะสลับค่า ด้วย `withAnimation` การเปลี่ยนแปลงขนาดของสี่เหลี่ยมจะถูกสร้างแอนิเมชันด้วยเอฟเฟกต์สปริง ไม่จำเป็นต้องระบุว่าจะสร้างแอนิเมชันอะไร SwiftUI จะจัดการให้เอง ### Transition: แอนิเมชันการปรากฏและหายไป Transition กำหนดวิธีที่ view ปรากฏหรือหายไป โดยค่าเริ่มต้นจะเป็นการจางหาย (`opacity`) แต่สามารถปรับแต่งได้: ```swift // TransitionDemo.swift struct TransitionDemo: View { @State private var showDetails = false var body: some View { VStack(spacing: 20) { Button("Show Details") { withAnimation(.easeInOut(duration: 0.3)) { showDetails.toggle() } } // This view appears/disappears with a transition if showDetails { DetailCard() // Asymmetric transition: different for entry and exit .transition( .asymmetric( insertion: .scale.combined(with: .opacity), // Entry: zoom + fade removal: .slide // Exit: slide ) ) } } } } struct DetailCard: View { var body: some View { VStack(alignment: .leading, spacing: 12) { Text("Interview Details") .font(.headline) Text("25 questions • 45 minutes") .foregroundColor(.secondary) } .padding() .background(Color(.systemGray6)) .cornerRadius(16) } } ``` การ์ดปรากฏด้วยเอฟเฟกต์ซูมจากตรงกลาง (scale + opacity) และหายไปด้วยการเลื่อนไปด้านข้าง (slide) แอนิเมชันเล็กเหล่านี้ทำให้อินเทอร์เฟซดูมีชีวิตชีวาและเป็นมืออาชีพ > **Performance** > > SwiftUI ปรับแต่งแอนิเมชันโดยอัตโนมัติ แนะนำให้ใช้ `.spring()` เพื่อให้ได้ความรู้สึกที่เป็นธรรมชาติ และหลีกเลี่ยงแอนิเมชันที่ยาวเกินไป (> 0.5 วินาที) ซึ่งอาจสร้างความรำคาญให้ผู้ใช้ ## การโหลดข้อมูลแบบ Asynchronous ในแอปพลิเคชันจริง ข้อมูลมักมาจาก API และ Swift Concurrency (async/await) ผสานกับ SwiftUI ได้อย่างสมบูรณ์ผ่าน modifier `.task` ### รูปแบบ Loading / Error / Success นี่คือรูปแบบมาตรฐานสำหรับแสดงข้อมูลจาก API โดยจัดการสาม state: กำลังโหลด เกิดข้อผิดพลาด และสำเร็จ ```swift // AsyncDataView.swift struct AsyncDataView: View { @State private var questions: [Question] = [] @State private var isLoading = true @State private var errorMessage: String? var body: some View { Group { if isLoading { // State: loading in progress ProgressView("Loading...") } else if let error = errorMessage { // State: error VStack(spacing: 16) { Image(systemName: "exclamationmark.triangle") .font(.largeTitle) .foregroundColor(.orange) Text(error) Button("Retry") { Task { await loadQuestions() } } } } else { // State: success, display data List(questions) { question in Text(question.title) } } } // .task runs automatically when the view appears .task { await loadQuestions() } // Pull-to-refresh .refreshable { await loadQuestions() } } private func loadQuestions() async { isLoading = true errorMessage = nil do { // Async API call questions = try await QuestionService.shared.fetchQuestions() } catch { errorMessage = "Unable to load questions" } isLoading = false } } ``` Modifier `.task` มีความสำคัญอย่างยิ่ง: เริ่มทำงาน async เมื่อ view ปรากฏ และยกเลิกโดยอัตโนมัติเมื่อ view หายไป ปัญหา memory leak จึงเป็นไปไม่ได้ ## บทสรุป SwiftUI กลายเป็นส่วนที่ขาดไม่ได้ของการพัฒนา iOS สมัยใหม่ ด้วย iOS 18 เฟรมเวิร์กนี้มีความสมบูรณ์เพียงพอสำหรับแอปพลิเคชัน production ### ประเด็นสำคัญ - **รูปแบบ Declarative**: อธิบายผลลัพธ์ที่ต้องการ ไม่ใช่วิธีการสร้าง - **@State และ @Binding**: จัดการ state แบบ reactive และส่งต่อระหว่าง view - **Stacks**: ผสมผสาน VStack, HStack และ ZStack สำหรับเลย์เอาต์ที่ยืดหยุ่น - **List**: แสดงคอลเลกชันด้วยโค้ดน้อยที่สุดพร้อมการโต้ตอบแบบ native - **แอนิเมชัน**: ใช้ `withAnimation` สำหรับการเปลี่ยนผ่านที่ราบรื่นแบบอัตโนมัติ - **Async/await**: โหลดข้อมูลด้วย `.task` และจัดการ state ของ loading/error ### รายการตรวจสอบ - เข้าใจความแตกต่างระหว่าง imperative (UIKit) และ declarative (SwiftUI) - เชี่ยวชาญ modifier และลำดับการใช้งาน - รู้ว่าเมื่อไหร่ควรใช้ @State, @Binding หรือ @Observable - สร้างเลย์เอาต์ด้วย Stacks - สร้างรายการพร้อมการดำเนินการ (ลบ ย้าย) - สร้างแอนิเมชันการเปลี่ยนแปลง state ด้วย withAnimation SwiftUI เปิดประตูสู่การสร้างแอปพลิเคชันที่สวยงามทั่วทั้งระบบนิเวศ Apple วิธีที่ดีที่สุดในการเรียนรู้คือการลงมือปฏิบัติ: สร้างโปรเจกต์ส่วนตัวเล็กๆ แล้วทดลองกับแต่ละแนวคิดจากบทความนี้ --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/ios/swiftui-building-modern-interfaces