บทความ
[iOS] มาทำ Widget Home Screen กันวัยรุ่น
หลายคนที่ใช้ อุปกรณ์ Apple กันน่าจะเคยใช้ Widget ทั้ง iPhone / iPad / Mac จะเห็นบางแอปก็มี บางแอปก็ไม่มี มันทำงานยังไงนะ หรือใครกำลังพัฒนา Application แล้วอยากมี Widget บนแอปที่กำลังทำอยู่ วันนี้ผมจะมาแชร์วิธีทำไปพร้อมๆ กันครับ
เนื้อหาที่จะมาแชร์มีดังนี้
Widget คืออะไร
สิ่งที่ต้องมีก่อนทำ Widget
เริ่มสร้าง Widget
สร้าง View แยกแต่ละ Size ของ Widget
การ Update ข้อมูล Widget Timeline
เพิ่ม Configuration ให้ตัว Widget
ทำ Deep link Widget
Widget Limitations
1. Widget คืออะไร
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 ดังภาพ
หลังจากสร้าง Project เสร็จแล้วเราจะทำการสร้าง Widget Extension
ไปที่ File -> New -> Target หลังจากนั้นพิมพ์ Widget ในช่อง Search เลือก Widget Extension แล้วกด Next
หลังจากกด Next ให้ตั้งชื่อ Product Name -> FavouriteVehicleWidget
Include Configuration Intent -> คือส่วนที่ทำให้ User สามารถตั้งค่า Configuration Widget ได้เดียวเราจะมาทำกันทีหลังครับ ตอนนี้ให้ติ๊กออกก่อน
กด Finish
หลังจากกด Finish Xcode จะถามว่าต้องการ เพิ่ม Scheme ตัว Widget Extension ไหมให้กด Activate เพื่อใช้ Run ตัว Widget
เราจะได้ 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 กันสุดท้ายหน้าตาจะออกมาแบบนี้
เริ่มแรกสร้าง 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) -> ()) {
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 ประเภท
.atEnd - Widget Request Data Timeline ใหม่หลังจากแสดงข้อมูลชุดสุดท้ายของ Timeline เดิมไปแล้ว
.after — Widget Request Data Timeline ใหม่โดยอ้างอิงจากค่าเวลาที่ส่งเข้ามา เช่น ส่งมา 1 ชั่วโมง การ Request Timeline ใหม่ก็จะเกิดขึ้นหลังจาก 1 ชั่วโมง ผ่านไปหลังจากแสดงข้อมูล Timeline เดิมเสร็จแล้ว
.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?, 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) -> 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 ชุดใหม่มาแสดง
เราก็จะได้ 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)
หลังจากนั้นมาเพิ่มในส่วนของ 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