[iOS] มาทำ Widget Home Screen กันวัยรุ่น

Vorapat Phorncharroenroj
Bitkub.com
Published in
10 min readDec 6, 2022

หลายคนที่ใช้ อุปกรณ์ Apple กันน่าจะเคยใช้ Widget ทั้ง iPhone / iPad / Mac จะเห็นบางแอปก็มี บางแอปก็ไม่มี มันทำงานยังไงนะ หรือใครกำลังพัฒนา Application แล้วอยากมี Widget บนแอปที่กำลังทำอยู่ วันนี้ผมจะมาแชร์วิธีทำไปพร้อมๆ กันครับ

เนื้อหาที่จะมาแชร์มีดังนี้

  1. Widget คืออะไร
  2. สิ่งที่ต้องมีก่อนทำ Widget
  3. เริ่มสร้าง Widget
  4. สร้าง View แยกแต่ละ Size ของ Widget
  5. การ Update ข้อมูล Widget Timeline
  6. เพิ่ม Configuration ให้ตัว Widget
  7. ทำ Deep link Widget
  8. Widget Limitations

1. Widget คืออะไร

ตัวอย่าง Widget บน Iphone

Widget คือ พื้นที่การแสดงข้อมูลของแอปพลิเคชัน โดยที่เราไม่ต้องเข้าแอปพลิเคชันเพื่อไปดูข้อมูลนั้นๆ เช่น เราต้องการดูสภาพอากาศวันนี้เราก็สามารถดูผ่าน Widget บน home screen ได้โดยไม่ต้องเข้าแอปสภาพอากาศไปดูข้อมูล แต่แอปพลิเคชันนั้นจะต้อง ทำ Widget มารองรับเพื่อแสดงผลด้วย

2. สิ่งที่ต้องมีก่อนทำ Widget

  • Xcode 12.0 +
  • พื้นฐาน SwiftUI

3. เริ่มสร้าง Widget

Widget ที่เราจะสร้างมีรายละเอียดดังนี้

  • โชว์ Favourite Vehicle ในหน้า Home Screen
  • Widget แต่ละขนาดจะโชว์ UI ที่แตกต่างกัน
  • Widget สามารถปรับเปลี่ยนการแสดงผลตามค่า config

สุดท้ายจะออกมาหน้าตาแบบนี้

มาเริ่มกันเลยอันดับแรก สร้าง New Project ขึ้นมาโดยตั้งชื่อ Project ว่า FavouriteVehicle ดังภาพ

FavouriteVehicle

หลังจากสร้าง Project เสร็จแล้วเราจะทำการสร้าง Widget Extension

  • ไปที่ File -> New -> Target หลังจากนั้นพิมพ์ Widget ในช่อง Search เลือก Widget Extension แล้วกด Next
Widget Extension

หลังจากกด Next ให้ตั้งชื่อ Product Name -> FavouriteVehicleWidget

  • Include Configuration Intent -> คือส่วนที่ทำให้ User สามารถตั้งค่า Configuration Widget ได้เดียวเราจะมาทำกันทีหลังครับ ตอนนี้ให้ติ๊กออกก่อน

กด Finish

Create Widget Extension

หลังจากกด Finish Xcode จะถามว่าต้องการ เพิ่ม Scheme ตัว Widget Extension ไหมให้กด Activate เพื่อใช้ Run ตัว Widget

Add Scheme Widget Extension

เราจะได้ Folder ใหม่เพิ่มเข้ามาตามภาพ

สร้าง Model

อันดับแรกสุดเราจะสร้าง Model เพื่อนำไปโชว์ในหน้า Widget กัน

  • สร้าง Folder ชื่อ Model แล้วสร้างไฟล์ VehicleDetails.swift โดยเราจะเก็บ name, description, image ส่วน id เราจะเอาไปใช้ทำ configuration กับ deeplink ทีหลัง
import Foundation

public struct VehicleDetails {
public let name: String
public let description: String
public let image: String

init(name: String, description: String, image: String) {
self.name = name
self.description = description
self.image = image
}
}

