Quick Menu Widget in iOS

Naratpon Buakhaw
TakoDigital
Published in
4 min readApr 2, 2024

What is Widget ?

Widget ใน iOS 14 คือองค์ประกอบที่แสดงข้อมูลหรือฟังก์ชันของแอปในหน้าจอโฮมสกรีนของอุปกรณ์ iPhone หรือ iPad ที่ถูกออกแบบให้ผู้ใช้สามารถเข้าถึงข้อมูลหรือทำงานบางอย่างได้โดยไม่ต้องเปิดแอปนั้นขึ้นมา

นอกจากความสวยงามและการเข้าถึงข้อมูลได้อย่างรวดเร็วแล้วเราจะเริ่มเห็น แอปพลิเคชัน ต่างๆ เริ่มมี widget ที่ช่วยให้ผู้ใช้ควบคุมแอปได้โดยตรงบนหน้าจอโฮมสกรีน เช่น widget ของแอปเพลงสามารถเปิดเพลงหรือข้ามเพลงได้โดยไม่ต้องเข้าแอป

Let’s Started

โดยบทความนี้จะมาแชร์การสร้าง static widgets ง่ายๆ โดยจำลองเป็น quick menu ของแอปธนาคารที่สามาถเข้าถึงและเปิดเมนูต่างๆในแอปพลิเคชันของเราได้เพียงจากการกด menu บน widget และ

  1. ก่อนอื่นเลยเราจะเริ่มจากการสร้างแอปพลิเคชันของเราโดยที่คิดไว้ว่าจะสร้าง หน้าขึ้นมาโดยมี menu tabbar ทั้งหมด 4 menu คือ Home, Balance, Transfer และ Scan

เริ่มจากสร้าง enum ขึ้นมาโดยกำหนดชื่อ tab และกำหนดค่าของ index ให้กับ แต่ละ tab

enum TabName: String {
case Home
case Balance
case Transfer
case Scan

var tabIndex: Int {
switch self {
case .Home:
return 0
case .Balance:
return 1
case .Transfer:
return 2
case .Scan:
return 3
}
}
}

2. สร้าง class และ method สำหรับการจัดการส่วน Deeplink

import SwiftUI

class DeeplinkCoordinator: ObservableObject {
@Published var selectedTab: Int = 0

func handleDeeplink(_ url: URL) {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let host = components.host,
let tab = TabName(rawValue: host)?.tabIndex else {
return
}
selectedTab = tab
}
}
  • สร้าง class DeeplinkCoordinator โดยใช้ ObservableObject ที่สามารถทำให้ SwiftUI สามารถอัพเดตข้อมูลและแสดงผลใหม่ได้ทันที
  • กำหนด property ชื่อ selectedTab ขึ้นมาเป็น @Published property เมื่อมีการเปลี่ยนแปลงค่าของ selectedTab แล้ว SwiftUI จะสามารถแสดงผลใหม่ได้ทันที
  • สร้าง function handleDeeplink ใช้ในการจัดการ Deeplink โดยรับพารามิเตอร์เป็น URL และทำการแกะ URL ออกมา และดึงค่า index ผ่าน enum TabName และกำหนดค่า index ที่ได้เข้ากับ property selectedTab

3. สร้าง main view หลักขึ้นมาโดยใช้ TabView เพื่อแสดงแท็บที่เกี่ยวข้องกับแต่ละหน้า (HomeView, BalanceView, TransferView, ScanView)

import SwiftUI

