MVVM in SwiftUI
สถาปัตยกรรม MVVM - แนวคิดสำคัญสำหรับผู้เริ่มต้นพัฒนาแอปด้วย SwiftUI
ในการพัฒนาแอปพลิเคชันสมัยใหม่ นักพัฒนาจำเป็นต้องออกแบบโครงสร้างของโปรแกรมให้สามารถจัดการข้อมูล ตรรกะ และส่วนติดต่อผู้ใช้ได้อย่างเป็นระบบ โดยเฉพาะอย่างยิ่งเมื่อแอปมีความซับซ้อนเพิ่มขึ้น SwiftUI ช่วยให้การสร้าง UI ทำได้สะดวกขึ้น แต่การจัดวางตำแหน่งของ State และ Logic อย่างถูกต้องยังคงเป็นปัจจัยสำคัญที่ส่งผลต่อคุณภาพของซอฟต์แวร์โดยตรง สถาปัตยกรรมแบบ Model–View–ViewModel (MVVM) จึงเป็นแนวทางที่เหมาะสมสำหรับการพัฒนาแอปใน SwiftUI เพราะช่วยแยกความรับผิดชอบของแต่ละส่วนออกจากกันอย่างชัดเจนตามหลักการ Separation of Concerns (SoC) ซึ่งเป็นหลักการพื้นฐานของการออกแบบซอฟต์แวร์คุณภาพสูง
เมื่อใช้ SwiftUI ในการสร้างส่วนติดต่อผู้ใช้ นักพัฒนาจะต้องทำงานกับเฟรมเวิร์กที่สนับสนุนแนวคิด Declarative UI ซึ่งกำหนดให้ UI เป็นผลลัพธ์ของ “State” หรือสถานะของข้อมูล โดยหากข้อมูลมีการเปลี่ยนแปลง UI ก็จะเปลี่ยนตามโดยอัตโนมัติ การที่ UI ปรับการแสดงผลให้สอดคล้องกับสถานะของข้อมูลได้นั้นต้องอาศัยโครงสร้างที่สนับสนุนแนวคิด Reactive Programming และกลไกสำคัญก็คือ Combine ซึ่งจะทำงานผ่านองค์ประกอบอย่าง ObservableObject, @Published และ @StateObject ซึ่งช่วยให้ระบบสามารถตอบสนองต่อข้อมูลที่เปลี่ยนแปลงได้ทันที โดยนักพัฒนาไม่ต้องจัดการด้วยการเขียนคำสั่งแบบเดิมที่มีความยุ่งยาก
บทความนี้จะอธิบายถึงเหตุผลที่ต้องใช้สถาปัตยกรรม MVVM และบทบาทของ Combine รวมถึงวิธีนำแนวคิดเหล่านี้ไปใช้ในการพัฒนาแอปด้วย SwiftUI พร้อมตัวอย่างประกอบอย่างชัดเจน
ปัญหา: การพัฒนาแอปโดยไม่ใช้สถาปัตยกรรม MVVM