extension VehicleDetails: Identifiable {
public var id: String {
name
}
}

สร้าง Data Provider

Data Provider มีหน้าที่ส่งค่า Vehicles กลับมา ให้สร้าง Folder ชื่อ Data แล้วสร้างไฟล์ชื่อ VehiclesProvider.swift จะได้ตามนี้

 struct VehiclesData {
static func fetchAllVehicles() -> [VehicleDetails] {
let vehicles = [
VehicleDetails(name: "Car", description: "I 💙 Car!!", image: "car"),
VehicleDetails(name: "Airplane", description: "I 💚 Airplan!!", image: "airplane"),
VehicleDetails(name: "Bus", description: "I 💛 Bus!!", image: "bus"),
VehicleDetails(name: "Ferry", description: "I 🧡 Ferry!!", image: "ferry"),
VehicleDetails(name: "Scooter", description: "I 💜 Car!!", image: "scooter")
]
return vehicles
}
}

อย่าลืมติ๊ก Apply Target ตัว Widget Extension ในไฟล์ VehicleDetails.swift กับ VehiclesProvider.swift ด้วยละวัยรุ่น เพราะไฟล์ที่สร้างมาจะถูกนำไปใช้ในส่วน Widget Extension

หลังจากเราสร้าง VehicleDetails.swift กับ VehiclesProvider.swift แล้ว Folder จะได้ประมาณนี้

4. สร้าง View แยกแต่ละ Size ของ Widget

มาถึงขั้นตอนที่เราจะออกแบบหน้า view ให้ไปแสดงในส่วนของ widget กันสุดท้ายหน้าตาจะออกมาแบบนี้

View Small Size
View Medium Size
View Large Size

เริ่มแรกสร้าง Folder ชื่อ View ใน Folder FavouriteVehicleWidget หลังจากนั้นสร้างไฟล์ SwiftUI ชื่อ VehicleWidgetView.swift หลังจากสร้างเสร็จ import WidgetKit ด้วยนะ

สร้าง View สำหรับ Widget Small / Medium / Large

struct VehicleSmallView: View {
let vehicleDetails: VehicleDetails

var body: some View {
ZStack {
Color(.systemTeal)
VStack(spacing: 12) {
Image(systemName: vehicleDetails.image)
Text(vehicleDetails.name)
}
}
}
}

struct VehicleMediumView: View {
let vehicleDetails: VehicleDetails

var body: some View {
ZStack {
Color(.systemTeal)
VStack(spacing: 12) {
HStack {
Image(systemName: vehicleDetails.image)
Text(vehicleDetails.name)
}
Text(vehicleDetails.description)
}
.padding()
}
}
}

struct VehicleLargeView: View {
let vehicleDetails: VehicleDetails

var body: some View {
ZStack {
Color(.systemTeal)
VStack(spacing: 12) {
Image(systemName: vehicleDetails.image)
Text(vehicleDetails.name)
Text(vehicleDetails.description)
}
}
}
}

Add ในส่วนของ Preview Provider UI เข้าไปเพื่อให้ง่ายต่อการสร้าง UI หรือ Debug UI ตัว Canvas เราก็จะได้ UI Widget มาโชว์แล้ว

struct VehicleWidgetView_Previews: PreviewProvider {
static var previews: some View {
VehicleSmallView(vehicleDetails: .init(name: "Ferry", description: "I love ferry", image: "ferry"))
.previewContext(WidgetPreviewContext(family: .systemSmall))

VehicleMediumView(vehicleDetails: .init(name: "Bus", description: "I like bus", image: "bus"))
.previewContext(WidgetPreviewContext(family: .systemMedium))

VehicleLargeView(vehicleDetails: .init(name: "Scooter", description: "I like scooter", image: "scooter"))
.previewContext(WidgetPreviewContext(family: .systemLarge))
}
}