struct MainView: View {
@EnvironmentObject var deeplinkCoordinator: DeeplinkCoordinator

var body: some View {
TabView(selection: $deeplinkCoordinator.selectedTab) {
HomeView()
.tabItem {
Label("Home", systemImage: "house.fill")
}
.tag(TabName.Home.tabIndex)
BalanceView()
.tabItem {
Label("Balance", systemImage: "folder.fill")
}
.tag(TabName.Balance.tabIndex)
TransferView()
.tabItem {
Label("Transfer", systemImage: "paperplane.fill")
}
.tag(TabName.Transfer.tabIndex)
ScanView()
.tabItem {
Label("Scan", systemImage: "qrcode")
}
.tag(TabName.Scan.tabIndex)
}
.onAppear() {
UITabBar.appearance().backgroundColor = .white
}

}
}
  • โดย TabView จะเลือกแท็บที่ถูกเลือกจาก DeeplinkCoordinator ด้วย $deeplinkCoordinator.selectedTab และกำหนด tag ของแต่ละแท็บตาม tabIndex ที่ได้กำหนดไว้ในแต่ละ TabName
  • เมื่อมีการเปลี่ยนแปลงใน selectedTab ที่อยู่ใน DeeplinkCoordinator แล้ว SwiftUI จะทำการแสดงผลตาม selectedTab ที่ถูกเลือกใหม่

4. กำหนดหน้าจอหลักที่ใช้เป็นตัวเริ่มต้นของแอปพลิเคชัน

import SwiftUI

@main
struct WidgetQuickMenuApp: App {
@StateObject private var deepLinkCoordinator = DeeplinkCoordinator()

var body: some Scene {
WindowGroup {
MainView()
.environmentObject(deepLinkCoordinator)
.onOpenURL(perform: { url in
deepLinkCoordinator.handleDeeplink(url)
})
}
}
}
  • ในส่วนของ class ที่เป็น entry ของแอปพลิเคชันจะกำนหนด property deepLinkCoordinator โดยใช้ @StateObject เพื่อให้เก็บ object
  • ในส่วน Scene ที่เป็น WindowGroup ซึ่งเป็นหน้าจอหลักของแอปพลิเคชัน โดยแสดง MainView และใช้ .environmentObject modifier เพื่อให้ MainView สามารถเข้าถึง DeeplinkCoordinator ได้ และใช้ .onOpenURL modifier เพื่อทำการ handle Deeplink เมื่อแอปพลิเคชันถูกเปิดด้วย URL ที่ถูกส่งเข้ามา

5. หลังจากเตรียมโปรเจคเรียบร้อยแล้ว เริ่มสร้าง widget โดยให้เราไปที่
File -> Target -> Widget Extension

  • เนื่องจากในบทความนี้เราจะทำแค่ static widget ที่เป็นแค่เพียงเมนูโดยใช้แค่ StaticConfiguration ไม่ได้มีการกำหนดค่าเพื่อส่งไปแสดงเลยไม่จำเป็นต้องใช้ Configuration App Intent ให้เราติ๊กออกเลยครับ ส่วน Live Activity ก็ด้วยติ๊กออกได้เลย

6. สร้างปุ่ม menu view และ การเปิดหน้าด้วย Deeplink ให้รองรับจากการกด menu บน widget ตาม source ที่ได้รับมาแต่ละ menu

struct QuickMenu: View {
var source: String

var body: some View {
if let url = URL(string: "widgetquickmenu://\(source)") {
Link(destination: url) {
VStack {
Image(source)
.resizable()
.frame(width: 25, height: 25)
Text(source)
.font(
.system(size: 8)
.weight(.heavy)
)
.foregroundColor(.black)
}
.frame(minWidth: 0, maxWidth: .infinity)
.padding()
.background(
Rectangle()
.cornerRadius(8)
.foregroundColor(Color.white)
.shadow(
color: Color.black.opacity(0.1),
radius: 5,
x: 0,
y: 0
)
)
}
}
}
}

7.หลังจากนั้นเรามาสร้าง widget view ของเรา โดยจะกำหนด source ที่เรากำหนดไว้แต่ละหน้า menu ให้เปิดตามที่เราต้องการ

