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

SwiftUI ได้เปลี่ยนแปลงวิธีการพัฒนาอินเทอร์เฟซบนแพลตฟอร์ม Apple อย่างสิ้นเชิง ด้วยไวยากรณ์แบบ declarative และการผสานกับระบบ native เฟรมเวิร์กนี้ช่วยให้สร้างแอปพลิเคชันที่สวยงามด้วยโค้ดที่น้อยกว่าที่เคย iOS 18 นำเสนอการปรับปรุงประสิทธิภาพที่สำคัญพร้อมความสามารถใหม่หลายประการ
SwiftUI มีความสมบูรณ์แล้วใน iOS 18 โดยมีการ render ที่ถูกปรับให้เหมาะสม การจัดการหน่วยความจำที่ดีขึ้น และการผสาน UIKit ที่ง่ายขึ้น ถือเป็นมาตรฐานสำหรับแอปพลิเคชัน iOS ใหม่
ทำความเข้าใจรูปแบบ Declarative
ก่อนเริ่มเขียนโค้ด ควรทำความเข้าใจว่า SwiftUI แตกต่างจากเฟรมเวิร์กเดิมอย่างไร ใน UIKit (เฟรมเวิร์กรุ่นก่อน) นักพัฒนาต้องบอก iOS วิธีการสร้างอินเทอร์เฟซทีละขั้นตอน เช่น "สร้าง label วางไว้ตรงนี้ เปลี่ยนสีเมื่อผู้ใช้กดปุ่ม" นี่คือรูปแบบ imperative
SwiftUI ทำงานแตกต่างออกไป นักพัฒนาจะอธิบายสิ่งที่ต้องการแสดงผล แล้วเฟรมเวิร์กจะจัดการส่วนที่เหลือเอง ความแตกต่างนี้เปรียบได้กับการนำทาง GPS: รูปแบบ imperative ให้คำแนะนำเลี้ยวทีละจุด ส่วนรูปแบบ declarative เพียงระบุจุดหมายปลายทาง
View แรก
ใน SwiftUI ทุกองค์ประกอบของอินเทอร์เฟซคือ View โดย View เป็น struct ที่อธิบายสิ่งที่ควรแสดงบนหน้าจอ ต่อไปนี้คือหน้าจอแรกที่มีหัวข้อ หัวข้อย่อย และปุ่ม:
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 เดิม
ตัวอย่างที่แสดงให้เห็นว่าลำดับสำคัญอย่างไร:
// 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 ทั้งสามแบบ เป้าหมายคือแสดงรูปภาพพร้อมตรายืนยันตัวตน ชื่อ และตำแหน่งของผู้ใช้:
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 จะอยู่ทางขวา
ใช้ ⌘ + 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 เดียว
มาสร้างตัวนับแบบโต้ตอบเพื่อทำความเข้าใจกลไก:
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 ที่มีอยู่
ตัวอย่างที่เป็นรูปธรรม: ช่องกรอกชื่อผู้ใช้พร้อมการตรวจสอบแบบเรียลไทม์
// 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 และสามารถแก้ไขค่าได้:
// 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 แม่เห็นการเปลี่ยนแปลงนี้และสามารถนำไปใช้ได้ นี่คือการสื่อสารแบบสองทางที่ชัดเจน
ควรใช้ @State สำหรับ state ที่เรียบง่ายและอยู่ภายในเท่านั้น สำหรับข้อมูลที่แชร์ระหว่างหลายหน้าจอหรือลอจิกที่ซับซ้อน แนะนำให้ใช้ @Observable (iOS 17+) หรือรูปแบบ MVVM
พร้อมที่จะพิชิตการสัมภาษณ์ iOS แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
การแสดงรายการแบบไดนามิก
รายการปรากฏอยู่ทุกที่ในแอปพลิเคชันมือถือ SwiftUI มี List สำหรับแสดงคอลเลกชันข้อมูลพร้อมสไตล์ native ของ iOS (เส้นแบ่ง การดำเนินการเมื่อปัด เป็นต้น)
การสร้างรายการแบบง่าย
การแสดงรายการต้องมีสองสิ่ง: ข้อมูลและวิธีระบุตัวตน โปรโตคอล Identifiable ช่วยให้ SwiftUI ทราบว่า view ใดสอดคล้องกับข้อมูลใด
เริ่มจากการกำหนดโมเดลข้อมูล:
// 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 และสร้างแถวสำหรับแต่ละรายการ:
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 สำหรับแต่ละแถว ซึ่งแยกออกมาเป็นคอมโพเนนต์ต่างหากเพื่อความอ่านง่าย:
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:
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 จะตรวจจับสิ่งที่เปลี่ยนแปลงและสร้างแอนิเมชันให้คุณสมบัติที่ได้รับผลกระทบโดยอัตโนมัติ
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) แต่สามารถปรับแต่งได้:
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) แอนิเมชันเล็กเหล่านี้ทำให้อินเทอร์เฟซดูมีชีวิตชีวาและเป็นมืออาชีพ
SwiftUI ปรับแต่งแอนิเมชันโดยอัตโนมัติ แนะนำให้ใช้ .spring() เพื่อให้ได้ความรู้สึกที่เป็นธรรมชาติ และหลีกเลี่ยงแอนิเมชันที่ยาวเกินไป (> 0.5 วินาที) ซึ่งอาจสร้างความรำคาญให้ผู้ใช้
การโหลดข้อมูลแบบ Asynchronous
ในแอปพลิเคชันจริง ข้อมูลมักมาจาก API และ Swift Concurrency (async/await) ผสานกับ SwiftUI ได้อย่างสมบูรณ์ผ่าน modifier .task
รูปแบบ Loading / Error / Success
นี่คือรูปแบบมาตรฐานสำหรับแสดงข้อมูลจาก API โดยจัดการสาม state: กำลังโหลด เกิดข้อผิดพลาด และสำเร็จ
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 วิธีที่ดีที่สุดในการเรียนรู้คือการลงมือปฏิบัติ: สร้างโปรเจกต์ส่วนตัวเล็กๆ แล้วทดลองกับแต่ละแนวคิดจากบทความนี้
แท็ก
แชร์
บทความที่เกี่ยวข้อง

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

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

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