Thinking in Data
"คิดเป็นข้อมูล" ก่อนการเขียนโค้ด
การพัฒนา Mobile Application ได้กลายเป็นทักษะสำคัญของนักพัฒนาซอฟต์แวร์ในยุคปัจจุบัน อย่างไรก็ตาม แนวทางการเรียนรู้หรือการพัฒนาซึ่งเริ่มต้นจากการออกแบบหน้าจอและการเขียนโค้ดเชิงโต้ตอบ อาจทำให้นักพัฒนามองแอปพลิเคชันเป็นเพียงชุดของ UI มากกว่าการมองเป็นระบบของข้อมูลที่มีโครงสร้าง ในเชิงวิศวกรรมซอฟต์แวร์ แอปพลิเคชันไม่ได้มีแกนอยู่ที่การสร้างการโต้ตอบกับหน้าจอ แต่ควรจะต้องให้ความสำคัญกับข้อมูล ความสัมพันธ์ของข้อมูล และกฎของการเปลี่ยนแปลงสถานะ หากการออกแบบระบบไม่ได้เริ่มจากโครงสร้างข้อมูลที่ชัดเจน ซอฟต์แวร์ที่พัฒนาขึ้นมักขยายต่อได้ยาก มีความซับซ้อนสูง และขาดความสอดคล้องเชิงสถาปัตยกรรม
สำหรับการพัฒนา iOS Application ด้วยภาษา Swift ซึ่งมีระบบชนิดข้อมูลที่เข้มแข็งและส่งเสริมความชัดเจนของโครงสร้าง การออกแบบ Data Model จึงเป็นขั้นตอนพื้นฐานที่กำหนดคุณภาพของระบบในระยะยาว

กิจกรรมในบทความนี้ (ซึ่งเป็นส่วนหนึ่งในหลักสูตร Integrated Data Modeling & SwiftData for iOS Application Development) จึงถูกออกแบบเพื่อปรับกรอบความคิดของผู้เรียน โดยใช้ แนวคิด Thinking in Data เป็นฐานการเรียนรู้ เพื่อให้นักศึกษาสามารถ “คิดโดยใช้ข้อมูลเป็นฐาน” ก่อนการเขียนโค้ด ซึ่งถือเป็นสมรรถนะสำคัญของนักพัฒนาในยุคที่ข้อมูลเป็นศูนย์กลางของระบบสารสนเทศ
Conceptual Framework ของ Thinking in Data
Thinking in Data เป็นรูปแบบหนึ่งของการคิดเชิงคำนวณ (Computational Thinking) ที่เน้นการทำความเข้าใจ “โครงสร้างและความสัมพันธ์ของข้อมูล” มากกว่า การมุ่งเน้นลำดับขั้นตอนของอัลกอริทึมเพียงอย่างเดียว แนวคิดนี้ตั้งอยู่บนหลักการสำคัญว่า หากเราสามารถออกแบบข้อมูลได้อย่างถูกต้องและมีเหตุผลแล้ว กระบวนการเขียนโค้ดและการจัดการความซับซ้อนของระบบจะเกิดขึ้นได้อย่างเป็นธรรมชาติ ดังนั้น แนวคิด Thinking in Data จึงมีบทบาทในการเปลี่ยนคำถามตั้งต้นจาก “หน้าจอแอปมีอะไรบ้าง” ไปสู่ “ระบบนี้ต้องจัดการข้อมูลอะไร " และ "ข้อมูลเหล่านั้นสัมพันธ์กันอย่างไร” การเปลี่ยนมุมมองดังกล่าวช่วยให้เกิดการพัฒนาทักษะเชิงนามธรรม (abstraction) และทักษะการออกแบบเชิงโครงสร้าง ซึ่งเป็นหัวใจของวิทยาการคอมพิวเตอร์ เพื่อให้แนวคิด Thinking in Data สามารถนำไปใช้ได้อย่างเป็นระบบ โดยกรอบแนวคิดนี้ประกอบด้วย 4 ส่วนหลัก ซึ่งเชื่อมโยงกันเป็นลำดับความคิด ดังนี้

Problem Context: การทำความเข้าใจบริบทของปัญหาหรือสถานการณ์จริงที่แอปพลิเคชันต้องตอบสนอง โดยยังไม่พิจารณาส่วนติดต่อกับผู้ใช้หรือเทคโนโลยีที่ใช้
Conceptual Data Model: การแยกแยะข้อมูลออกเป็น Entity, Attribute และวิเคราะห์ Relationship ในเชิงนามธรรม โดยไม่ผูกติดกับการเขียนโปรแกรมหรือฐานข้อมูลใดๆ
Data-Oriented Application Design : การเชื่อมโยงแบบจำลองข้อมูลเข้ากับการออกแบบแอป โดยแยกบทบาทของ Data Model, UI State และ Persistent Data อย่างชัดเจน
Implementation Readiness : การนำแบบจำลองข้อมูลไปพัฒนาเป็นโค้ดด้วย SwiftUI และ SwiftData
กรอบแนวคิดนี้สะท้อนให้เห็นว่า การเขียนโค้ดและการออกแบบ UI ไม่ใช่จุดเริ่มต้นของการพัฒนาแอป แต่เป็นผลลัพธ์ของกระบวนการคิดเชิงข้อมูลที่มีลำดับและเหตุผลรองรับ
ตัวอย่างการวิเคราะห์โครงสร้างของข้อมูลตามแนวคิด Thinking in Data
กรณีศึกษา: แอปบันทึกรายจ่าย (Expense Tracker Application)
เมื่อกล่าวถึงแอปสำหรับบันทึกรายจ่าย คนส่วนใหญ่มักเริ่มจินตนาการถึงหน้าจอของแอป เช่น หน้าจอแสดงรายการรายจ่าย ปุ่มเพิ่มข้อมูล หรือหน้าสรุปยอดรวมรายเดือน อย่างไรก็ตาม การเริ่มต้นจากภาพของหน้าจอเหล่านี้สะท้อนการคิดเชิง UI มากกว่า การคิดเชิงข้อมูล ซึ่งอาจนำไปสู่การออกแบบระบบที่ขาดโครงสร้างและยากต่อการขยายในอนาคต ดังนั้น แนวคิด Thinking in Data จึงเสนอให้ย้อนกลับมาพิจารณา “ข้อมูล” ที่ระบบต้องรับผิดชอบเป็นลำดับแรก
ขั้นตอนที่ 1: ทำความเข้าใจบริบทของปัญหา (Problem Context) การคิดเชิงข้อมูล เริ่มต้นจากการทำความเข้าใจบริบทของปัญหาในโลกจริง โดยไม่กล่าวถึงเทคโนโลยีหรือส่วนติดต่อกับผู้ใช้ใดๆ แอปบันทึกรายจ่ายมีเป้าหมายเพื่อช่วยให้ผู้ใช้สามารถบันทึก ติดตาม และทบทวนพฤติกรรมการใช้จ่ายของตนเองในช่วงเวลาหนึ่ง ซึ่งสะท้อนกิจกรรมที่เกิดขึ้นซ้ำๆ ในชีวิตประจำวัน ข้อมูลที่เกี่ยวข้องจึงเป็นข้อมูลเชิงเหตุการณ์ (event-based data) ที่เกิดขึ้นตามเวลา การทำความเข้าใจบริบทในลักษณะนี้ช่วยให้เห็นภาพรวมของปัญหา โดยการตั้งคำถามว่า “ระบบต้องจัดการสิ่งใดในโลกจริง” มากกว่าถามว่า “แอปควรมีหน้าจออย่างไร”
ขั้นตอนที่ 2: การสร้างแบบจำลองข้อมูลเชิงแนวคิด (Conceptual Data Model) เมื่อบริบทของปัญหามีความชัดเจนแล้ว ขั้นตอนถัดมา คือ การแยกแยะสิ่งที่ระบบต้องจัดการออกมาเป็นหน่วยข้อมูลเชิงนามธรรม หรือ Entity เพื่อใช้เป็นโครงสร้างพื้นฐานของแบบจำลองข้อมูล สำหรับแอปบันทึกรายจ่าย Entity ที่ชัดเจนที่สุดคือ “รายจ่าย” เนื่องจากเป็นข้อมูลหลักของระบบ ซึ่งจะเกิดขึ้นทุกครั้งที่ผู้ใช้มีการใช้จ่ายเงิน และข้อมูลนี้จะถูกบันทึกและนำไปใช้ในการประมวลผล นอกจากนี้ หากต้องการวิเคราะห์พฤติกรรมการใช้จ่าย ระบบจำเป็นต้องมี กลไกในการจัดกลุ่มรายจ่าย เพื่อทำให้ข้อมูลมีความหมายในเชิงวิเคราะห์และสนับสนุนการตัดสินใจของผู้ใช้ ความต้องการนี้นำไปสู่การระบุ Entity อีกตัวหนึ่ง คือ “หมวดหมู่รายจ่าย” การระบุ Entity ในขั้นนี้ไม่ได้เกิดจากการออกแบบหน้าจอหรือแบบฟอร์ม แต่เกิดจากการพิจารณาเชิงตรรกะว่า ข้อมูลใดมีความสำคัญต่อระบบ และจำเป็นต่อการจัดเก็บ ตีความ และนำไปใช้งานในโลกจริง เมื่อกระบุ Entity ได้แล้ว เราจะเริ่มพิจารณาว่า ข้อมูลหนึ่งรายการควรมีคุณลักษณะ (Attributes) ใดบ้างจึงจะสมบูรณ์ สำหรับรายจ่ายแต่ละรายการ จำเป็นต้องทราบจำนวนเงิน วันที่เกิดรายการ และรายละเอียดประกอบ เพื่อให้สามารถนำไปวิเคราะห์หรือสรุปผลในภายหลังได้ ส่วนหมวดหมู่รายจ่ายอาจมีเพียงชื่อและข้อมูลเชิงอธิบายบางประการ โดยการออกแบบข้อมูลไม่ใช่การใส่ฟิลด์ให้ครบตามแบบฟอร์มในหน้าจอ แต่เป็นการพิจารณาว่า ข้อมูลใดจำเป็นต่อการอธิบายเหตุการณ์และสามารถนำไปใช้งานในโลกจริงได้อย่างมีความหมาย ในการวิเคราะห์ความสัมพันธ์ของข้อมูล (Relationships) เราจะสังเกตได้ว่า รายจ่ายแต่ละรายการย่อมสังกัดหมวดหมู่ใดหมวดหมู่หนึ่ง ขณะที่หมวดหมู่หนึ่งสามารถมีรายจ่ายได้หลายรายการ ความสัมพันธ์ในลักษณะนี้เป็นความสัมพันธ์แบบหนึ่งต่อหลาย (One-to-Many) ซึ่งเกิดจากธรรมชาติของข้อมูล ไม่ได้เกิดจากข้อจำกัดของเทคโนโลยีหรือรูปแบบของการนำเสนอ การตระหนักถึงความสัมพันธ์ของข้อมูลในขั้นนี้เป็นหัวใจสำคัญของแนวคิด Thinking in Data เนื่องจากช่วยให้เราเห็นโครงสร้างของระบบในเชิงนามธรรม ก่อนจะนำไปสู่การออกแบบเชิงเทคนิคในลำดับถัดไป

