การสร้าง Design System รองรับผลิตภัณฑ์เดียวก็ยาก - ต้อง Robust และ Flexible พร้อมกันเพื่อ Scalability 雖然ท้าทาย แต่มีทรัพยากรดีเยอะแบ่งปัน Principles และ Approaches ที่ช่วยทีมสร้างระบบที่ดีทั้ง Visual และ Programmatically บนบานพื้นฐานเหล่านี้ บทความนี้พยายามมีส่วนร่วมในพื้นที่ที่ยังไม่ถูกสำรวจ โดยโฟกัสสร้างระบบดีใน SwiftUI
ทำไมผมเขียนบทความนี้
ช่วง Summer แรกที่ ITP New York ผมโชคดีได้ทำ iOS Developer Intern ที่ Line Break Studio หนึ่งในงานคือสร้าง Design System 2 ขั้นตอน: ก่อน Visual ใน Sketch แล้วค่อย Programmatically ใน SwiftUI ประสบการณ์ทดลอง Framework ใหม่และสร้าง Design System กับมัน น่าตื่นเต้นแต่ก็ Buggy พอสมควร จึงอยากแบ่งปันประสบการณ์ให้ชุมชน หวังทำให้ Development Process ของคุณง่ายขึ้น
SwiftUI คืออะไร
Apple ปล่อย Framework แนวหน้านี้ใน WWDC 2019 ซึ่งเป็นหนึ่งในที่ดีที่สุดในหลายปี จากมุมมอง Web Developer ประสบการณ์พัฒนาโปรเจกต์ใน SwiftUI ใกล้เคียง Front-end Stack และ Frameworks แบบดั้งเดิมมาก
นี่คือการเคลื่อนไหวที่ยอดเยี่ยม เพราะ Programming Interface และ State Management ง่ายกว่าเดิมมาก และส่วนที่ดีที่สุดคือ ผสาน UIKit และ SwiftUI ได้ราบรื่น สำหรับพื้นฐาน SwiftUI Tutorials จาก Apple ช่วยได้มาก
Demo Project
เพื่อ Demonstration ผมทำ Simplified Version ของ Design System ที่สร้างที่ Line Break Studio เป็นชุด Button Components รูปแบบต่างๆ สร้างบน 2 ส่วนระดับล่าง: Typography และ ColorPalette

โปรเจกต์ Public บน GitHub ใช้ Xcode 11 Beta 5 พัฒนา Airtable Base เป็น Design System Management Hub (อ่านเพิ่มเติม Workflow Management) ก็ Public เหมือนกัน
Principles ของการสร้าง Design System
Design System ใน Code เป็น Middleware ระหว่าง Designers และ Developers Developer ของระบบรับ Input จาก Design System แบบ Visual ผลลัพธ์คือ API ที่เหมือนกันสำหการพัฒนาต่อไป 2 Principles ต่อไปควรยอมรับเพื่อทำระบบนี้ใน Code ให้สมบูรณ์:
1. สื่อสารด้วย Tokens
พื้นฐานแล้ว จุดประสงค์ของ Design System ใน Code ไม่ใช่ Code Management หรือ Development Efficiency แต่อย่างเดียว แต่เพื่อให้ View สอดคล้องกับ Design Files การใช้ Tokens เพื่อระบุสี ฟอนต์ ขนาด หรือ Visual Elements ใดๆ จำเป็นต่อการรักษาคุณภาพการสื่อสารระหว่าง Developers, Designers และ Managers ในทีม

2. ระดับ Hierarchy (ลำดับชั้น)
ในบทความ EightShapes ระบุว่า ควร "Show Options First, Then Decisions Next" เพราะ "You Can't Make Decisions Without Options"

