Save 37% off PRO during our Black Friday Sale! »

使用 WidgetKit 開發 iOS widget

使用 WidgetKit 開發 iOS widget

設定 widget 時,顯示的名字跟說明
產生 timeline 的 Provider
widget 顯示的畫面 
TimelineProvider 的 getSnapshot
TimelineProvider 的 getTimeline
widget 畫面支援的元件
設定 widget 背景顏色
 隨時間自動更新的 widget
點擊 widget 啟動 App
從 App 更新 widget
利用 App Group 讓 widget 跟 iOS App 共享資料
widget 顯示網路抓取的資料
TimelineProvider 的 placeholder
依據不同的 size 設定 widget

Transcript

  1. WidgetKit 彼得潘

  2. 相關教學 • WidgetKit framework • Creating a Widget Extension
 •

    Building Widgets Using WidgetKit and SwiftUI
  3. 建立 widget File > New > Target Widget Extension

  4. 建立 widget 不勾選 Include Con f i guration Intent。勾選的話比較複雜,表⽰使⽤者可以客製 widget

  5. 使⽤者客製 widget https://support.apple.com/en-us/HT207122

  6. 使⽤者客製 widget : Edit Widget

  7. widget 相關檔案 widget 有⾃⼰的 asset

  8. 將 widget 加到 iOS 桌⾯ 長按桌⾯後按 + 沒啟動過的 App ,不會出現在

    widget 選單裡,所以 App ⾄少要啟動⼀次
  9. 三種 widget 的⼤⼩

  10. widget 顯⽰在桌⾯

  11. 設定 widget 時,顯⽰的名字跟說明 @main struct SumikkoGurashiWidget: Widget { let kind:

    String = "SumikkoGurashiWidget" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: Provider()) { entry in SumikkoGurashiWidgetEntryView(entry: entry) } .configurationDisplayName("My Widget") .description("This is an example widget.") } }
  12. 設定 widget 時,顯⽰的名字跟說明 var body: some WidgetConfiguration { StaticConfiguration(kind: kind,

    provider: Provider()) { entry in SumikkoGurashiWidgetEntryView(entry: entry) } .configurationDisplayName("⾓落⽣物") .description("住在⾓落的可愛⽣物") }
  13. Provider • conforming to TimelineProvider 
 • 產⽣ timeline,控制 widget

    顯⽰的資料跟更新的時間 • timeline 裡包含多個 timeline entry
 • timeline entry conforming to TimelineEntry,包含了資料和更新 widget 的時間
  14. 範例的 time line entry struct SimpleEntry: TimelineEntry { let date:

    Date } date 欄位決定 widget 更新的時間 public protocol TimelineEntry { /// The date for WidgetKit to render a widget. var date: Date { get } /// The relevance of a widget’s content to the user. var relevance: TimelineEntryRelevance? { get } }
  15. widget 顯⽰的畫⾯  @main struct SumikkoGurashiWidget: Widget { let kind: String

    = "SumikkoGurashiWidget" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: Provider()) { entry in SumikkoGurashiWidgetEntryView(entry: entry) } .configurationDisplayName("⾓落⽣物") .description("住在⾓落的可愛⽣物") } }
  16. widget 顯⽰的畫⾯  struct SumikkoGurashiWidgetEntryView : View { var entry: Provider.Entry

    var body: some View { Text(entry.date, style: .time) } } entry 是 widget 要顯⽰的資料 顯⽰時間
  17. Provider.Entry 的型別是 SimpleEntry struct Provider: TimelineProvider { func placeholder(in context:

    Context) -> SimpleEntry { SimpleEntry(date: Date()) } 因為 public protocol TimelineProvider { associatedtype Entry : TimelineEntry func placeholder(in context: Self.Context) -> Self.Entry
  18. TimelineProvider 的 getSnapshot struct Provider: TimelineProvider { func getSnapshot(in context:

    Context, completion: @escaping (SimpleEntry) -> ()) { let entry = SimpleEntry(date: Date()) completion(entry) } 選擇 widget 時顯⽰的資料
  19. widget 顯⽰的畫⾯改成⽇期  struct SumikkoGurashiWidgetEntryView : View { var entry: Provider.Entry

    var body: some View { Text(entry.date, style: .date) } }
  20. 定義 getSnapshot func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) ->

    ()) { let birthday = DateComponents(calendar: .current, year: 2021, month: 2, day: 7).date! let entry = SimpleEntry(date: birthday) completion(entry) } 選擇 widget 時顯⽰的⽇期是重要的 2/7
  21. TimelineProvider 的 getTimeline 產⽣ Timeline,加入 widget 顯⽰的資料 func getTimeline(in context:

    Context, completion: @escaping (Timeline<Entry>) -> ()) { var entries: [SimpleEntry] = [] // Generate a timeline consisting of five entries an hour apart, starting from the current date. let currentDate = Date() for hourOffset in 0 ..< 5 { let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)! let entry = SimpleEntry(date: entryDate) entries.append(entry) } let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) } 每 1 ⼩時更新⼀次,產⽣ 5 筆資料 加入 widget 時,呼叫 getTimeline
  22. TimelineProvider 的 getTimeline 每 10 秒更新⼀次,產⽣ 100 筆資料 func getTimeline(in

    context: Context, completion: @escaping (Timeline<Entry>) -> ()) { var entries: [SimpleEntry] = [] // Generate a timeline consisting of five entries an hour apart, starting from the current date. let currentDate = Date() for offset in 0 ..< 100 { let entryDate = Calendar.current.date(byAdding: .second, value: 10 * offset, to: currentDate)! let entry = SimpleEntry(date: entryDate) entries.append(entry) } let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) }
  23. policy : 決定何時產⽣新的 timeline, 加入 widget 要顯⽰的資料 atEnd: 當 widget 顯⽰完最後⼀筆資料時,再呼叫

    getTimeline 下⼀次呼叫 getTimeline 的時間是 iOS 決定,因此不會準時觸發 let timeline = Timeline(entries: entries, policy: .atEnd)
  24. widget 顯⽰的畫⾯改成幾點幾分幾秒  struct SumikkoGurashiWidgetEntryView : View { var entry: Provider.Entry

    var body: some View { let formatter = DateFormatter() formatter.dateFormat = "HH:mm:ss" return Text(entry.date, formatter: formatter) } }
  25. 下⼀次呼叫 getTimeline 的時間是 iOS 決定,因此不會知道何時觸發 經過 40 秒 每隔 10

    秒更新 最後⼀筆資料 新的 timeline
  26. policy : after(_ date:) func getTimeline(in context: Context, completion: @escaping

    (Timeline<Entry>) -> ()) { var entries: [SimpleEntry] = [] let currentDate = Date() let entry = SimpleEntry(date: currentDate) entries.append(entry) let timeline = Timeline(entries: entries, policy: .after(currentDate.addingTimeInterval(60))) completion(timeline) } 希望⼀分鐘後呼叫 getTimeline 更新資料 iOS 決定什時候呼叫,不會剛好⼀分鐘後
  27. widget 畫⾯⽀援的元件 https://cutt.ly/UjAJiX9 Widgets present read-only information and don’t support

    interactive elements such as scrolling elements or switches. WidgetKit omits interactive elements when rendering a widget’s content. 不⽀援可互動的元件,比⽅捲動元件,輸入⽂字等
  28. widget 背景顏⾊: 預設⿊⽩

  29. 設定 widget 背景顏⾊ struct DemoWidgetEntryView : View { var entry:

    Provider.Entry var body: some View { ZStack { Color("WidgetBackground") Text(entry.date, style: .time) } } }
  30. 設定 widget 背景顏⾊

  31. 隨時間⾃動更新的 widget https://cutt.ly/2jAPO4O struct SumikkoGurashiWidgetEntryView : View { var entry:

    Provider.Entry var body: some View { let components = DateComponents(minute: 1, second: 10) let futureDate = Calendar.current.date(byAdding: components, to: Date())! return VStack(spacing: 5) { HStack { Text("relative") Text(futureDate, style: .relative) } HStack { Text("offset") Text(futureDate, style: .offset) } HStack { Text("timer") Text(futureDate, style: .timer) } } .padding() } }
  32. 隨時間⾃動更新的 widget

  33. 點擊 widget 啟動 App 可傳送 url 資訊給 App 透過 Link

    或 widgetURL Demo 點選 widget 顯⽰⿁滅圖片
  34. App 顯⽰ url 的圖片 import SwiftUI import Kingfisher struct ContentView:

    View { @State private var url: URL? var body: some View { KFImage(url) .resizable() .scaledToFit() .onOpenURL { (url) in self.url = url } } } 點選 widget 觸發 onOpenURL,更新 url
  35. Widget 裡設定⿁滅⾓⾊ struct Role { let name: String let url:

    URL? } extension Role { static var data: [Role] { [ Role(name: "竈⾨炭治郎", url: URL(string: "https://img.ruten.com.tw/s1/a/9f/e1/21915751087073_823.jpg")), Role(name: "竈⾨禰⾖⼦", url: URL(string: "https://hinetcdn.waca.ec/uploads/shops/7874/products/b3/ b338a565e28eeac80388e3bdcb3905b6.jpg")), Role(name: "富岡義勇", url: URL(string: "https://cf.shopee.tw/file/bf18021da08168d5883ad9ada8e511dc")) ] } }
  36. Widget 隨機顯⽰⾓⾊名字 struct Provider: TimelineProvider { func placeholder(in context: Context)

    -> SimpleEntry { SimpleEntry(date: Date(), role: Role.data[0]) } func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) { let entry = SimpleEntry(date: Date(), role: Role.data[0]) completion(entry) } func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) { var entries: [SimpleEntry] = [] // Generate a timeline consisting of five entries an hour apart, starting from the current date. let currentDate = Date() let roles = Role.data for offset in 0 ..< 100 { let entryDate = Calendar.current.date(byAdding: .second, value: 10 * offset, to: currentDate)! let entry = SimpleEntry(date: entryDate, role: roles.randomElement()!) entries.append(entry) } let timeline = Timeline(entries: entries, policy: .atEnd) completion(timeline) } }
  37. 透過 widgetURL 設定網址 struct SumikkoGurashiWidgetEntryView : View { var entry:

    Provider.Entry var body: some View { Text(entry.role.name) .widgetURL(entry.role.url) } }
  38. 透過 Link 設定多個網址 struct SumikkoGurashiWidgetEntryView : View { var entry:

    Provider.Entry var body: some View { VStack(spacing: 5) { ForEach(Role.data, id: \.name) { (role) in Link(role.name, destination: role.url!) .padding(.horizontal, 10) .background(Capsule().foregroundColor(.yellow)) } } } } Link 只能作⽤在 medium & large size
  39. 透過 Link 設定多個網址

  40. print WidgetKit extension 的 debug 訊息 https://cutt.ly/mjAHM8E

  41. 從 App 更新 widget import SwiftUI import WidgetKit struct ContentView:

    View { var body: some View { Button(action: { WidgetCenter.shared.reloadAllTimelines() }) { Text("Update Widget") } } } 觸發 widget 的 getTimeline
  42. 利⽤ App Group 讓 widget 跟 iOS App 共享資料 https://cutt.ly/MjQ5NSY

  43. Widget 顯⽰網路抓取的資料 struct SimpleEntry: TimelineEntry { let date: Date let

    uiImage: UIImage? } Widget 顯⽰的資料必須直接放在 TimelineEntry 裡,不能 widget 顯⽰時再抓取
  44. 在 getTimeline 抓資料,抓到後加入 Timeline func getTimeline(in context: Context, completion: @escaping

    (Timeline<Entry>) -> ()) { var entries: [SimpleEntry] = [] let number = Int.random(in: 0...1000) URLSession.shared.dataTask(with: URL(string: "https://picsum.photos/id/\ (number)/200/300")!) { (data, response, error) in let currentDate = Date() if let data = data, let uiImage = UIImage(data: data){ let entry = SimpleEntry(date: currentDate, uiImage: uiImage) entries.append(entry) } let timeline = Timeline(entries: entries, policy: .after(currentDate.addingTimeInterval(60))) completion(timeline) }.resume() }
  45. Widget 顯⽰網路抓取的圖片 struct DemoWidgetEntryView : View { var entry: Provider.Entry

    var body: some View { VStack { Text(entry.date, style: .time) entry.uiImage.map { Image(uiImage: $0) .resizable() .frame(width: 50, height: 50) } } } }
  46. Widget 下載 JSON 例⼦ https://www.youtube.com/watch?v=vMciaDT1Tos

  47. TimelineProvider 的 placeholder func placeholder(in context: Context) -> SimpleEntry {

    SimpleEntry(date: Date(), role: Role(name: "彼得潘", url: nil)) } 資料顯⽰前出現⼀兩秒的畫⾯ 移除 redaction 效果(遮蔽⽂字) struct SumikkoGurashiWidgetEntryView : View { var entry: Provider.Entry var body: some View { Text(entry.role.name) }
  48. 移除 redaction 效果(遮蔽⽂字) struct SumikkoGurashiWidgetEntryView : View { var entry:

    Provider.Entry var body: some View { Text(entry.role.name) .unredacted() } } func placeholder(in context: Context) -> SimpleEntry { SimpleEntry(date: Date(), role: Role(name: "彼得潘", url: nil)) }
  49. 依據不同的 size 設定 widget struct SumikkoGurashiWidgetEntryView : View { var

    entry: Provider.Entry @Environment(\.widgetFamily) var widgetFamily let names = [ "富岡義勇", "禰⾖⼦", "善逸", "伊之助" ] var body: some View { switch widgetFamily { case .systemSmall: Image(names[0]) .resizable() .scaledToFit() case .systemMedium: HStack { Image(names[0]) .resizable() .scaledToFit() Text(names[0]) } case .systemLarge: let columns = [ GridItem(), GridItem() ] LazyVGrid(columns: columns, content: { ForEach(names, id: \.self) { (name) in Image(name) .resizable() .scaledToFit() } }) default: Text("") } } }
  50. 依據不同的 size 設定 widget

  51. http://apppeterpan.strikingly.com 彼得潘簡介