หลังจากนั้นเราจะแก้ในส่วน VehicleWidgetView ให้เป็นตัวดักทางเข้า View ว่าจะ แสดง Widget Size แบบไหน โดยอ้างอิงจากตัวแปร family โดยเราจะ Handle Switch Case Family หากเงื่อนไขการ Render ตรง Size ไหนก็จะเข้าเงื่อนไข View Size นั้นๆ

import SwiftUI
import WidgetKit

struct VehicleWidgetView: View {
@Environment(\.widgetFamily) var family: WidgetFamily
let vehicleDetails: VehicleDetails

@ViewBuilder
var body: some View {
switch family {
case .systemSmall:
VehicleSmallView(vehicleDetails: vehicleDetails)
case .systemMedium:
VehicleMediumView(vehicleDetails: vehicleDetails)
case .systemLarge:
VehicleLargeView(vehicleDetails: vehicleDetails)
default:
EmptyView()
}
}
}

5. การ Update ข้อมูล Widget Timeline

ต่อไปเราจะนำ Data ที่เราสร้างขึ้นมาพร้อมกับหน้า View นำไปแสดงในส่วนของ Widget โดย Data Widget เราจะทำให้เปลี่ยนข้อมูลทุกๆ 1 นาที

ขั้นตอนแรกเราจะเปลี่ยนชื่อ Model TimelineEntry จาก SimpleEntry เป็น VehicleDetailsEntry และเพิ่ม ตัวแปรสำหรับส่งค่า Vehicle Details ในไฟล์ FavouriteVehicleWidget.swift

struct VehicleDetailsEntry: TimelineEntry {
let date: Date
let vehicleDetails: VehicleDetails
}

หลังจากนั้นมาในส่วน Provider ทำการเปลี่ยนไปใช้โมเดล VehicleDetailsEntry ตามนี้

Placeholder

ข้อมูล Widger ตอน Render ครั้งแรกจะนำ View Placholder โดยใช้ข้อมูลส่วนนี้นำไปโชว์

struct Provider: TimelineProvider {
func placeholder(in context: Content) -> VehicleDetailsEntry {
VehicleDetailsEntry(
date: Date(),
vehicleDetails: .init(
name: "Car",
description: "I Love Car",
image: "car")
}
}

Get Snapshot

ข้อมูลที่จะเอาไปโชว์ตอน Preview สร้าง Widget ตอนที่จะเพิ่ม Widget ลงในหน้า Home Screen

func getSnapshot(in context: Context, completion: @escaping (VehicleDetailsEntry) -> ()) {
let entry = VehicleDetailsEntry(
date: Date(),
vehicleDetails: .init(
name: "Ferry",
description: "I Love Ferry",
image: "ferry")
)
completion(entry)
}

Get Timeline

การปั้น Data Timeline เพื่อนำไป Update Widget จากตัวอย่าง เราจะปั้น 1 Timeline ที่ประกอบไปด้วย Data Vehicle Details 5 ก้อน โดย อัปเดตข้อมูล Widget ทุกๆ 1 นาที

func getTimeline(in context: Context, completion: @escaping(Timeline<Entry>) -> ()) {
var entries: [VehicleDetailsEntry] = []
let currentDate = Date()
let vehiclesDetails = VehiclesData.fetchAllVehicles()
for index in 0..<5 {
let entryDate = Calendar.current.date(byAdding: .minute, value: index, to: currentDate)!
let entry = VehicleDetailsEntry(date: entryDate, vehicleDetails: vehiclesDetails[index])
entries.append(entry)
}

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

โดยส่วน Object Timeline นั้นจะมีส่วน Policy ที่เราต้อง Set ค่า Policy ลงไป

TimelineReloadPolicy มี 3 ประเภท

  1. .atEnd - Widget Request Data Timeline ใหม่หลังจากแสดงข้อมูลชุดสุดท้ายของ Timeline เดิมไปแล้ว
  2. .after — Widget Request Data Timeline ใหม่โดยอ้างอิงจากค่าเวลาที่ส่งเข้ามา เช่น ส่งมา 1 ชั่วโมง การ Request Timeline ใหม่ก็จะเกิดขึ้นหลังจาก 1 ชั่วโมง ผ่านไปหลังจากแสดงข้อมูล Timeline เดิมเสร็จแล้ว
  3. .never - Widget ไม่ Request Data Timeline ใหม่หลังจากแสดงข้อมูลชุดสุดท้ายของ Timlieline เก่าไปแล้ว

หลังจากนั้นเปลี่ยนทางเข้า View ไปเป็นอันที่เราสร้างขึ้นมาตามนี้

struct FavouriteVehicleWidgetEntryView : View {
var entry: Provider.Entry

var body: some View {
VehicleWidgetView(vehicleDetails: entry.vehicleDetails)
}
}

หลังจากนั้นเราจะมาเพิ่ม Support Families ให้ตัว Widget รวมถึง Display Name และ Description ให้ตัว Widget ตามนี้

  • kind คือ ID ของ Widget ต้องเป็น Unique String
  • StaticConfiguration คือ Object เพื่อแสดงเนื้อหาของ Widget โดยรับ kind และ Object Provider เพื่ออัปเดต Timeline Widget View
  • .supportedFamilies กำหนดว่า Widget Support ขนาดไหนบ้าง ซึ่งเราจะเพิ่มในส่วนของที่เราสร้าง View มา 3 ขนาด คือ systemSmall, systemMedium, systemLarge
  • configurationDisplayName คือ Title Name Widget
  • description คือ คำอธิบายของ Widget นั้นๆ
@main
struct FavouriteVehicleWidget: Widget {
let kind: String = "FavouriteVehicleWidget"

var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
FavouriteVehicleWidgetEntryView(entry: entry)
}
.configurationDisplayName("Favourite Vehicle")
.description("Display Favourite Vehicle")
.supportedFamilies([.systemSmall, systemMedium, systemLarge])
}
}