สถาปัตยกรรมลำดับชั้นแบบนี้ทำให้ Coupling ระหว่างระดับต่างๆ หลวมขึ้น ให้ Flexibility และ Dynamic สำห Revision ที่อาจเกิดขึ้น โครงสร้างระดับของผมจากล่างขึ้นบน: Material → Base → Token แต่อาจเป็นอย่างอื่นตามความสะดวกของทีม
Diving into Code
ส่วนต่อไปคือ Highlights ที่อยากเน้นจากประสบการณ์ โปรดไปที่ GitHub Repo สำหโค้ดเต็ม Feedback และ Critiques ยินดีรับทั้งหมด
1. Architecting Levels of Hierarchy
มี 2 วิธี Stack Materials ระดับล่าง สร้าง Tokens ระดับบน:
- ใช้
enumสำห Type Safety และ Code Literacy
ข้อดีของ Enum ใน Code เป็น Grouping Wrapper หรือ Parameter ใน Function ได้รับการยอมรับดีแล้ว จุดหนึ่งคือ Implementation ของ Levels of Hierarchy
เราเก็บ Raw Values รวม Font Size (CGFloat) และ Font Name (String) ที่ Lowest Level เพราะไม่อยากสับสนกับมัน แต่ Raw Value ต้องเป็น Literal ใน Enum จึงไม่สามารถ Assign case เป็นค่าจาก Enum อื่นได้
แก้ด้วยการ Implement Function getValue คืน Raw Value ใน switch case เมื่อจำเป็น
- ใช้
structสำหโครงสร้างที่ง่ายกว่า
แม้ Enum ดี แต่ไม่ต้องการ Feature เฉพาะในบางกรณี เช่น Xcode จัดการ Dynamic Colors ให้เอง และไม่ต้องการ Parameter Options ใน API Endpoint เราสามารถตั้ง Color Palettes ด้วย Struct 2 ระดับง่ายๆ
2. การตั้งชื่อ API Endpoint ให้ชัดเจนและตรงไปตรงมา
Naming Convention หัวข้อกว้างสำหอภิปรายและโต้เถียง นอกจาก Swift Conventions พื้นฐาน กฎ 2 ข้อที่ผมปฏิบัติ: 1) ห้าม Acronym และ 2) ทำให้ Simple ตัวอย่าง เพื่อใช้ Typography และ Color System แทนที่จะสร้าง Endpoints ใหม่ เราทำ Extension จาก Font และ Color Structs วิธีนี้ลดความพยายามจดจำ API Names ที่ไม่คุ้นเคยสำห Developers
3. จัดการ Color Sets แบบ Dynamic ใน 2 โหมด
Dark Mode กลายเป็นมาตรฐานในอุตสาหกรรม iOS และ Android ทั้งคู่ Implement แล้ว Trend ดีสำห User แต่อาจท้าทาย Designers และ Developers รวมถึงการจัดการและตั้งชื่อ Color Sets โดยเฉพาะ Gray Scale

เพื่อคิดและสื่อสารเกี่ยวกับ Gray Scale Colors แบบ Dynamic การใช้คำว่า White, Light, Black หรือ Dark ไม่ได้ เพราะถ้าเราอ้างอิง Dynamic Color #000000 (Black ใน HEX) ว่า Black หรือ Dark ใน Light Color Scheme แล้วใน Dark Color Scheme มันควรกลายเป็น #FFFFFF (White) จะเรียกมันว่า DefaultDark หรือ LightDark?

การตั้งชื่อ Gray Scale Dynamic Color Sets แบบเดิมสับสนมาก เพื่อหลีกเลี่ยง เราใช้ Theme และ Contrast จัดการหนึ่งชุดสีใน Light และ Dark Schemes แทน

Gray Scale Color ไม่จำเป็นต้อง Reverse ใน Opposite Color Mode เสมอไป ในกรณีที่ Light คง Light และ Dark คง Dark เราแค่ตั้งชื่อ Light หรือ Dark ตรงๆ
เข้าใจ Naming Method นี้แล้ว จัดการ Architecture ของ Color Palette ใน Xcode ได้ง่าย สร้าง Color Set: สร้าง Asset Catalog ใหม่ → เพิ่ม Color Set → เปลี่ยน Appearances เป็น Any, Light, Dark