struct WidgetView: View {
var body: some View {
VStack {
Text("Quick Menu")
.font(
.system(size: 22)
.weight(.heavy)
)
.foregroundColor(.black)
Spacer()
HStack {
QuickMenu(source: "Home")
.frame(minWidth: 0, maxWidth: .infinity)
QuickMenu(source: "Balance")
.frame(minWidth: 0, maxWidth: .infinity)
QuickMenu(source: "Transfer")
.frame(minWidth: 0, maxWidth: .infinity)
QuickMenu(source: "Scan")
.frame(minWidth: 0, maxWidth: .infinity)
}
}
}
}

8.กำหนด TimlineEntry และ Provider

struct DataEntry: TimelineEntry {
let date: Date
}

struct Provider: TimelineProvider {
func placeholder(in context: Context) -> DataEntry {
DataEntry(date: Date())
}

func getSnapshot(in context: Context, completion: @escaping (DataEntry) -> ()) {
let entry = DataEntry(date: Date())
completion(entry)
}

func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
let entries: [DataEntry] = [DataEntry(date: Date())]

let timeline = Timeline(entries: entries, policy: .never)
completion(timeline)
}
}

TimlineEntry

ในส่วนของ DataEntry จะเป็น Model ที่ใช้ในการกำหนดและส่งไปอัพเดต widgets เราจะกำหนดแค่ date เพราะว่า static widget ของเราไม่ต้องการส่งข้อมูลอะไรเลย

Provider

Provider จะมี mandatory หลัก 3 function คือ

  • placeholder: กำหนดการแสดงข้อมูลครั้งแรกของ Widgets
  • getSnapShot: กำหนดการ preview ของ widgets ในส่วนของเพิ่ม widget
  • getTimeline : กำหนดข้อมูลที่จะส่งไปแสดงและกำหนดระยะเวลาโหลดข้อมูล

จากตัวอย่างเราจะกำหนด Policy ของ timeline เป็น policy: .nerver เพื่อระบุว่าไม่มีการอัปเดตข้อมูลและป้องกันไม่ให้มีการโหลดข้อมูลใหม่โดยไม่จำเป็น ถ้าต้องการกำหนด timeline ในแบบอื่นๆ สามาถเข้าไปดู policy ต่างๆ ได้ ที่ TimelineReloadPolicy

9.กำหนด Widget Configuration

struct Quickmenu: Widget {
private let kind: String = "Quickmenu"

var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
WidgetView()
}
.configurationDisplayName("Quickmenu")
}
}
  • kind ระบุชื่อการตั้งค่าของ widget เปรียบเสมือน id ของ widget ตัวนั้นๆ
  • StaticConfiguration กำหนดค่าสถานะและการแสดงผลของ Widget ด้วย provider ที่เราสร้างกำหนดขึ้นมาก่อนหน้านี้ และเรียกใช้ WidgetView
  • configurationDisplayName ตัวกำหนดชื่อแสดงของ Widget ที่จะปรากฏในการตั้งค่า

นอกเหนือจากตัวอย่างสามารถดูการตั้งค่าอื่นๆได้ที่ WidgetConfiguration

ทุกอย่างพร้อมแล้วมาทดสอบการทำการงานกันเลย

Conclusion

หลังจากในตัวอย่างคงเห็นกันแล้วว่าการสร้าง widget นั้นไม่ซับซ้อนและง่ายกว่าที่คิดนอกจาก Widget แบบ static ที่ทำเป็นตัวอย่างให้ดู จริงๆ แล้วตัว widget เอง สามารถไป Config และใช้งานได้อีกหลายอย่าง เช่นการแสดงข้อมูลจาก application โดยใช้ IntentConfiguration สามารถเข้าไปศึกษาเพิ่มเติมกันได้เลย ส่วนข้อจำกัดตอนนี้ คือ Widgets สามารถใช้งานได้บน iOS14 ขึ้นไปและพัฒนาได้ด้วย SwiftUI เท่านั้น

ใครสนใจอยากโหลดไปลองเล่นสามารถเข้าไปดูตัวอย่างโปรเจคได้ที่นี้ Example Project

Reference:

--

--