ในการพัฒนาแอปด้วย SwiftUI นักพัฒนามือใหม่มักเริ่มต้นด้วยการเขียนโค้ดทั้งหมดเอาไว้รวมกันภายใน View ไม่ว่าจะเป็นการจัดการข้อมูล การคำนวณ การจัดเก็บค่า ตลอดจนการตอบสนองต่อการกระทำของผู้ใช้ โดยเฉพาะเมื่อแอปมีขนาดเล็กหรือหน้าจอไม่ซับซ้อน ตัวอย่างเช่น
import SwiftUI
struct ContactView: View {
// เก็บข้อมูล Contact ของสมาชิกไว้ใน View (ไม่แยก Model)
@State private var name: String = "Jessica Anderson"
@State private var dateOfBirth: String = "28 Aug 1979"
@State private var gender: String = "Female"
@State private var occupation: String = "Artist"
@State private var salary: Int = 32000
@State private var statusOfMembership: Bool = true
var body: some View {
VStack(alignment: .leading, spacing: 8) {
// การแสดงผล UI
Text("Name: \(name)")
Text("Date of Birth: \(dateOfBirth)")
Text("Gender: \(gender)")
Text("Occupation: \(occupation)")
Text("Salary: \(salary) THB")
Text("Status: \(statusOfMembership ? "Active" : "Inactive")")
.bold()
.foregroundColor(statusOfMembership ? .green : .red)
.padding(.top, 10)
// ปุ่มที่มี Logic อยู่ใน View โดยตรง
Button("Change Status") {
changeStatus() // UI ควบคุม Logic เอง
}
.buttonStyle(.borderedProminent)
.padding(.top, 12)
}
.padding()
}
// Logic: การเปลี่ยนสถานะสมาชิก ถูกสร้างอยู่ใน View
func changeStatus() {
statusOfMembership.toggle()
}
}
แม้ว่าโค้ดลักษณะนี้จะเขียนได้ง่ายและมองเห็นภาพรวมได้ในไฟล์เดียว แต่หากมองในมุมของสถาปัตยกรรมแล้ว จะพบว่า มันรวมเอา “ทุกอย่าง” เข้าไปอยู่ในวัตถุเพียงตัวเดียว ไม่ว่าจะเป็นข้อมูลสมาชิก (ชื่อ วันเกิด เพศ อาชีพ รายได้ สถานะการเป็นสมาชิก) หรือฟังก์ชันสำหรับเปลี่ยนสถานะสมาชิก หากในอนาคต เราจำเป็นต้องเพิ่มรายละเอียดอื่นๆ ลงไปในโปรแกรม เช่น การเก็บข้อมูลที่อยู่ หรือ การคำนวณค่าดัชนีมวลกาย (BMI) โค้ดใน ContactView ก็จะยิ่งมีความยาวมากขึ้นและมีแนวโน้มในการเกิดความซ้ำซ้อนหรือข้อผิดพลาดได้ง่าย
การสร้าง View ที่กำหนดให้รับภาระหลายอย่างมากเกินไปเช่นนี้ มักถูกเรียกว่า Massive View หรือ Massive Object ซึ่งเป็นจุดเริ่มต้นของปัญหาด้านการออกแบบสถาปัตยกรรม เช่น
ความซับซ้อนของโค้ดและความยากในการดูแลรักษา - เมื่อโค้ดในการส่วนติดต่อผู้ใช้และการควบคุมขั้นตอนการทำงานของโปรแกรม (Business logic) อยู่ในไฟล์เดียวกัน โค้ดจะเติบโตอย่างรวดเร็วและยากต่อการทำความเข้าใจ นักพัฒนาต้องไล่ดูโค้ดที่ยาวและปนกันหลายส่วน ทำให้เพิ่มความเสี่ยงในการทำให้ส่วนอื่นเสียหายโดยไม่ตั้งใจ
การขยายฟีเจอร์ใหม่ทำได้ยาก - การแก้ไขหรือเพิ่มฟีเจอร์ใน View ที่มีทั้ง Logic และ UI ปนกันอยู่จะกระทบส่วนอื่นได้ง่าย การเปลี่ยนแปลงเพียงเล็กน้อยอาจส่งผลเสียต่อส่วนอื่นของแอปที่ไม่เกี่ยวข้อง
ไม่สามารถทดสอบ Logic ได้อย่างอิสระ - ขั้นตอนการทำงานที่ปะปนอยู่ใน View ไม่สามารถทดสอบด้วย Unit Test ได้ง่าย ๆ เพราะ View ของ SwiftUI ไม่ได้ถูกออกแบบมาให้ทดสอบโดยตรง การแยก Logic ออกไปใน ViewModel ช่วยให้การทดสอบเป็นไปได้อย่างชัดเจนและเป็นระบบ
การแชร์ข้อมูลระหว่างหลายหน้าจอมีความยุ่งยาก - เมื่อข้อมูลกระจัดกระจายอยู่ใน View หลายตัว ทำให้การแชร์ข้อมูลระหว่างหน้าจอหรือ Component เป็นเรื่องซับซ้อนและเพิ่มความเสี่ยงต่อความไม่สอดคล้องของข้อมูล
โค้ดไม่สามารถนำกลับมาใช้ซ้ำได้ - เมื่อ View บรรจุทั้งข้อมูลและ Logic การนำส่วนของระบบกลับมาใช้ซ้ำมักเป็นไปได้ยาก เนื่องจากส่วนประกอบไม่ถูกออกแบบมาให้เป็นอิสระต่อกัน
ปัญหาเหล่านี้เป็นเหตุผลสำคัญที่นำไปสู่การใช้ สถาปัตยกรรมแบบ Model–View–ViewModel (MVVM) ซึ่งเหมาะสมอย่างยิ่งสำหรับการพัฒนาแอปพลิเคชันด้วย SwiftUI เพราะเราสามารถแยกความรับผิดชอบระหว่างโครงสร้างข้อมูล (Model) ส่วนติดต่อผู้ใช้ (View) ขั้นตอนการทำงานและการจัดการสถานะ (ViewModel) ออกจากกันอย่างชัดเจน
หลักการและองค์ประกอบของสถาปัตยกรรม MVVM