ส่วน Preview ก็ให้เพิ่มข้อมูลเพื่อใช้ Preview ลงไป

struct FavouriteVehicleWidget_Previews: PreviewProvider {
static var previews: some View {
FavouriteVehicleWidgetEntryView(
entry: VehicleDetailsEntry(
date: Date(),
vehicleDetails: .init(
name: "Ferry",
description: "I Love Ferry",
image: "ferry")
)
)
.previewContext(WidgetPreviewContext(family: systemSmall))
}
}

กดเลือก Schema Widget แล้ว Run เลยวัยรุ่นเมืองไทย ตอนนี้เราก็จะได้ Widget ที่มี 3 ขนาดตามที่เราสร้างมาและ Reload Data ทุกๆ 1 นาทีแล้ว 👏👏 🎉🎉

6. เพิ่ม Configuration ให้ตัว Widget

ต่อไปเราจะเพิ่ม Configuration ให้ตัว Widget แสดงผลตามที่ User ตั้งค่าได้โดยเราจะให้ User สามารถแก้ไข Widget เพื่อเลือก Vehicle ที่ชอบแล้วนำไปแสดงบน Widget มาเริ่มกันเลย

ซึ่งส่วนนี้หากเราติ๊ก Include Configuration Intent ตอนสร้าง Project จะได้ไฟล์ ในส่วนนี้มาแต่เราจะมาสร้างเพิ่มทีหลังกัน

สร้าง Intent Definition

อันดับแรก ไปที่ File -> New -> File ค้นหา Sirikit Intent Definition File กดถัดไปโดยตั้งชื่อตามนี้ FavouriteVehicleIntents

หลังจากนั้นเปิดไฟล์ FavouriteVehicleIntents.intentdefinition แล้วกด + New Intent เปลี่ยนชื่อเป็น SelectVehicle

จากนั้นสร้าง New Type กด + แล้วไปที่ New Type ตั้งชื่อว่า VehicleINO จะได้ properties identifier กับ displayString