4. Environment Settings
Feature ยอดเยี่ยมใน SwiftUI คือ Environment Modifier ให้ความสามารถควบคุม Environment Values บน Target View ด้าน Design System ความสามารถนี้ให้ Approach สะดวกเปลี่ยน Font ของแอปที่ Root Level และข้อดีอีกอย่างของ EnvironmentValue คือ เปลี่ยนและทดสอบ Light/Dark Color Schemes ใน Development
5. ButtonStyle และ Button Label
เปรียบเทียบกับวันเก่าใน UIKit การสร้าง Reusable Buttons ใน SwiftUI ง่ายขึ้นมาก View Button ประกอบ 2 ส่วน: Action Closure (Event ตอนกดปุ่ม) และ Label (Body ของปุ่ม) View ต่อกับ Modifier buttonStyle ได้ สำหรายละเอียดสร้าง Reusable Buttons แนะนำอ่าน Tutorial ของ Alejandro ซึ่งครอบคลุมและเป็นประโยชน์
ใน Customized Button Components ของเรา ขั้นแรกสร้าง 2 Structs: TokenButtonLabel และ TokenButtonStyle โปรแกรมตามประเภทปุ่มใน Design Files เช่น มี 2 ประเภท Label: Icon และ Text แต่ละประเภทมี init Function แยกต่างหากกับ Parameters ต่างกัน
อีกด้านหนึ่ง มี 4 Major Button Styles: Circle Icon, Icon, Capsule และ Text เพื่อ Follow ButtonStyle Protocol ต้อง Implement makeBody Func ฟังก์ชันนี้นำ Configuration Property มาให้ มี Native isPressed Value ตรวจสอบว่าปุ่มถูกกดหรือไม่
สุดท้าย Stack บน TokenButtonLabel และ TokenButtonStyle Endpoint ของ Button Component API จะเป็น TokenButton - Grouping ที่รวม Content และ Style ของปุ่มเข้าด้วยกัน Conform กับ Button Types ใน Visual Design System
6. AnyView เป็น Wrapper
เมื่อจัดการ makeBody Function จาก ButtonStyle Protocol พบ Tip มีประโยชน์ทำงานกับ View เพื่อเก็บ View ใน Variable Type Annotation สามารถเป็น AnyView ซึ่งทำงานเป็น General Container ของ Views ใน SwiftUI
ในกรณีเรา อยากเพิ่ม Opacity Modifier ให้ configuration.label ทุกประเภทปุ่ม แทนทำซ้ำในแต่ละ switch case 合理的กว่าจะ Chain Modifier ท้ายสุดหมด ใช้ข้อได้เปรียบของ AnyView แบบนี้:
7. สร้าง View Modifier ด้วย Mutating Function
เพื่อ Update Styles ของปุ่มแบบ Dynamic สร้าง Modifier เอง Instantiate Customized Mutable State Properties ใน View แล้วสร้าง Mutating Function คืน Self Type หลัง Update Target State Property
8. Tricky Border Style
Drawback ของ SwiftUI คือ Styling Circle Shape ด้วย Circular Border ไม่ตรงไปตรงมาเลย ผมพยายามนาน และเจอวิธีแก้บน StackOverflow ต้องใช้ clipShape และ overlay Modifier
สรุป
SwiftUI เป็น Improvement ยอดเยี่ยมของ Apple แม้ยังมี Flaws การสร้าง Design System ที่ Robust และ Flexible ด้วยมัน และ UI ที่ซับซ้อนใน iOS มีประสิทธิภาพมากกว่าเดิม หวังบทความนี้ช่วยทีม iOS ที่พยายามสร้าง UI และยินดีรับ Feedback ทุกประการ!
📖 อ่านผลงานเพิ่มเติมที่ vinceshao.com / Follow Twitter หรือ LinkedIn