ขั้นตอนที่ 3: การออกแบบแอปโดยยึดข้อมูลเป็นศูนย์กลาง (Data-Oriented Design) เมื่อแบบจำลองข้อมูลเชิงแนวคิดมีความชัดเจน ขั้นตอนถัดไปคือการเชื่อมโยง Data Model เข้ากับการออกแบบแอปโดยยึดข้อมูลเป็นศูนย์กลาง แนวคิดสำคัญในขั้นนี้คือการแยก Data Model ออกจาก UI State อย่างชัดเจนเพื่อให้โครงสร้างข้อมูลมีเสถียรภาพและไม่ผูกติดกับวิธีการแสดงผล ข้อมูลที่ต้องจัดเก็บจริง เช่น รายการรายจ่ายและหมวดหมู่รายจ่าย ถือเป็น ข้อมูลถาวรของระบบ (Persistent Data) เนื่องจากต้องถูกบันทึกและใช้อ้างอิงในระยะยาว ข้อมูลเหล่านี้ควรถูกออกแบบให้สะท้อนความหมายของเหตุการณ์ในโลกจริง และสามารถนำไปวิเคราะห์หรือประมวลผลต่อได้ ในทางตรงกันข้าม ค่าที่ผู้ใช้กำลังพิมพ์ในช่องกรอกข้อมูล สถานะการเลือกหมวดหมู่ก่อนบันทึก หรือค่าที่ใช้ควบคุมการแสดงผลของหน้าจอ เป็นเพียง สถานะชั่วคราวของส่วนติดต่อผู้ใช้ (UI State) ซึ่งมีหน้าที่สนับสนุนการโต้ตอบระหว่างผู้ใช้กับระบบเท่านั้น และไม่ควรถูกผสมเข้าไปในโครงสร้างข้อมูลหลัก
การแยกบทบาทระหว่างข้อมูลถาวรกับสถานะชั่วคราวเช่นนี้ ช่วยลดความซับซ้อนของระบบ ทำให้ Data Model มีความเป็นอิสระจาก UI และป้องกันไม่ให้ตรรกะของส่วนติดต่อผู้ใช้เข้าไปปะปนกับโครงสร้างข้อมูลหลัก ผลลัพธ์คือระบบที่มีโครงสร้างชัดเจน ขยายต่อได้ง่าย และรองรับการเปลี่ยนแปลงของ UI ได้โดยไม่กระทบต่อแกนข้อมูลของแอป
ขั้นตอนที่ 4: เตรียมความพร้อมสู่การออกแบบแอปเชิงข้อมูล (Implementation Readiness) เมื่อโครงสร้างข้อมูลของระบบมีความชัดเจนและมีการแยกบทบาทระหว่าง Data Model กับ UI State แล้ว ขั้นตอนถัดไปคือ การเตรียมความพร้อมก่อนเข้าสู่การพัฒนาเชิงเทคนิคจริง ซึ่งเป็นช่วงเปลี่ยนผ่านจาก แบบจำลองเชิงแนวคิด ไปสู่ การนำไปใช้งานในระดับโค้ด หัวใจของขั้นนี้ไม่ใช่การเริ่มเขียนโปรแกรมทันที แต่เป็นการตรวจสอบว่าแบบจำลองข้อมูลที่ออกแบบไว้สามารถรองรับการทำงานของระบบได้ครบถ้วน และสามารถแมปไปสู่โครงสร้างในระดับเทคโนโลยีได้อย่างชัดเจน โดยมีประเด็นสำคัญดังนี้
การตรวจสอบความสมบูรณ์ของ Data Model เพื่อให้สามารถรองรับเหตุการณ์ในโลกจริงได้ เช่น รายจ่ายหนึ่งรายการสามารถถูกสร้าง แก้ไข ลบ และเรียกดูได้อย่างชัดเจน หมวดหมู่สามารถเพิ่มใหม่หรือเปลี่ยนชื่อได้โดยไม่กระทบข้อมูลเดิม การทบทวนในขั้นนี้ช่วยลดความเสี่ยงของการต้องปรับโครงสร้างข้อมูลภายหลัง ซึ่งมักมีต้นทุนสูงในระบบที่เริ่มใช้งานแล้ว
การตรวจสอบความสอดคล้องของ Relationships กับพฤติกรรมการใช้งานจริงของผู้ใช้ เช่น รายจ่ายต้องสังกัดหมวดหมู่เสมอหรือไม่ หากหมวดหมู่ถูกลบ รายจ่ายเดิมควรถูกลบตาม ย้ายหมวดหมู่ หรือคงอยู่โดยไม่มีหมวดหมู่ การตัดสินใจเชิงนโยบายข้อมูลเหล่านี้ควรถูกกำหนดให้ชัดก่อนการพัฒนา เพื่อป้องกันความกำกวมในตรรกะของระบบ
การเตรียมแนวทางการแมป Data Model สู่โครงสร้างเชิงเทคนิค เช่น การเลือก Data Schema และวิธีในการจัดเก็บความสัมพันธ์ของ Entity หรือการระบุว่า ข้อมูลใดควรถูกจัดเก็บแบบถาวร และข้อมูลใดควรถูกคำนวณขึ้นใหม่เมื่อใช้งาน การกำหนดแนวทางเหล่านี้ช่วยให้การเขียนโค้ดในลำดับถัดไปเป็นเพียงการ “แปลแบบจำลอง” ไปสู่ภาษาโปรแกรม ไม่ใช่การออกแบบระบบใหม่อีกครั้ง
ตัวอย่างการวิเคราะห์แอปบันทึกรายจ่ายตามแนวคิด Thinking in Data แสดงให้เห็นว่า การเริ่มต้นจากการทำความเข้าใจข้อมูลและความสัมพันธ์ของข้อมูล ช่วยให้ผู้เรียนมองเห็นโครงสร้างของระบบอย่างเป็นระบบและมีเหตุผล การปลูกฝังแนวคิดนี้ตั้งแต่ต้นไม่เพียงช่วยให้การเขียนโค้ดในภายหลังง่ายขึ้นเท่านั้น แต่ยังช่วยสร้างพื้นฐานการออกแบบซอฟต์แวร์ที่ยั่งยืน ซึ่งเป็นเป้าหมายสำคัญของการศึกษาด้านวิทยาการคอมพิวเตอร์
การสร้าง Data Model ด้วย Swift
หลังจากนักศึกษาได้พัฒนาฐานคิดเชิงข้อมูลผ่านแนวคิด Thinking in Data แล้ว สิ่งสำคัญในขั้นตอนถัดไปคือ การแปลงความคิดเชิงนามธรรมเกี่ยวกับข้อมูลให้กลายเป็นโครงสร้างที่สามารถนำไปพัฒนาเป็นซอฟต์แวร์ได้จริง การเปลี่ยนผ่านจาก Conceptual Data Model ไปสู่การพัฒนาเชิงเทคนิค จึงไม่ควรถูกมองเป็นเพียงการ “เริ่มเขียนโค้ด” แต่เป็นกระบวนการเลือกภาษาโปรแกรมที่สามารถทำหน้าที่เป็นสื่อกลางในการถ่ายทอดความหมายของข้อมูลได้อย่างะมีโครงสร้างและถูกต้อง บทความในส่วนนี้จึงขอมุ่งเน้นไปที่การใช้ภาษา Swift ในฐานะภาษาสำหรับการสร้างแบบจำลองข้อมูล (Modeling Language) โดยภาษา Swift ถูกออกแบบให้รองรับแนวคิดด้านความปลอดภัยของข้อมูล (safety) และความชัดเจนของโครงสร้าง (clarity) ผ่านกลไกต่าง ๆ เช่น type system, value semantics และ immutability คุณลักษณะเหล่านี้ทำให้ Swift เหมาะสมอย่างยิ่งสำหรับการใช้เป็นภาษาสำหรับ Data Modeling
หนึ่งในประเด็นสำคัญของ Swift ในการสร้าง Data Model คือ การแยกความแตกต่างระหว่าง struct และ class ซึ่งสะท้อนแนวคิดเรื่อง Value Semantics และ Reference Semantics
โดย
structในภาษา Swift ใช้แนวคิดแบบ Value Semantics กล่าวคือ เมื่อมีการกำหนดค่าหรือส่งต่อข้อมูล จะเกิดการคัดลอกค่า (copy) ทำให้แต่ละอินสแตนซ์มีสถานะเป็นอิสระจากกัน การเปลี่ยนแปลงค่าของอินสแตนซ์หนึ่งจะไม่ส่งผลกระทบต่ออินสแตนซ์อื่นโดยไม่ตั้งใจ คุณสมบัตินี้เอื้อต่อการออกแบบ Data Model ที่มีความปลอดภัย (safety) และลดปัญหาผลข้างเคียง (side effects) ซึ่งเป็นอุปสรรคสำคัญต่อความเข้าใจของผู้เรียนในระยะเริ่มต้น
ในทางตรงกันข้าม
classใช้แนวคิดแบบ Reference Semantics ซึ่งหมายความว่าอินสแตนซ์หลายตัวสามารถอ้างอิงไปยังข้อมูลชุดเดียวกันได้ การเปลี่ยนแปลงผ่านอ้างอิงหนึ่งจะส่งผลกระทบต่ออ้างอิงอื่นทั้งหมด แนวคิดนี้มีความสำคัญในบริบทที่ข้อมูลต้องมีอัตลักษณ์ (identity) และวงจรชีวิต (lifecycle) ที่ชัดเจน เช่น ข้อมูลที่ต้องถูกจัดเก็บถาวร ถูกแก้ไขซ้ำ และถูกใช้งานร่วมกันในหลายส่วนของระบบ
การทำความเข้าใจความแตกต่างระหว่าง Value Semantics และ Reference Semantics จึงไม่ใช่เพียงประเด็นเชิงไวยากรณ์ของภาษา Swift แต่เป็นรากฐานเชิงแนวคิดของการออกแบบ Data Model ในระดับสถาปัตยกรรม แนวทางการนำเสนอในบทความนี้จะขอเริ่มต้นจากการใช้ struct เพื่อสร้างแบบจำลองข้อมูลเชิงแนวคิด และจึงค่อยยกระดับไปสู่การใช้ class เมื่อข้อมูลมีบทบาทเป็นข้อมูลเชิงโดเมนที่ต้องมีความคงอยู่และความสัมพันธ์ในระดับระบบ
ตัวอย่างการสร้าง Data Model ด้วย struct
โครงสร้างนี้สะท้อนแนวคิดว่า Category และ Expense เป็นข้อมูลหนึ่งหน่วยที่มีคุณลักษณะชัดเจน ไม่ขึ้นกับสถานะภายนอก การใช้ struct ทำให้ Entity ทั้งสองอยู่ภายใต้แนวคิด Value Semantics ซึ่งหมายความว่า เมื่อมีการคัดลอกหรือส่งต่อค่าจะเกิดอินสแตนซ์ใหม่ที่เป็นอิสระจากต้นฉบับ การเปลี่ยนแปลงค่าในอินสแตนซ์หนึ่งจะไม่ส่งผลกระทบต่ออินสแตนซ์อื่นโดยไม่ตั้งใจ
การทำให้ Category และ Expense ปฏิบัติตามโปรโตคอล Identifiable หมายความว่า ข้อมูลแต่ละรายการต้องมีคุณสมบัติ id ที่สามารถใช้ระบุเอกลักษณ์ของอินสแตนซ์นั้นโดยไม่ซ้ำกัน ค่าของ id ทำหน้าที่เสมือน Primary Key ของข้อมูล ซึ่งช่วยให้ระบบสามารถแยกแยะข้อมูลแต่ละรายการออกจากกันและสามารถติดตามข้อมูลนั้นเมื่อมีการเปลี่ยนแปลงได้ กล่าวอีกนัยหนึ่ง การใช้ Identifiable ทำให้ Category ไม่ใช่เพียงค่าข้อมูลทั่วไป แต่เป็นข้อมูลที่มี “ตัวตนในระบบ”
การกำหนดให้ Category ปฏิบัติตามโปรโตคอล Hashable หมายความว่า อินสแตนซ์ของโครงสร้างนี้สามารถถูกแปลงเป็นค่าแฮช (hash value) เพื่อใช้ในการเปรียบเทียบ จัดเก็บ และค้นหาได้อย่างรวดเร็ว ในเชิงการออกแบบระบบข้อมูล สิ่งนี้ทำให้ Category สามารถใช้ใน Set เพื่อป้องกันค่าซ้ำและรองรับการจัดกลุ่มข้อมูล กล่าวในเชิงแนวคิดคือ
Hashable ทำให้ข้อมูลสามารถถูกนำไปใช้ในโครงสร้างข้อมูลที่เน้น “ความสัมพันธ์และการจัดกลุ่ม” ได้อย่างมีประสิทธิภาพ
ในการกำหนดให้ Expense ปฏิบัติตาม Identifiable หมายความว่า รายจ่ายแต่ละรายการต้องมีค่า id ที่ไม่ซ้ำกันซึ่งการกำหนด id เป็น UUID ช่วยให้ข้อมูลแต่ละรายการมีอัตลักษณ์ที่ชัดเจน เพราะทำให้รายจ่ายแต่ละรายการมีตัวตนเฉพาะ (unique identity) ลดความเสี่ยงของข้อมูลซ้ำเมื่อมีจำนวนรายการมาก ซึ่งหมาะกับข้อมูลประเภท “เหตุการณ์” ที่เกิดขึ้นตามเวลา และการทำงานร่วมกับ SwiftUI โดยเฉพาะในโครงสร้างที่ต้องแสดงรายการข้อมูล เช่น List หรือ ForEach ในเชิง Data Model Expense จึงไม่ใช่เพียงค่าจำนวนเงิน แต่เป็น record ของเหตุการณ์จริง ที่สามารถระบุได้ชัดเจน
การกำหนด property ด้วย let ทำให้มีลักษณะ immutable กล่าวคือ เมื่อถูกสร้างขึ้นแล้ว ค่าภายในจะไม่สามารถเปลี่ยนแปลงได้ การออกแบบในลักษณะนี้ช่วยป้องกันการเปลี่ยนแปลงข้อมูลโดยไม่ตั้งใจ และลดความเสี่ยงของข้อผิดพลาดที่เกิดจากการแก้ไขข้อมูลในหลายตำแหน่ง
การกำหนด note เป็น String? สะท้อนการออกแบบข้อมูลอย่างมีความหมาย โดยยอมรับว่าข้อมูลบางอย่างอาจไม่มีค่าในโลกจริง การใช้ Optional ไม่ใช่เพียงกลไกเพื่อหลีกเลี่ยงข้อผิดพลาด แต่เป็นการสื่อสารความไม่แน่นอนหรือความไม่จำเป็นของข้อมูลบางส่วนในระดับ Data Model
การสร้าง initializer ในส่วนของ Expense ช่วยให้สามารถกำหนดวิธีสร้างข้อมูลรายจ่ายได้อย่างชัดเจนและสอดคล้องกับความหมายของ Data Model มากขึ้น กล่าวคือ initializer ทำหน้าที่ควบคุมว่าการสร้างอินสแตนซ์หนึ่งรายการต้องมีข้อมูลใดบ้าง และข้อมูลใดสามารถละไว้ได้โดยไม่ทำให้โมเดลสูญเสียความสมบูรณ์
จากตัวอย่างการสร้าง Data Model ด้วย struct ข้างต้น แสดงให้เห็นว่า Swift สามารถใช้เป็นภาษาสำหรับการสร้างแบบจำลองข้อมูลที่ดีได้ เพราะภาษาเอื้อให้ผู้พัฒนานิยามทั้งโครงสร้าง ความหมาย ความถูกต้อง และพฤติกรรมพื้นฐานของข้อมูลไว้ในที่เดียว ทำให้โมเดลที่สร้างขึ้นมีความชัดเจน สื่อสารได้ดี ปลอดภัยต่อการใช้งาน และพร้อมต่อการขยายในระบบจริง
จาก Data Model สู่ SwiftData
ในส่วนที่ผ่านมา เราได้เรียนรู้การใช้ภาษา Swift เพื่อออกแบบ Data Model อย่างมีหลักการ โดยเน้นการใช้ struct เพื่อสะท้อนโครงสร้างข้อมูลในโลกจริง อย่างไรก็ตาม Model ดังกล่าวมีลักษณะเป็น In-memory Model กล่าวคือ ข้อมูลมีอยู่เพียงในหน่วยความจำ และจะสูญหายไปทันทีเมื่อแอปถูกปิดลง เมื่อแอปถูกใช้งานจริง ข้อมูลจำนวนมากจำเป็นต้องถูกเก็บอย่างถาวร จึงเกิดคำถามสำคัญว่า “หากแอปถูกปิดแล้วเปิดใหม่ ข้อมูลควรถูกเก็บไว้ที่ไหน?” คำถามนี้เป็นจุดเริ่มต้นของการยกระดับไปสู่การเก็บข้อมูลแบบ Persistent Model ด้วย SwiftData
การนำ SwiftData เข้ามาใช้ไม่มได้มีเป้าหมายเพื่อเปลี่ยนรูปแบบของข้อมูลแต่เพื่อเปลี่ยนบทบาทของข้อมูล จากข้อมูลที่มีชีวิตอยู่ชั่วคราวไปสู่ ข้อมูลที่มี วงจรชีวิต (lifecycle) และ ความคงอยู่ (persistence) ในระดับแอปพลิเคชัน ข้อมูลในลักษณะนี้ไม่เพียงถูกสร้างและใช้งานเท่านั้น แต่ยังต้องถูกจัดเก็บ เรียกคืน แก้ไข และคงสภาพความสอดคล้องไว้ตลอดอายุการใช้งานของระบบ ดังนั้น ในบริบทของ SwiftData ซึ่งกำหนดให้ Data Model มีบทบาทเป็น persistent domain object การใช้ class จึงเป็นทางเลือกที่สอดคล้องกับบทบาทดังกล่าวมากกว่า struct
การเปลี่ยนจาก struct ไปสู่ class ไม่ใช่เป็นเพียงแค่ข้อกำหนดของ SwiftData framework แต่เป็นการยกระดับบทบาทของข้อมูลให้มีความหมายในเชิงสถาปัตยกรรมของระบบมากขึ้น โดยมีเหตุผลสำคัญดังนี้
การมีอัตลักษณ์ของข้อมูล (Identity) -
classรองรับ Reference Semantics ซึ่งเหมาะสมกับข้อมูลที่ต้องมีอัตลักษณ์เดียวในระบบ ตัวอย่างเช่น ตัวอย่างเช่น เมื่อรายจ่ายรายการหนึ่งถูกใช้งานในหลายส่วนของแอป การแก้ไขใดๆ ก็ตาม ควรสะท้อนผลไปยังทุกส่วนที่อ้างอิงข้อมูลเดียวกันทันที พฤติกรรมเช่นนี้ไม่สามารถเกิดขึ้นได้อย่างเป็นธรรมชาติในstructที่ทำงานแบบ Value Semanticsการจัดการวงจรชีวิตของข้อมูล (Lifecycle Management) - SwiftData ต้องดูแลข้อมูลตั้งแต่ การสร้าง (creation) การบันทึก (persistence) การดึงกลับมาใช้งาน (fetching) ไปจนถึงการลบ (deletion) การจัดการวงจรชีวิตในลักษณะนี้ต้องอาศัยออบเจกต์ที่มีตัวตนต่อเนื่อง ซึ่งสอดคล้องกับธรรมชาติของ
classมากกว่าstructการคงสภาพความสอดคล้องของข้อมูล (consistency) - ในแอปที่มีหลาย View และหลาย ViewModel จำเป็นต้องอาศัยโมเดลที่สามารถถูกสังเกตและติดตามการเปลี่ยนแปลงได้ SwiftData จึงออกแบบให้
@Modelทำงานร่วมกับclassเพื่อให้ระบบสามารถติดตามเพื่อสังเกตและอัปเดตสถานะของออบเจกต์เดียวกันได้ตลอดเวลา
กล่าวได้ว่า การเลือกใช้ class ใน SwiftData ไม่ได้เป็นประเด็นเชิงไวยากรณ์ของภาษา แต่เป็นการตัดสินใจเชิงแนวคิดที่สะท้อนว่า ข้อมูลชุดนี้มีบทบาทเป็น domain object ของระบบ
การนิยามข้อมูลด้วย Class และ @Model
SwiftData ใช้ แนวคิดเชิง Object-Relational Mapping (ORM) ในการจัดการข้อมูล โดยเปิดโอกาสให้นักพัฒนานิยามโมเดลผ่านภาษา Swift โดยตรง แล้วให้ระบบดูแลการจัดเก็บ การเชื่อมโยง และการเรียกคืนข้อมูลให้อัตโนมัติ การประกาศ Data Model ด้วย @Model จึงเป็นการประกาศว่า ข้อมูลชุดนี้มีตัวตนในระบบ มีอัตลักษณ์ และมีวงจรชีวิตที่ถูกจัดการเพื่อการบันทึก ดึงกลับ และติดตามการเปลี่ยนแปลงอย่างเป็นระบบ
การเปลี่ยนจาก
structเป็นclass: SwiftData ใช้ class ในการนิยามข้อมูล เพราะต้องอาศัย Reference semantics เพื่อกำหนดให้ข้อมูลมี identity ซึ่งใช้สำหรับการติดตามการเปลี่ยนแปลงของออบเจกต์ การรักษาความสอดคล้องของข้อมูล และการจัดการวงจรชีวิตของข้อมูลการไม่ต้องประกาศ
idด้วยตนเอง : เนื่องจาก SwiftData มีการสร้าง persistent identity ให้กับออบเจกต์โดยอัตโนมัติ ทำให้ระบบสามารถแยกออบเจกต์ออกจากกันเพื่อติดตามการเปลี่ยนแปลง เชื่อมโยงความสัมพันธ์ระหว่างโมเดล และดึงข้อมูลเดิมกลับมาใช้งานได้ถูกต้อง โดยนักพัฒนาไม่ต้องกำหนดUUIDเองการออกแบบ initializer อย่างมีความหมาย : แม้ SwiftData จะช่วยจัดการ persistence ให้ แต่การออกแบบ initializer ยังมีบทบาทสำคัญ เพราะเป็นส่วนที่ถูกใช้เพื่อการควบคุมการสร้างข้อมูลใหม่ให้สอดคล้องกับ business rule ของระบบ
การจัดการข้อมูลผ่าน ModelContext
เมื่อ Data Model ถูกยกระดับเป็น SwiftData Model แล้ว การทำงานกับข้อมูลใน SwiftUI จะเปลี่ยนจากการจัดการตัวแปรทั่วไป ไปสู่การจัดการข้อมูลผ่าน ModelContext ซึ่งเป็นศูนย์กลางของระบบ persistence
ตัวอย่างการสร้างรายการรายจ่าย
เมื่อ Data Model ถูกผนวกเข้าสู่ SwiftData แล้ว การสร้างข้อมูลใหม่ไม่ได้เป็นเพียงการแค่สร้างออบเจกต์ในหน่วยความจำเท่านั้น แต่เป็นการสร้างออบเจกต์ที่มีสถานะอยู่ภายใต้ระบบจัดการข้อมูลของแอปทันที
การประกาศ @Environment(.modelContext) private var context คือ การดึง ModelContext ซึ่งทำหน้าที่คล้าย transaction layer ของระบบจัดเก็บข้อมูลจาก Environment ของ SwiftUI เข้ามาใช้งานใน View เพื่อใช้เป็นตัวกลางในการจัดการข้อมูล ทำให้ View สามารถจัดการข้อมูลได้ผ่านการใช้คำสั่ง เช่น context.insert(expense) context.delete(expense) try? context.save() โดยไม่ต้องรู้รายละเอียดว่าข้อมูลถูกเก็บไว้ที่ไหนหรืออย่างไร
ดังนั้น การใช้คำสั่ง context.insert(expense) ไม่ใช่เพียงการเพิ่มข้อมูลเข้า collection แต่เป็นการลงทะเบียนออบเจกต์เข้าสู่ระบบจัดการข้อมูลของ SwiftData ทำให้ระบบสามารถติดตามการเปลี่ยนแปลงของออบเจกต์นั้นได้ และทำให้ View อื่นที่เรียกใช้ (query) ข้อมูลเดียวกันสามารถอัปเดตตามได้อัตโนมัติ
ตัวอย่างนี้ทำให้เห็นว่า SwiftUI ไม่ได้ทำงานกับ “ข้อมูลธรรมดา” อีกต่อไป แต่ทำงานกับข้อมูลที่มีตัวตนในระบบ persistence ผ่าน ModelContext ซึ่งช่วยเชื่อมโยงการสร้างข้อมูล การจัดเก็บ และการอัปเดต UI เข้าไว้ในกลไกเดียวกันอย่างเป็นระบบ
การสร้างความสัมพันธ์ระหว่างข้อมูล
เมื่อเราสามารถออกแบบ Data Model ได้อย่างถูกต้องในระดับโครงสร้างข้อมูลแล้ว ขั้นตอนสำคัญถัดไป คือ การทำความเข้าใจว่า ข้อมูลหนึ่งหน่วยไม่ได้ดำรงอยู่โดยลำพัง หากแต่มีความสัมพันธ์กับข้อมูลอื่นเสมอ ในกรณีของแอปบันทึกรายจ่าย ความสัมพันธ์ที่ชัดเจนที่สุด คือ “รายจ่าย (Expense) และ “หมวดหมู่รายจ่าย (Category)”
ในโลกจริง รายจ่ายแต่ละรายการย่อมถูกจัดให้อยู่ในหมวดหมู่หนึ่ง เช่น อาหาร การเดินทาง หรือที่พัก ขณะเดียวกัน หมวดหมู่หนึ่งสามารถมีรายจ่ายได้หลายรายการ ความสัมพันธ์ลักษณะนี้เกิดจากธรรมชาติของข้อมูล ไม่ได้เกิดจากข้อจำกัดของ UI หรือ Framework ใดๆ และเป็นตัวอย่างที่เหมาะสมอย่างยิ่งในการอธิบายแนวคิด Relationship ใน SwiftData เมื่อพิจารณาในระดับแนวคิด (Conceptual) ความสัมพันธ์ระหว่าง Expense และ Category ถูกอธิบายว่าเป็นความสัมพันธ์แบบ One-to-Many กล่าวคือ
Category หนึ่งรายการ → มี Expense ได้หลายรายการ
Expense หนึ่งรายการ → สังกัด Category เพียงหนึ่งเดียว
เมื่อเราใช้ SwiftData เป้าหมายไม่ใช่เพียงการ “เขียนโค้ดให้เก็บข้อมูลได้” แต่จะต้องถ่ายทอดความสัมพันธ์เชิงแนวคิดนี้ลงไปในระดับ Persistent Model อย่างถูกต้องและมีความหมายด้วย
คราวนี้ลองสร้าง Categary สำหรับเก็บข้อมูล "ประเภทรายจ่าย" ใน SwiftData
ในตัวอย่างนี้ Category ถูกออกแบบให้มีคุณลักษณะหลัก คือ name และมี expenses เพื่อสะท้อนความสัมพันธ์แบบ One-To-Many ตัวแปร expenses ไม่ได้มีไว้เพื่อการแสดงผลเท่านั้น แต่เป็นส่วนหนึ่งของโครงสร้างข้อมูลถาวรที่ใช้บันทึกความสัมพันธ์ของโมเดล
เมื่อสร้าง Categary แล้ว เราสามารถทำให้ Expense รับรู้ถึงหมวดหมู่ที่ตนเองสังกัด เพื่อให้ความสัมพันธ์สมบูรณ์ ได้ดังนี้
การกำหนด category เป็น Optional สะท้อนความเป็นไปได้ในโลกจริงว่า รายจ่ายบางรายการอาจยังไม่ได้ถูกจัดหมวดหมู่ในขณะบันทึก ซึ่งเป็นการออกแบบที่สอดคล้องกับหลัก Safe Data Design ซึ่งเป็นการออกแบบข้อมูลให้ความถูกต้องซึ่งเกิดจากโครงสร้างของข้อมูลเอง ไม่ใช่จากการตรวจสอบภายหลัง
Inverse Relationship ใน SwiftData
สิ่งที่ทำให้ SwiftData แตกต่างจากการจัดการข้อมูลแบบธรรมดา คือ การรองรับ Inverse Relationship โดยอัตโนมัติ เมื่อความสัมพันธ์ของ Expense กับ Category ถูกกำหนดอย่างสอดคล้องกัน หากมีการสร้างรายการรายจ่ายใหม่ ซึ่งมีการกำหนดหมวดหมู่รายจ่ายไว้ ระบบจะทำให้ Expense นั้นปรากฏอยู่ใน category.expenses โดยไม่ต้องเขียนโค้ดจัดการเอง
หากเราสร้าง Model ของข้อมูลใน SwiftData เรียบร้อยแล้ว เราอาจทดลองสร้างข้อมูลจริงเพื่อสังเกตพฤติกรรมที่เกิดจากความสัมพันธ์ของข้อมูล ได้ดังนี้
จากตัวอย่างนี้ รายจ่ายทั้งสองรายการถูกจัดให้อยู่ในหมวดเดียวกัน เมื่อดึงข้อมูล Category ขึ้นมา เราก็จะสามารถเข้าถึงรายการรายจ่ายทั้งหมดได้ทันทีผ่าน category.expenses
การเข้าถึงข้อมูลด้วย @Query
เมื่อเราสร้างความสัมพันธ์ระหว่าง Expense และ Category ได้อย่างถูกต้องแล้ว ขั้นตอนถัดไปคือ การตั้งคำถามว่า “เราจะดึงข้อมูลเหล่านี้มาใช้อย่างไร โดยไม่ทำลายโครงสร้างของระบบ” คำถามนี้นำไปสู่การทำความเข้าใจบทบาทของ @Query ใน SwiftData และการจัดวางความรับผิดชอบของโค้ดภายใต้สถาปัตยกรรม MVVM
ในแนวคิด Thinking in Data การเข้าถึงข้อมูลไม่ควรเป็นภาระของ UI แต่ควรเป็นผลลัพธ์ของโครงสร้างข้อมูลที่ถูกออกแบบไว้อย่างเหมาะสม SwiftData จึงทำหน้าที่เป็นสะพานเชื่อมระหว่าง Data Model กับการแสดงผล โดยลดภาระการเขียนโค้ดเชิงกลไกลงอย่างมาก
@Query: การเข้าถึงข้อมูลเชิงประกาศ (Declarative Data Access)
@Query เป็นกลไกสำคัญของ SwiftData ที่ช่วยให้ SwiftUI สามารถดึงข้อมูลจาก Persistent Store ได้ในลักษณะเชิงประกาศ กล่าวคือ ผู้พัฒนาเพียงระบุว่า“ต้องการข้อมูลแบบใด” โดยไม่ต้องเขียนโค้ด fetch หรือจัดการ lifecycle ของข้อมูลด้วยตนเอง
ตัวอย่างการดึงข้อมูลรายการรายจ่ายทั้งหมด พร้อมเรียงตามวันที่
สำหรับการใช้เงื่อนไขเพื่อการกรองข้อมูล (Filtering) เช่น การดูรายจ่ายเฉพาะหมวดอาหาร หรือ ดูรายการเฉพาะช่วงเวลาใดช่วงเวลาหนึ่ง สามารถทำได้โดยใช้ predicate บน @Query ซึ่งช่วยให้เงื่อนไขการคัดกรองถูกผูกกับโครงสร้างข้อมูลโดยตรง ไม่ใช่การกรองในระดับ UI
ตัวอย่างการดึงรายจ่ายเฉพาะหมวดหมู่หนึ่ง
หรือเพิ่มการจัดเรียงข้อมูลด้วย Sort
การกรองข้อมูลในลักษณะนี้เกิดจากความสัมพันธ์ของ Data Model ที่ถูกออกแบบไว้ล่วงหน้า หากไม่มีความสัมพันธ์ Expense–Category ที่ชัดเจน การเขียนเงื่อนไขลักษณะนี้จะมีความซับซ้อนมากขึ้นอย่างเห็นได้ชัด
จาก View สู่ MVVM: การจัดวางความรับผิดชอบ
แม้ @Query จะสามารถใช้งานได้โดยตรงใน View แต่ในเชิงสถาปัตยกรรม การปล่อยให้ View จัดการตรรกะของข้อมูลมากเกินไปจะนำไปสู่โค้ดที่ยากต่อการดูแลรักษา บทความในส่วนนี้จึงเชื่อมต่อแนวคิดไปสู่สถาปัตยกรรม MVVM เพื่อจัดวางบทบาทของแต่ละส่วนให้ชัดเจน
ภายใต้ MVVM
Model คือ
ExpenseและCategory(SwiftData Model)ViewModel คือ ส่วนที่ทำหน้าที่จัดการการเข้าถึงและแปลงข้อมูล
View คือ ส่วนแสดงผลข้อมูลและรับการโต้ตอบจากผู้ใช้
แนวคิดนี้สอดคล้องโดยตรงกับ Thinking in Data เพราะช่วยแยก โครงสร้างข้อมูล ออกจาก การนำเสนอข้อมูล
โครงสร้างไฟล์ใน ExpenseTrackerApp
ไฟล์ ExpenseTrackerApp.swift
ไฟล์ ExpenseTrackerApp.swift ทำหน้าที่เป็น จุดเริ่มต้นของแอปพลิเคชัน ใน SwiftUI โดยกำหนดโครงสร้างระดับบนสุดของระบบ ทั้งในด้าน UI และการจัดการข้อมูลผ่าน SwiftData กล่าวได้ว่าไฟล์นี้คือจุดที่เชื่อมโลกของ แอปพลิเคชัน, โครงสร้าง View, และ ระบบ persistence เข้าด้วยกัน
เขียนคำสั่งในไฟล์นี้ โดยกำหนดให้ View แรกที่ผู้ใช้เห็นคือ ExpenseListView() และจัดเตรียมระบบจัดเก็บข้อมูลด้วย SwiftData ด้วยคำสั่ง .modelContainer(for: [Expense.self, Category.self]) สำหรับการสร้าง persistent container ของแอป
หลังจากกำหนดโครงสร้างระดับระบบของแอปแล้ว ขั้นตอนเป็นการกำหนด ข้อมูลหลักของโดเมน ซึ่งเป็นแกนของระบบทั้งหมดในแอป โดยข้อมูลหลักในแอปนี้ คือ หมวดหมู่รายจ่าย (Category) และ รายจ่าย (Expense)
ไฟล์ Category.swift
ไฟล์ Expense.swift
ไฟล์ ExpenseViewModel.swift
ในขั้นตอนถัดไป คือ การสร้าง ViewModel ซึ่งทำหน้าที่เป็น command layer ในการเพิ่ม (addExpense) และลบ (deleteExpense) ข้อมูลรายการรายจ่ายของระบบ และมีบทบาทเป็น “ตัวกลาง” ระหว่าง Model (Expense / Category) Persistence (ModelContext) และ View (หน้าจอของแอป)
การประกาศ
@Observableทำให้ ViewModel นี้สามารถถูกสังเกตได้ หากค่าภายใน เช่น isSaving หรือ errorMessage มีการเปลี่ยนแปลง View ที่ใช้ ViewModel จะอัปเดตอัตโนมัติการกำหนด ตัวแปร
isSavingและerrorMessageเพื่อใช้สำหรับการตรวจสอบสถานะการทำงานของของระบบ โดยสามารถนำไปใช้ในการจัดการสถนะของ UI เช่น การปิดการโต้ตอบของผู้ใช้ การแสดง loading indicator หรือการแสดงข้อความ error บน AlertBox เป็นต้นคำสั่ง
private let context: ModelContextไม่ได้เป็นการสร้าง ModelContext ใหม่ แต่เป็นการประกาศตัวแปรcontextเพื่อใช้อ้างอิงถึง ModelContext ที่ถูกส่งเข้ามาจาก SwiftData ผ่าน View โดย ModelContext นี้ใช้เป็นตัวกลางในการสร้าง ลบ และบันทึกข้อมูลใน ModelContainer ของแอปฟังก์ชัน
addExpense,deleteExpenseและsetupInitialDataเป็นตัวแทนของการกระทำต่อข้อมูลในระบบ โดย ViewModel ทำหน้าที่เป็นชั้นที่ควบคุมการสร้าง ลบ และเตรียมข้อมูลเริ่มต้นผ่าน ModelContext การออกแบบลักษณะนี้ช่วยให้ View ไม่ต้องจัดการ persistence โดยตรง ทำให้โค้ดมีโครงสร้างชัดเจน รองรับการขยายระบบ และสอดคล้องกับสถาปัตยกรรม MVVM
ไฟล์ ExpenseRowView.swift
การสร้าง UI ในการแสดงข้อมูลของรายจ่ายหนึ่งรายการ
ไฟล์ ExpenseListView.swift
ในส่วนของการสร้าง View สำหรับหน้ารายการรายจ่าย โค้ดนี้ทำหน้าที่เป็นโครงสร้างหลักของ UI ที่ใช้แสดงข้อมูลจาก SwiftData ในลักษณะรายการ โดยใช้แนวคิด declarative ของ SwiftUI ซึ่งให้ View ทำหน้าที่แสดงผลข้อมูล ขณะที่แหล่งข้อมูลถูกจัดการโดยระบบ persistence อยู่เบื้องหลัง
คำสั่ง
@Environment(\.modelContext) private var contextเป็นการดึง ModelContext จากระบบ SwiftData ผ่าน Environment มาใช้ใน View นี้ส่วนของการ Qury ทำหน้าที่ในการดึงข้อมูลจาก SwiftData persistence store มาให้ View ใช้โดยอัตโนมัติ
ตัวแปร
showingAddExpenseViewทำหน้าที่เก็บสถานะว่าหน้าจอสำหรับเพิ่มรายจ่ายควรถูกแสดงหรือไม่ในส่วนของการแสดงผล โครงสร้างเริ่มต้นด้วย
NavigationStackซึ่งทำหน้าที่กำหนดบริบทของการนำทางภายในหน้าจอ ทำให้ View นี้สามารถแสดงชื่อหน้าด้านบน และรองรับการนำทางไปยัง View อื่นในอนาคตได้ เช่น หน้ารายละเอียดหรือหน้าสร้างข้อมูลใหม่ภายใน
NavigationStackมีการใช้Listเพื่อแสดงข้อมูลในรูปแบบรายการแนวตั้ง ซึ่งเป็นรูปแบบที่เหมาะสมกับข้อมูลประเภท collection อย่างรายจ่าย โดยListจะทำหน้าที่จัดการ layout การเลื่อนหน้าจอ การแบ่งแถว และพฤติกรรมพื้นฐานของรายการให้โดยอัตโนมัติ ทำให้ผู้พัฒนาไม่ต้องจัดการรายละเอียดเชิงกลไกของ UI ด้วยตนเองการนำข้อมูลจาก SwiftData มาแสดงผลเกิดขึ้นผ่านคำสั่ง
ForEach(expenses)ซึ่งรับข้อมูลจากตัวแปรexpensesที่ถูกดึงมาจาก persistence ด้วย@Queryคำสั่งForEachทำหน้าที่วนผ่านข้อมูลรายจ่ายทีละรายการ และส่งข้อมูลแต่ละหน่วยไปยังExpenseRowViewเพื่อสร้าง UI สำหรับแถวนั้นโดยเฉพาะExpenseRowViewมีบทบาทสำคัญในฐานะ View ย่อยที่ทำหน้าที่แสดงข้อมูลของรายจ่ายหนึ่งรายการโดยเฉพาะ การแยก Row View ออกเป็นคอมโพเนนต์เฉพาะช่วยให้โค้ดมีความชัดเจนและสามารถนำกลับมาใช้ซ้ำได้ นอกจากนี้ยังช่วยให้ View หลักไม่ต้องรับผิดชอบรายละเอียดของการจัดรูปแบบข้อมูลแต่ละรายการ ส่งผลให้โครงสร้างของหน้ารายการอ่านง่ายและดูแลรักษาได้สะดวกขึ้นเมื่อข้อมูลใน SwiftData เปลี่ยนแปลง ไม่ว่าจะเป็นการเพิ่ม ลบ หรือแก้ไขรายการ ตัวแปร
expensesจะถูกอัปเดตโดยอัตโนมัติผ่านกลไก reactive ของ SwiftData และ SwiftUI ทำให้ForEachสร้าง UI ใหม่ตามข้อมูลล่าสุดทันทีโดยไม่ต้องเขียนโค้ดรีเฟรชหน้าจอเอง นี่คือจุดสำคัญที่ทำให้การแสดงข้อมูลผ่านExpenseRowViewมีความสอดคล้องกับสถานะจริงของระบบอยู่เสมอนอกจากนี้
Listยังรองรับการลบข้อมูลผ่านการปัด (swipe to delete) โดยคำสั่ง.onDeleteจะได้รับตำแหน่งของรายการที่ถูกลบ จากนั้น View จะสร้าง ViewModel และเรียกใช้เมธอดdeleteExpenseเพื่อจัดการการลบข้อมูลใน persistence อย่างเป็นระบบ การออกแบบนี้ทำให้ View ไม่ต้องจัดการฐานข้อมูลโดยตรง แต่ส่งต่อการกระทำไปยัง ViewModel ซึ่งเป็นไปตามแนวคิด MVVM ที่แยกตรรกะของข้อมูลออกจากการแสดงผลส่วนของ toolbar และ sheet ทำหน้าที่เสริมให้หน้ารายการสามารถเปิดหน้าสำหรับเพิ่มข้อมูลใหม่ได้ โดยใช้ state เป็นตัวควบคุมการแสดงผลของหน้าต่างย่อย แสดงให้เห็นว่า UI ใน SwiftUI ถูกกำหนดโดยสถานะของระบบ ไม่ใช่การเรียกคำสั่งนำเสนอหน้าจอแบบลำดับขั้น
ในตอนท้ายของโครงสร้าง View มีการใช้ตัวปรับแต่ง
.taskเพื่อกำหนดงานที่ควรถูกดำเนินการเมื่อ View ปรากฏขึ้นบนหน้าจอ คำสั่งนี้ทำหน้าที่เป็นจุดเริ่มต้นของกระบวนการเตรียมระบบให้พร้อมใช้งาน โดยเฉพาะในกรณีที่แอปเพิ่งถูกเปิดครั้งแรกหรือยังไม่มีข้อมูลในฐานข้อมูล ภายใน.taskมีการสร้างอ็อบเจกต์ของExpenseViewModelโดยส่งModelContextเข้าไปผ่าน initializer การกระทำนี้สะท้อนแนวคิดของ Dependency Injection ซึ่งทำให้ ViewModel สามารถเข้าถึงระบบจัดเก็บข้อมูลของ SwiftData ได้โดยไม่ต้องสร้าง context เอง ส่งผลให้การจัดการข้อมูลยังคงอยู่ภายใต้โครงสร้างสถาปัตยกรรมที่ชัดเจน คือ View ทำหน้าที่สั่งงาน ส่วน ViewModel ทำหน้าที่จัดการข้อมูล เมื่อได้ ViewModel แล้ว View จะเรียกใช้เมธอดsetupInitialDataพร้อมส่งรายการcategoriesและexpensesที่ดึงมาจาก SwiftData ผ่าน@Queryเข้าไป ฟังก์ชันนี้มีหน้าที่ตรวจสอบว่าระบบมีข้อมูลพื้นฐานอยู่แล้วหรือไม่ หากยังไม่มี ก็จะสร้างหมวดหมู่เริ่มต้นและข้อมูลตัวอย่างขึ้นมา การออกแบบลักษณะนี้ช่วยให้แอปไม่เริ่มต้นจากหน้าว่าง
ไฟล์ CategoryPickerView.swift
ไฟล์ CategoryPickerView.swift ทำหน้าที่เป็น View ย่อยสำหรับเลือกหมวดหมู่รายจ่าย ในหน้าการเพิ่มข้อมูลรายการรายจ่ายใหม่ โดยจะดึงข้อมูลจากรายการหมวดหมู่รายจ่ายที่เก็บอยู่ใน SwiftData เพื่อนำมาใส่ใน Picker ซึ่งอยู่ใน AddExpenseView.swift
คำสั่ง
@Query private var categories: [Category]ทำหน้าที่ดึงข้อมูลหมวดหมู่จาก SwiftDataคำสั่ง
@Binding var selectedCategory: Category?แสดงว่า View นี้ไม่ได้เป็นเจ้าของค่าที่เลือก แต่ได้รับสิทธิ์ในการอ่านและแก้ไขค่าที่อยู่ใน View อื่น ค่าจริงของselectedCategoryนั้นถูกเก็บในAddExpenseView(ผ่าน@State) ส่วนCategoryPickerViewแค่ผูกตัวเองเข้ากับค่านั้น ดังนั้น ค่าที่ถูกผู้ใช้เลือกจาก Picker จะถูกส่งกลับไปยังAddExpenseView
ไฟล์ AddExpenseView.swift
ไฟล์ AddExpenseView.swift ทำหน้าที่เป็นหน้าจอสำหรับสร้างข้อมูลรายจ่ายใหม่ในระบบ โดยเป็นจุดที่ผู้ใช้โต้ตอบกับแอปเพื่อป้อนข้อมูล ก่อนที่ข้อมูลนั้นจะถูกส่งไปยัง ViewModel และถูกบันทึกลงใน SwiftData
ภายใน View มีการประกาศตัวแปร
viewModelเพื่อใช้เป็นตัวกลางในการติดต่อกับชั้นข้อมูล แทนที่ View จะเพิ่มข้อมูลเอง การออกแบบลักษณะนี้ช่วยแยกความรับผิดชอบอย่างชัดเจน กล่าวคือ View ทำหน้าที่รวบรวมข้อมูลจากผู้ใช้ ส่วน ViewModel ทำหน้าที่สร้างและบันทึกข้อมูลในระบบตัวแปรที่ประกาศด้วย
@Stateได้แก่amountText,noteและselectedCategoryทำหน้าที่เก็บสถานะชั่วคราวของฟอร์มที่ผู้ใช้กำลังกรอกอยู่ ข้อมูลเหล่านี้เป็น UI State ไม่ใช่ข้อมูลถาวรของระบบ เพราะมีชีวิตอยู่เพียงระหว่างการแสดงหน้าจอนี้เท่านั้นภายใน
bodyมีการใช้NavigationStackเพื่อกำหนดบริบทของหน้าจอ และใช้Formเพื่อจัดวางช่องกรอกข้อมูลในลักษณะที่เหมาะสมกับการป้อนข้อมูลของผู้ใช้ ฟอร์มถูกแบ่งเป็นส่วนย่อย ได้แก่ จำนวนเงิน หมายเหตุ และหมวดหมู่ โดยส่วนของหมวดหมู่ใช้CategoryPickerViewซึ่งเป็น View ย่อยที่ดึงข้อมูลหมวดจาก SwiftData และส่งค่าที่เลือกกลับมาผ่าน Binding การออกแบบนี้ช่วยให้ View หลักไม่ต้องจัดการรายละเอียดของ Picker เองในส่วนของ toolbar มีปุ่ม Save และ Cancel ปุ่ม Save จะเรียกฟังก์ชัน
saveExpense()ซึ่งทำหน้าที่ตรวจสอบความถูกต้องของข้อมูลก่อน แล้วส่งค่าที่ผู้ใช้กรอกไปยัง ViewModel ผ่านเมธอดaddExpenseเมื่อบันทึกเสร็จ View จะเรียกdismiss()เพื่อปิดหน้าจอ การจัดลำดับเช่นนี้ทำให้การสร้างข้อมูลใหม่เป็นกระบวนการที่มีขั้นตอนชัดเจน คือ รับข้อมูล → ตรวจสอบ → ส่งต่อ → ปิดหน้าจอตัวแปรคำนวณ
isValidทำหน้าที่ตรวจสอบว่าค่าที่ผู้ใช้กรอกสามารถแปลงเป็นตัวเลขได้หรือไม่ และถูกใช้เพื่อควบคุมสถานะของปุ่ม Save ไม่ให้กดได้เมื่อข้อมูลยังไม่ถูกต้อง นอกจากนี้ยังมีการตรวจviewModel.isSavingเพื่อป้องกันการบันทึกซ้ำในระหว่างที่ระบบกำลังทำงานอยู่ ซึ่งช่วยเพิ่มความถูกต้องของข้อมูลและปรับปรุงประสบการณ์ผู้ใช้
การพัฒนาแอปในโปรเจกต์นี้ไม่ได้มุ่งเน้นเพียงการสร้างหน้าจอหรือทำให้โค้ดทำงานได้เท่านั้น แต่เน้นการทำความเข้าใจโครงสร้างของข้อมูล การไหลของข้อมูล และการจัดวางความรับผิดชอบของแต่ละส่วนในระบบอย่างเป็นระบบ
หัวใจสำคัญของการออกแบบแอปด้วย SwiftUI อยู่ที่การเข้าใจความสัมพันธ์ระหว่าง Data Flow, State และ Model โดย Model ทำหน้าที่แทนข้อมูลเชิงโดเมนที่มีความหมายในระยะยาว ขณะที่ State เป็นตัวแทนของสถานะปัจจุบันที่ควบคุมการแสดงผลของ UI ความเข้าใจความแตกต่างระหว่างสองสิ่งนี้ช่วยให้ผู้พัฒนาสามารถออกแบบระบบที่แยกความรับผิดชอบได้ชัดเจน ลดการผูกติดระหว่างข้อมูลกับหน้าจอ และทำให้โค้ดมีความยืดหยุ่นมากขึ้น
ในกระบวนการพัฒนา เราได้เห็นบทบาทของเครื่องมือสำคัญใน SwiftUI ได้แก่ @State ซึ่งใช้เก็บสถานะภายใน View, @Binding ซึ่งใช้เชื่อม State ระหว่าง View ต่าง ๆ และ @Observable ซึ่งช่วยให้ ViewModel สามารถแจ้งเตือน View เมื่อสถานะภายในเปลี่ยนแปลง กลไกเหล่านี้ทำงานร่วมกันเพื่อสร้างการไหลของข้อมูลแบบทิศทางเดียว (Unidirectional Data Flow) ซึ่งเป็นหลักการสำคัญของสถาปัตยกรรมสมัยใหม่
ขณะเดียวกัน การนำ SwiftData มาใช้ทำให้ Data Model ถูกยกระดับจากข้อมูลชั่วคราวในหน่วยความจำไปสู่ Persistent Domain Object ที่มีตัวตนในระบบจริง ผู้เรียนจึงได้เห็นว่าการเลือกใช้ class และ @Model ไม่ใช่เพียงข้อกำหนดของ framework แต่เป็นการตัดสินใจเชิงแนวคิดที่เกี่ยวข้องกับ identity, lifecycle และความสอดคล้องของข้อมูลในระดับแอปพลิเคชัน
เมื่อเชื่อม SwiftUI กับ SwiftData ผ่าน @Query และ ModelContext ระบบจึงสามารถแสดงผลข้อมูลและตอบสนองต่อการเปลี่ยนแปลงได้โดยอัตโนมัติ โดย View ทำหน้าที่แสดงผล ViewModel ทำหน้าที่จัดการการกระทำต่อข้อมูล และ Model ทำหน้าที่แทนโครงสร้างของข้อมูลจริง การจัดวางบทบาทลักษณะนี้สะท้อนสถาปัตยกรรม MVVM อย่างชัดเจน และช่วยให้โค้ดมีความเป็นระเบียบ อ่านง่าย และขยายต่อได้ในอนาคต
ดังนั้น เป้าหมายที่แท้จริงของโปรเจกต์นี้ไม่ใช่เพียงการสร้างแอปบันทึกรายจ่าย แต่คือการสร้างความเข้าใจในแนวคิดเชิงสถาปัตยกรรมของการพัฒนาแอปสมัยใหม่ ได้แก่ การแยก Model ออกจาก State การกำหนดเจ้าของข้อมูลอย่างชัดเจน การควบคุม UI ผ่าน State และการใช้ ViewModel เป็นตัวกลางของ Data Flow เมื่อเข้าใจหลักการเหล่านี้แล้ว ผู้เรียนจะสามารถออกแบบระบบที่มีโครงสร้างดี ไม่ซับซ้อนเกินจำเป็น และพร้อมต่อยอดสู่แอปที่มีความซับซ้อนมากขึ้นได้ในอนาคต
แนวทางในการจัดกิจกรรมการเรียนรู้
ดาวน์โหลดเอกสารประกอบการทำกิจกรรมในห้องปฏิบัติการ ที่นี่
ดาวน์โหลดสไลด์สำหรับใช้ประกอบการสอน ที่นี่
รายละเอียดเพื่อการอ้างอิง ผู้เขียน ธิติ ธีระเธียร วันที่เผยแพร่ วันที่ 10 กุมภาพันธ์ 2569 วันที่ปรุงปรุงล่าสุด วันที่ 10 กุมภาพันธ์ 2569 เข้าถึงได้จาก https://ajthiti.gitbook.io/develop-in-swift/getting-started/view-and-modifier เ งื่อนใขในการใช้งาน This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.
Last updated