สถาปัตยกรรม Model–View–ViewModel (MVVM) ช่วยจัดระเบียบโค้ดและแยกความรับผิดชอบอย่างเป็นระบบ ตามหลัก Separation of Concerns (SoC)โดยประกอบด้วย 3 ส่วนสำคัญ ได้แก่
Model — โครงสร้างข้อมูล เป็นส่วนที่ทำหน้าที่เก็บรายละเอียดของโครงสร้างที่ใช้แทนวัตถุหรือสิ่งที่สนใจ เช่น รายละเอียดเกี่ยวกับสมาชิก ผู้ใช้ ผลิตภัณฑ์ หรือข้อมูลอื่นๆ โดยไม่มีขั้นตอนการทำงานที่ซับซ้อนอยู่ภายใน มักถูกนิยามเป็น
structหรือclassและไม่มีหน้าที่เกี่ยวข้องกับการแสดงผล
import Foundation
struct Contact {
let name: String
let dateOfBirth: String
let gender: String
var weight: Double
var height: Double
var occupation: String
var salary: Int
var statusOfMembership: Bool
}ViewModel — จัดการตรรกะ ขั้นตอนการทำงาน และสถานะของข้อมูล เป็นตัวกลางระหว่าง Model และ View โดยมีหน้าที่ในการจัดการเกี่ยวกับการเปลี่ยนแปลงสถานะของข้อมูล การประมวลผล และการเตรียมข้อมูลเพื่อให้ View ใช้ในการแสดงผล ภายใน ViewModel จะมีการเรียกใช้เฟรมเวิร์กชื่อ Combine เพื่อแจ้งให้ View มีการปรับปรุง UI เมื่อสถานะของข้อมูลมีการเปลี่ยนแปลง
import SwiftUI
import Combine
class ContactViewModel: ObservableObject {
@Published var clubMember = Contact(
name: "Jessica Anderson",
dateOfBirth: "28 Aug 1979",
gender: "Female",
weight: 65.7,
height: 162.5,
occupation: "Artist",
salary: 32000,
statusOfMembership: true
)
func changeStatus() {
clubMember.statusOfMembership.toggle()
}
}View — ส่วนในการโต้ตอบกับผู้ใช้ ทำหน้าที่สร้างหน้าจอและแสดงข้อมูลที่จัดเตรียมโดย ViewModel โดยไม่มีการบรรจุขั้นตอนการทำงานทางตรรกะภายใน ทั้งนี้เพื่อรักษาความเรียบง่ายของ UI
import SwiftUI
struct ContactView: View {
@StateObject private var viewModel = ContactViewModel()
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Name: \(viewModel.clubMember.name)")
Text("Date of Birth: \(viewModel.clubMember.dateOfBirth)")
Text("Gender: \(viewModel.clubMember.gender)")
Text("Occupation: \(viewModel.clubMember.occupation)")
Text("Salary: \(viewModel.clubMember.salary) THB")
Text("Status: \(viewModel.clubMember.statusOfMembership ? "Active" : "Inactive")")
.bold()
.foregroundColor(
viewModel.clubMember.statusOfMembership ? .green : .red
)
.padding(.top, 10)
Button("Change Status") {
viewModel.changeStatus()
}
.buttonStyle(.borderedProminent)
.padding(.top, 12)
}
.padding()
}
}
Combine: กลไกสำคัญของการอัปเดต UI แบบ Reactive
Reactive Programming เป็นแนวคิดการเขียนโปรแกรมที่ให้ความสำคัญกับ “ข้อมูลที่เปลี่ยนแปลง” โดยการมองข้อมูลเป็น กระแสข้อมูล (data stream) ที่ไหลผ่านระบบ และส่วนต่างๆ ของโปรแกรมจะต้อง “ตอบสนอง” ต่อการเปลี่ยนแปลงของข้อมูลแบบอัตโนมัติ โดยไม่จำเป็นต้องสั่งให้อัปเดตด้วยคำสั่งเชิงสั่งการแบบเดิม (imperative) เช่น reloadData() หรือ refreshUI()
หัวใจสำคัญของ Reactive Programming คือ Publisher–Subscriber
Publisher คือ แหล่งข้อมูลที่เกิดการเปลี่ยนแปลง
Subscriber คือ View ที่ต้องการรับรู้ถึงการเปลี่ยนแปลงของข้อมูลนั้น
เมื่อ Publisher เปลี่ยน → Subscriber ได้รับข้อมูล → UI บนหน้าจอก็จะเปลี่ยนตามทันที
แม้ SwiftUI จะมีความสามารถในการวาด UI แบบ Declarative แต่เบื้องหลังการอัปเดต UI ตามข้อมูลที่เปลี่ยนแปลงนั้น มาจากเฟรมเวิร์ก Combine ซึ่งเป็นกลไกหลักที่ทำหน้าที่จัดการกระแสข้อมูลแบบ Reactive ให้กับ SwiftUI โดยมีองค์ประกอบสำคัญดังนี้:
ObservableObject – ตัวแจ้งเตือนว่ามีข้อมูลเปลี่ยนแปลง โดยเมื่อ ViewModel ประกาศตัวเองว่าเป็น
ObservableObjectมันจะมีช่องทางสำหรับแจ้ง SwiftUI ว่า “ในตัวฉันมีข้อมูลเปลี่ยนแล้วนะ”@Published – ตัวประกาศค่าแบบ Reactive – เมื่อใส่
@Publishedหน้าตัวแปรใน ViewModel ตัวแปรนั้นจะถูกจับตามองโดย Combine ซึ่งหมายความว่า เมื่อค่าของตัวแปรนี้เปลี่ยน → Combine จะส่งสัญญาณให้ View ทราบทันที@StateObject – การใช้
@StateObjectกำกับตัวแปร viewModel ซึ่งเป็นอินแสตนท์ของ ViewModel ทำให้ View ทำหน้าที่รับสัญญาณผ่านการสมัครเป็น Subscriber ที่จะรับสัญญาณจาก ViewModel โดยเมื่อข้อมูลที่ถูกใส่@Publishedไว้มีการเปลี่ยนแปลง SwiftUI จะ re-render UI โดยอัตโนมัติ
หากกล่าวอย่างกระชับและเข้าใจง่ายก็จะสามารถอธิบายได้ว่า
Reactive Programming คือ แนวคิดในการเขียนโปรแกรมที่กำหนดให้ UI มีการตอบสนองต่อการเปลี่ยนแปลงของข้อมูลแบบอัตโนมัติ
Combine คือ เครื่องมือที่ทำให้แนวคิดนี้เกิดขึ้นจริง
MVVM คือ สถาปัตยกรรมที่จัดวางบทบาทของส่วนต่าง ๆ อย่างเป็นระเบียบ
SwiftUI คือ เครื่องมือในการสร้างส่วนแสดงผลที่ตอบสนองต่อข้อมูลได้แบบ Reactive
การทำความเข้าใจแนวคิดของสถาปัตยกรรม MVVM ควบคู่กับการใช้ Combine จึงเป็นพื้นฐานสำคัญของการสร้างการตอบสนองต่อการเปลี่ยนแปลงของข้อมูลแบบอัตโนมัติ จะช่วยให้นักศึกษาสามารถใช้ SwiftUI ในการออกแบบและพัฒนาแอปพลิเคชันที่ดูแลง่าย ขยายเพิ่มเติมได้ในอนาคต และรองรับการทำงานในระบบที่มีความซับซ้อนสูงได้อย่างดี
ลองทำ : ระบบติดตามคะแนนผู้เล่น (Player Score Tracker)
คราวนี้เราลองมาออกแบบ "ระบบติดตามคะแนนผู้เล่นอย่างง่าย" ประกอบด้วย Player 1 คน ซึ่งมีชื่อผู้เล่น และคะแนนเริ่มต้นเป็น 0 จากนั้นสร้างปุ่มเพื่อเพิ่มคะแนนและปุ่มเพื่อลดคะแนน โดยมีเงื่อนไขว่า คะแนนต้องไม่ติดลบ
หมายเหตุ: เมื่อคะแนนมีการเปลี่ยนแปลง UI ต้องอัปเดตทันที (อาศัย Combine)
ขั้นตอนที่ 1 : การสร้าง Model ของผู้เล่น
import Foundation
struct Player {
var name: String
var score: Int
}ขั้นตอนที่ 2 : สร้าง ViewModel ที่ใช้ Combine
import SwiftUI
import Combine
class PlayerViewModel: ObservableObject {
@Published var player = Player(name: "Player One", score: 0)
// การเพิ่มคะแนน
func increaseScore() {
player.score += 1
}
// การลดคะแนน
func decreaseScore() {
if player.score > 0 {
player.score -= 1
}
}
}ขั้นตอนที่ 3 : สร้าง View และผูกเข้ากับกับ ViewModel
import SwiftUI
struct PlayerView: View {
@StateObject private var viewModel = PlayerViewModel()
var body: some View {
VStack(spacing: 20) {
Text(viewModel.player.name)
.font(.title)
Text("Score: \(viewModel.player.score)")
.font(.largeTitle)
.bold()
HStack {
Button(action: {
viewModel.decreaseScore()
}) {
Text("-")
.font(.largeTitle)
.frame(width: 60, height: 60)
}
.buttonStyle(.bordered)
Button(action: {
viewModel.increaseScore()
}) {
Text("+")
.font(.largeTitle)
.frame(width: 60, height: 60)
}
.buttonStyle(.borderedProminent)
}
}
.padding()
}
}
หากเราต้องการเพิ่มกฎว่า "ห้ามให้คะแนนเกิน 10" เราจะแก้โค้ดที่ ViewModel เพียงที่เดียว เนื่องจากเป็นส่วนที่ควบคุมตรรกะและขั้นตอนการทำงาน (Business logic) โดยไม่ต้องแก้อะไรใน View หรือ Model
func increaseScore() {
if player.score < 10 {
player.score += 1
}
}หากต้องการเพิ่มปุ่ม Reset Score ก็ทำได้ง่าย โดยเพิ่มฟังก์ชัน resetScore() ลงใน ViewModel
func resetScore() {
player.score = 0
}และสร้างปุ่ม Reset Score ใน View ซึ่งทำหน้าที่ในการโต้ตอบกับผู้ใช้ โดยกำหนดให้มีการเรียกใช้ resetScore() จาก ViewModel
Button("Reset Score") {
viewModel.resetScore()
}
.buttonStyle(.bordered)
.padding(.top, 12)จะเห็นได้ว่า สถาปัตยกรรม MVVM ช่วยให้การพัฒนาแอปด้วย SwiftUI มีความเป็นระเบียบ โดยแยกบทบาทระหว่าง Model, View และ ViewModel ออกจากกันอย่างชัดเจน ทำให้โค้ดอ่านง่าย แก้ไขง่าย และรองรับการปรับปรุงหรือขยายคุณสมบัติของแอปในอนาคตได้ดี ในขณะเดียวกัน Combine ก็ทำหน้าที่เป็นกลไกสำคัญที่เชื่อมการเปลี่ยนแปลงของข้อมูลใน ViewModel กับการอัปเดต UI ใน View ผ่านแนวคิด Reactive Programming
เมื่อใช้ MVVM ร่วมกับ Combine นักพัฒนาจะได้โครงสร้างแอปที่สามารถจัดการสถานะของข้อมูลและอัปเดต UI ได้ทันทีโดยอัตโนมัติ เมื่อข้อมูลมีการเปลี่ยนแปลงสถานะ ลดความซับซ้อนในการเขียนโค้ด และเพิ่มความสามารถในการดูแลรักษาระยะยาว ซึ่งทั้งหมดนี้ทำให้การพัฒนาแอปด้วย SwiftUI มีประสิทธิภาพ ยืดหยุ่น และเหมาะกับโปรเจกต์ตั้งแต่ขนาดเล็กไปจนถึงขนาดใหญ่
แนวทางในการจัดกิจกรรมการเรียนรู้
Last updated