หลังจากเพิ่ม Type Vehicle แล้วกลับมาที่ SelectVehicle

  • เปลี่ยน Category ให้เป็น View
  • ติ๊ก Intent is eligible for widget
  • ติ๊ก User can edit value in Shortcuts widget
  • ติ๊ก Options are provideed dynamically
  • กด + ในช่อง parameter เพื่อเพิ่ม Parameter เลือกเปลี่ยนชื่อเป็น vehicle และตรง Type ให้เลือก VehicleINO ที่เราพึ่งสร้างไปจะได้ตามนี้

สร้าง Intents Extension

เราจะมาสร้าง Intent Extention กันต่อ ให้ไปที่ Project กด + แล้วเลือก Intents Extension

ตั้งชื่อว่า FavouriteVehicle Intent แล้วกด Finish แล้วกด Activate Scheme

เราจะกลับมาติ๊ก Target Membership ของตัว FavouriteVehicle Intent โดยไฟล์ที่ต้องติ๊ก Apply Target มีดังนี้

  • VehiclesProvider.swift
  • VehicleDetails.swift
  • FavouriteVehicleIntents.intentdefinition

เชื่อม Class Intent Definition กับ Intent Extension

คลิก Project แล้วไปที่ Target FavouriteVehicle Intent ที่เราพึ่งสร้าง ตรง Support Intent ให้ใส่ ชื่อ class SelectVehicleIntent ลงไปตามที่เราพึ่งสร้างใน intent definition

Provide Data Configuration

ต่อไปเราจะมาสร้างในส่วน Provide Data ในส่วนของ Configuration ให้คลิกไปที่ไฟล์ IntentHandler.swift

  • Apply Protocol ชื่อ SelectVehicleIntentHandling

Protocol นี้จะ Generate Auto มาให้หลังจากเราสร้าง Custom Intent ในไฟล์ Intent Definition และเชื่อม Class Intent Definition กับ Intent Extension แล้ว

  • provideVehicleOptionsCollection เราจะส่ง Data Vehicles ทั้งหมดที่มีให้ User เลือก Configuration จาก Widget โดยปั้น Object VehicleINO ที่เราสร้างขึ้นมา แล้วส่งค่า id และ name จะได้ตามนี้
class IntentHandler: INExtension {
override func handler(for intent: INIntent) -> Any {
// This is the default implementation. If you want different objects to handle different intents,
// you can override this and return the handler you want for that particular intent.

return self
}
}

extension IntentHandler: SelectVehicleIntentHandling {
func provideVehicleOptionsCollection(
for intent: SelectVehicleIntent,
with completion: @escaping (INObjectCollection<VehicleINO>?, Error?) -> Void
) {
var vehicles = [VehicleINO]()
VehiclesData.fetchAllVehicles().forEach { vehicle in
let vehicleINO = VehicleINO(identifier: vehicle.id, display: vehicle.name)
vehicles.append(vehicleINO)
}
completion(INObjectCollection(items: vehicles), nil)
}
}

เปลี่ยน StaticConfiguration เป็น IntentConfiguration

ตอนนี้ Widget เป็นแบบ StaticConfiguration อยู่ดังนั้นเราจะมาเปลี่ยนให้เป็น IntentConfiguration กัน

  • StaticConfiguration คือ Widget ที่ไม่สามารถแก้ไขตั้งค่า Widget ได้
  • IntentConfiguration คือ Widget ที่สามารถแก้ไขตั้งค่า Widget ได้
  • Provider เปลี่ยน Protocol จาก TimelineProvider เป็น IntentTimelineProvider ตัว typealias ของ Entry ให้ใช้ VehicleDetailsEntry และ typealias ของ Intent ให้ใช้ SelectVehicleIntent
struct Provider: IntentTimelineProvider {
typealias Entry = VehicleDetailsEntry
typealias Intent = SelectVehicleIntent
....
....
}
  • function getSnapshot จะมีการเพิ่ม Parameter Configuration เพิ่มเข้ามา เราจะทำการแก้ไขให้ ส่ง Vehicle ตรงตามค่า configuration ที่รับมาแล้ว return Vehicle Model ที่มีค่า Id ตรงกับ ค่า Configuration
