Slide 1

Slide 1 text

WidgetKit 彼得潘

Slide 2

Slide 2 text

相關教學 • WidgetKit framework • Creating a Widget Extension
 • Building Widgets Using WidgetKit and SwiftUI

Slide 3

Slide 3 text

建立 widget File > New > Target Widget Extension

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

使⽤者客製 widget : Edit Widget

Slide 7

Slide 7 text

widget 相關檔案 widget 有⾃⼰的 asset

Slide 8

Slide 8 text

將 widget 加到 iOS 桌⾯ 長按桌⾯後按 + 沒啟動過的 App ,不會出現在 widget 選單裡,所以 App ⾄少要啟動⼀次

Slide 9

Slide 9 text

三種 widget 的⼤⼩

Slide 10

Slide 10 text

widget 顯⽰在桌⾯

Slide 11

Slide 11 text

設定 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.") } }

Slide 12

Slide 12 text

設定 widget 時,顯⽰的名字跟說明 var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: Provider()) { entry in SumikkoGurashiWidgetEntryView(entry: entry) } .configurationDisplayName("⾓落⽣物") .description("住在⾓落的可愛⽣物") }

Slide 13

Slide 13 text

Provider • conforming to TimelineProvider 
 • 產⽣ timeline,控制 widget 顯⽰的資料跟更新的時間 • timeline 裡包含多個 timeline entry
 • timeline entry conforming to TimelineEntry,包含了資料和更新 widget 的時間

Slide 14

Slide 14 text

範例的 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 } }

Slide 15

Slide 15 text

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("住在⾓落的可愛⽣物") } }

Slide 16

Slide 16 text

widget 顯⽰的畫⾯  struct SumikkoGurashiWidgetEntryView : View { var entry: Provider.Entry var body: some View { Text(entry.date, style: .time) } } entry 是 widget 要顯⽰的資料 顯⽰時間

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

TimelineProvider 的 getSnapshot struct Provider: TimelineProvider { func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) { let entry = SimpleEntry(date: Date()) completion(entry) } 選擇 widget 時顯⽰的資料

Slide 19

Slide 19 text

widget 顯⽰的畫⾯改成⽇期  struct SumikkoGurashiWidgetEntryView : View { var entry: Provider.Entry var body: some View { Text(entry.date, style: .date) } }

Slide 20

Slide 20 text

定義 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

Slide 21

Slide 21 text

TimelineProvider 的 getTimeline 產⽣ Timeline,加入 widget 顯⽰的資料 func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) { 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

Slide 22

Slide 22 text

TimelineProvider 的 getTimeline 每 10 秒更新⼀次,產⽣ 100 筆資料 func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) { 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) }

Slide 23

Slide 23 text

policy : 決定何時產⽣新的 timeline, 加入 widget 要顯⽰的資料 atEnd: 當 widget 顯⽰完最後⼀筆資料時,再呼叫 getTimeline 下⼀次呼叫 getTimeline 的時間是 iOS 決定,因此不會準時觸發 let timeline = Timeline(entries: entries, policy: .atEnd)

Slide 24

Slide 24 text

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) } }

Slide 25

Slide 25 text

下⼀次呼叫 getTimeline 的時間是 iOS 決定,因此不會知道何時觸發 經過 40 秒 每隔 10 秒更新 最後⼀筆資料 新的 timeline

Slide 26

Slide 26 text

policy : after(_ date:) func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) { 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 決定什時候呼叫,不會剛好⼀分鐘後

Slide 27

Slide 27 text

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. 不⽀援可互動的元件,比⽅捲動元件,輸入⽂字等

Slide 28

Slide 28 text

widget 背景顏⾊: 預設⿊⽩

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

設定 widget 背景顏⾊

Slide 31

Slide 31 text

隨時間⾃動更新的 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() } }

Slide 32

Slide 32 text

隨時間⾃動更新的 widget

Slide 33

Slide 33 text

點擊 widget 啟動 App 可傳送 url 資訊給 App 透過 Link 或 widgetURL Demo 點選 widget 顯⽰⿁滅圖片

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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")) ] } }

Slide 36

Slide 36 text

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) -> ()) { 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) } }

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

透過 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

Slide 39

Slide 39 text

透過 Link 設定多個網址

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

從 App 更新 widget import SwiftUI import WidgetKit struct ContentView: View { var body: some View { Button(action: { WidgetCenter.shared.reloadAllTimelines() }) { Text("Update Widget") } } } 觸發 widget 的 getTimeline

Slide 42

Slide 42 text

利⽤ App Group 讓 widget 跟 iOS App 共享資料 https://cutt.ly/MjQ5NSY

Slide 43

Slide 43 text

Widget 顯⽰網路抓取的資料 struct SimpleEntry: TimelineEntry { let date: Date let uiImage: UIImage? } Widget 顯⽰的資料必須直接放在 TimelineEntry 裡,不能 widget 顯⽰時再抓取

Slide 44

Slide 44 text

在 getTimeline 抓資料,抓到後加入 Timeline func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) { 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() }

Slide 45

Slide 45 text

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) } } } }

Slide 46

Slide 46 text

Widget 下載 JSON 例⼦ https://www.youtube.com/watch?v=vMciaDT1Tos

Slide 47

Slide 47 text

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) }

Slide 48

Slide 48 text

移除 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)) }

Slide 49

Slide 49 text

依據不同的 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("") } } }

Slide 50

Slide 50 text

依據不同的 size 設定 widget

Slide 51

Slide 51 text

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