func getSnapshot(for configuration: SelectVehicleIntent, in context: Context, completion: @escaping (VehicleDetailsEntry) -> Void) {
let vehicle = VehiclesData.fetchAllVehicles().first ?? VehicleDetails(name: "Car", description: "I Love Car", image: "car")
var entry = VehicleDetailsEntry(date: Date(), vehicleDetails: vehicle)
guard
let id = configuration.vehicle?.identifier,
let vehicle = VehiclesData.fetchAllVehicles().first(where: { vehicle in
vehicle.id == id
})
else {
return completion(entry)
}
entry = VehicleDetailsEntry(date: Date(), vehicleDetails: vehicle)
completion(entry)
}
  • function getTimeline จะมีการเพิ่ม Parameter Configuration เหมือนกันดังนั้นเราจะเพิ่มเงื่อนไขหาก User เลือก Configuration มาเราจะส่ง Vehicle Id ที่ตรงกับ Configuration นั้นกลับไปเช่นเดียวกัน
func getTimeline(for configuration: SelectVehicleIntent, in context: Context, completion: @escaping (Timeline<VehicleDetailsEntry>) -> Void) {
var entries = [VehicleDetailsEntry]()
let vehicle = VehiclesData.fetchAllVehicles().first(where: { vehicle in
vehicle.id == configuration.vehicle?.identifier
}) ?? VehicleDetails(name: "Car", description: "I Love Car", image: "car")
let entry = VehicleDetailsEntry(date: Date(), vehicleDetails: vehicle)
entries.append(entry)
let timeline = Timeline(entries: entries, policy: .never)
completion(timeline)
}
  • แก้ไขตัว Widget จาก Staticonfiguration ให้เป็น IntentConfiguration โดยค่าที่ต้องส่งเพิ่มเข้าไปคือ intent มาจาก Class ที่เราสร้างเอาไว้ก่อนหน้านี้
@main
struct FavouriteVehicleWidget: Widget {
let kind: String = "FavouriteVehicleWidget"

var body: some WidgetConfiguration {
IntentConfiguration(
kind: kind,
intent: SelectVehicleIntent.self,
provider: Provider()
) { entry in
FavouriteVehicleWidgetEntryView(entry: entry)
}
.configurationDisplayName("Favourite Vehilce")
.description("Display Favourite Vehicle")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}

จากนั้นเรามาลอง Run Widget กัน ให้เลือก Scheme Widget แล้วกด Run เราจะสามารถแก้ไข Widget ได้แล้วโดยข้อมูลที่แสดงบน Widget จะอ้างอิงจาก การเลือก โดยเทียบค่า id ให้ตรงกัน ทุกครั้งที่เราเลือก Config ตัว function getTimeline จะถูกเรียกอีกครั้งและส่ง timeline ชุดใหม่มาแสดง

มีหน้าแก้ไข Widget เพิ่มเข้ามา
เลือก Vehicle เพื่อนำไปแสดง
Bus Vehicle

เราก็จะได้ Vehicle นำไปแสดงบน Widget ตามที่เราเลือกแล้วเกินปุยมุ้ยวัยรุ่น

7. ทำ Deep Link Widget

อันดับแรกเราจะสร้างหน้า Vehicle Detail ไว้รองรับ Deep Link เมื่อ Tap Widget ให้มาเปิดหน้านี้

เริ่มสร้าง Folder View และสร้างไฟล์ SwiftUI ชื่อ VehicleDetailsView.swift

import SwiftUI

struct VehicleDetailsView: View {
@Environment(\.dismiss) var dismiss
let vehicleDetails: VehicleDetails

var body: some View {
ZStack {
Color(.systemPink)
VStack(spacing: 16) {
Image(systemName: vehicleDetails.image)
Text(vehicleDetails.name)
Text(vehicleDetails.description)
Button("Dismiss") {
dismiss()
}
}
}
.ignoresSafeArea()
}
}

struct VehicleDetailsView_Previews: PreviewProvider {
static var previews: some View {
VehicleDetailsView(vehicleDetails: .init(name: "Car", description: "I Love Car", image: "car"))
}
}

Folder ที่ได้จะเป็นแบบนี้

จากนั้นมาเพิ่ม Url ใน model VehicleDetails เพื่อนำมาใช้ในการเปิด Deep Link

import Foundation

public struct VehicleDetails {
public let name: String
public let description: String
public let image: String
public let url: URL?

init(name: String, description: String, image: String) {
self.name = name
self.description = description
self.image = image
self.url = URL(string: "vehicle://\(name)")
}
}

extension VehicleDetails: Identifiable {
public var id: String {
name
}
}

ทำการเพิ่ม View Modifier ของ VehicleSmallView / VehicleMediumView / VehicleLargeView ด้วย .widgetURL(vehicleDetail.url)

VehicleSmallView
VehicleMediumView
VehicleLargeView

หลังจากนั้นมาเพิ่มในส่วนของ ContentView

  • เพิ่ม model vehicleDetails ไว้ส่งเป็น binding ตอนเปิดหน้า VehicleDetailsView.swift และเก็บค่า model เมื่อเปิด url
  • เพิ่ม onOpenURL เราจะ Handle โดยการเช็ก url ที่เปิดมาจาก Widget ตรงกับ Data ชุดไหนเราก็จะเอา Data ชุดนั้น Assign เก็บไปที Model VehicleDetails ที่เราสร้างมา
  • เพิ่ม fullScreenCover โดยส่งตัวแปร binding vehicleDetails เมื่อเราเปิด url แล้วเก็บค่าเข้า vehicleDetails ก็จะทำการเปิด present full screen ไปที่หน้า VehicleDetailsView
struct ContentView: View {
@State private var vehicleDetails: VehicleDetails?

var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
.padding()
.onOpenURL { url in
guard let vehicleDetails = VehiclesData.fetchAllVehicles().first(where: { $0.url == url }) else { return }
self.vehicleDetails = vehicleDetails
}
.fullScreenCover(item: $vehicleDetails, content: { vehicleDetails in
VehicleDetailsView(vehicleDetails: vehicleDetails)
})
}
}

ผลลัพธ์ที่ได้ก็จะออกมาเป็นแบบนี้ตอนกด Tap Widget ตัวแอปจะไปหน้า VehicleDetails และส่งข้อมูล Data ที่ตรงกับ ค่า Config Widget เพื่อนำมาแสดง

8. Widget Limitations

  • Widget รองรับเฉพาะบน SwiftUI เท่านั้น บน UIKit หมดสิทธิ์
  • Widget ไม่สามารถทำ Animation หรือ ใส่ Video ได้
  • Widget ไม่สามารถใส่ Scroll View ได้
  • Widget Size ไม่สามารถ Custom ได้ต้องทำขนาดที่ Apple กำหนดมาให้เท่านั้น Small / Medium / Large / Extra Large (iPad)
  • Widget บางครั้งเมื่อถึงเวลา Reload Data แล้ว Widget จะไม่แสดงข้อมูลล่าสุดจนกว่า User จะเลื่อนหน้าจอไปดู Widget

มาถึงตอนนี้แล้วบอกเลยการทำ Widget ไม่ได้ยากอย่างที่คิด และสิ่งที่น่าสนใจมากกว่านั้นคือ Apple พึ่งปล่อย Widget Lock Screen มาไม่นาน อาจไปลองศึกษาเพิ่มเติมกันได้ครับ

สุดท้ายนี้หากอธิบายตรงไหนผิดพลาดประการใด ต้องอภัยมา ณ ที่นี้ด้วยครับ

ขอบคุณวัยรุ่นทุกคนที่เข้ามาอ่านครับผม

References

Creating a Widget Extension

Making a Configurable Widget

--

--