iOS桌面小插件 Widget Extension
- 2022 年 3 月 6 日
- 筆記
- iOS, Objective C
iOS桌面小插件 Widget Extension
- 這個插件時iOS14以後才出現的,基於SwiftUI
- 舊項目新建時可能一堆錯誤,其中一個時要把插件target 開發sdk版本設置為14.0以上
新建target
- File – Target – Widget Extension
項目結構
- @main 這裡是主入口,這裡可以設置小組件的 Provider以及 WidgetEntryView,以及長按後彈出框的 APP 信息設置。
- Provider:控制器,這裡可以用來做小組件的刷新操作
- SimpleEntry: 這個是數據模型,Provider 里如果想更新數據到 WidgetEntryView,必須通過 SimpleEntry 來實現,當然命名隨意了,但是這個必須繼承 TimelineEntry。同時也可以新增參數,變量什麼的,用來傳遞自己需要的數據類型。
- WidgetEntryView: 這就是主視圖了,在這裡自定義頁面用來顯示在手機桌面。
import WidgetKit
import SwiftUI
import Intents
// 控制器,類似Controller,這裡可以用來做小組件的刷新操作
struct Provider: IntentTimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), configuration: ConfigurationIntent())
}
func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), configuration: configuration)
completion(entry)
}
func getTimeline(for configuration: ConfigurationIntent, 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, configuration: configuration)
entries.append(entry)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
// 數據模型,數據顯示在View上必須經過這裡
struct SimpleEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationIntent
}
// View,小組件的界面
struct WidgetExtensionEntryView : View {
var entry: Provider.Entry
var body: some View {
Text(entry.date, style: .time)
}
}
// 程序入口,初始化相關信息,如Provider,View等
@main
struct WidgetExtension: Widget {
let kind: String = "WidgetExtension"
var body: some WidgetConfiguration {
IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
WidgetExtensionEntryView(entry: entry)
}
.configurationDisplayName("小組件")
.description("This is an 測試一下 widget.")
}
}
// 自定義樣式
struct WidgetExtension_Previews: PreviewProvider {
static var previews: some View {
// 設置小組件尺寸 systemSmall systemMedium systemLarge
WidgetExtensionEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent()))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}
自定義UI
自定義小組件尺寸
// 自定義樣式
struct WidgetExtension_Previews: PreviewProvider {
static var previews: some View {
// 設置小組件尺寸 systemSmall systemMedium systemLarge
WidgetExtensionEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent()))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}
HStack、VStack、ZStack
- HStack、VStack相當於UIStackView,H是水平方向,V是豎直方向。ZStack可以理解為相對於屏幕里外方向,也就是相當於以前superView和subView的方式。
// View,小組件的界面
struct WidgetExtensionEntryView : View {
var entry: Provider.Entry
var body: some View {
// 深度布局,屏幕深度
ZStack(alignment: .center, content: {
// 背景圖
Image("2").resizable().aspectRatio(contentMode: .fit)
// 水平
HStack(alignment: .center, spacing: 5, content: {
// 左側圖
Image("1").frame(width: 80, height: 80, alignment: .center).aspectRatio(contentMode: .fit).cornerRadius(10.0)
// 垂直
VStack(alignment: .center, spacing: 5, content: {
// 右側文字
Text("小組件1").foregroundColor(.blue)
Text("小組件2").foregroundColor(.blue).lineLimit(2)
})
})
})
}
}
傳遞數據
- 通過widgetURL 和Link
- 在主應用添加 URL Types
// View,小組件的界面
struct WidgetExtensionEntryView : View {
var entry: Provider.Entry
var body: some View {
// 深度布局,屏幕深度
ZStack(alignment: .center, content: {
// 背景圖
Image("2").resizable().aspectRatio(contentMode: .fit)
// 水平
HStack(alignment: .center, spacing: 5, content: {
// 左側圖
Image("1").frame(width: 80, height: 80, alignment: .center).aspectRatio(contentMode: .fit).cornerRadius(10.0)
// 垂直
VStack(alignment: .center, spacing: 5, content: {
// 右側文字
Text("小組件1").foregroundColor(.blue)
Text("小組件2").foregroundColor(.blue).lineLimit(2)
})
})
}).widgetURL(URL(string: "widgetExtensionDemo://test1"))
}
}
-
接受數據
-
只能用SceneDelegate來接受數據,AppDelegate不行。
-
SceneDelegate中相應事件
- (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts{
NSLog(@"%s",__FUNCTION__);
UIOpenURLContext * context = URLContexts.allObjects.firstObject;
NSLog(@"%@", context.URL);
}
適配不同尺寸小組件
// View,小組件的界面
struct WidgetExtensionEntryView : View {
@Environment(\.widgetFamily) var family:WidgetFamily
var entry: Provider.Entry
var body: some View {
switch family {
case .systemSmall:
// 深度布局,屏幕深度
ZStack(alignment: .center, content: {
// 背景圖
Image("2").resizable().aspectRatio(contentMode: .fill)
// 水平
HStack(alignment: .center, spacing: 5, content: {
// 左側圖
Image("1").frame(width: 80, height: 80, alignment: .center).aspectRatio(contentMode: .fit).cornerRadius(10.0)
// 垂直
VStack(alignment: .center, spacing: 5, content: {
// 右側文字
Text("小組件1").foregroundColor(.blue)
Text("小組件2").foregroundColor(.blue).lineLimit(2)
})
})
}).widgetURL(URL(string: "widgetExtensionDemo://test1"))
case .systemMedium:
// 深度布局,屏幕深度
ZStack(alignment: .center, content: {
// 背景圖
Image("2").resizable().aspectRatio(contentMode: .fill)
// 水平
HStack(alignment: .top, spacing: 5, content: {
// 左側圖
Image("1").resizable().aspectRatio(contentMode: .fit).frame(width: 200, height: 80, alignment: .leading).cornerRadius(10.0).foregroundColor(.blue)
// 垂直
VStack(alignment: .trailing, spacing: 5, content: {
// 右側文字
Text("zh組件1").foregroundColor(.blue)
Text("小組件2").foregroundColor(.blue).lineLimit(2)
}).foregroundColor(.gray)
})
}).widgetURL(URL(string: "widgetExtensionDemo://test2"))
case .systemLarge:
// 深度布局,屏幕深度
ZStack(alignment: .center, content: {
// 背景圖
Image("2").resizable().aspectRatio(contentMode: .fill)
// 水平
HStack(alignment: .center, spacing: 5, content: {
// 左側圖
Image("1").aspectRatio(contentMode: .fit).cornerRadius(10.0).frame(width: 200, height: 100, alignment: .leading)
// 垂直
VStack(alignment: .center, spacing: 5, content: {
// 右側文字
Text("小組件1").foregroundColor(.blue)
Text("小組件2").foregroundColor(.blue).lineLimit(2)
})
}).foregroundColor(.blue)
}).widgetURL(URL(string: "widgetExtensionDemo://test3"))
default:
// 深度布局,屏幕深度
ZStack(alignment: .center, content: {
// 背景圖
Image("2").resizable().aspectRatio(contentMode: .fit)
// 水平
HStack(alignment: .center, spacing: 5, content: {
// 左側圖
Image("1").frame(width: 80, height: 80, alignment: .center).aspectRatio(contentMode: .fit).cornerRadius(10.0)
// 垂直
VStack(alignment: .center, spacing: 5, content: {
// 右側文字
Text("小組件1").foregroundColor(.blue)
Text("小組件2").foregroundColor(.blue).lineLimit(2)
})
})
}).widgetURL(URL(string: "widgetExtensionDemo://test1"))
}
}
}
更多小組件創建
- 重寫@main入口
// 更多小組件
@main
struct Widgets:WidgetBundle {
init() {
}
@WidgetBundleBuilder
var body: some Widget{ // 最多創建5次,也就是15個小組件
WidgetExtension()
CustomWidget()
CustomWidget()
CustomWidget()
CustomWidget()
}
}
struct CustomWidget:Widget {
var kind:String="自定義組件"
var body: some WidgetConfiguration{
IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
CustomEntryView(entry:entry)
}
.configurationDisplayName("自定義更多組件")
.description("ios14自定義更多小組件")
}
}
// 自定義Ui
struct CustomEntryView:View {
@Environment(\.widgetFamily) var family:WidgetFamily
var entry: Provider.Entry
@ViewBuilder
var body: some View {
switch family {
case .systemSmall:
// 深度布局,屏幕深度
ZStack(alignment: .center, content: {
// 背景圖
Image("2").resizable().aspectRatio(contentMode: .fill)
// 水平
HStack(alignment: .center, spacing: 5, content: {
// 左側圖
Image("1").frame(width: 80, height: 80, alignment: .center).aspectRatio(contentMode: .fit).cornerRadius(10.0)
// 垂直
VStack(alignment: .center, spacing: 5, content: {
// 右側文字
Text("小組件1").foregroundColor(.blue)
Text("小組件2").foregroundColor(.blue).lineLimit(2)
})
})
}).widgetURL(URL(string: "widgetExtensionDemo://test1"))
case .systemMedium:
// 深度布局,屏幕深度
ZStack(alignment: .center, content: {
// 背景圖
Image("2").resizable().aspectRatio(contentMode: .fill)
// 水平
HStack(alignment: .top, spacing: 5, content: {
// 左側圖
Image("1").resizable().aspectRatio(contentMode: .fit).frame(width: 200, height: 80, alignment: .leading).cornerRadius(10.0).foregroundColor(.blue)
// 垂直
VStack(alignment: .trailing, spacing: 5, content: {
// 右側文字
Text("zh組件1").foregroundColor(.blue)
Text("小組件2").foregroundColor(.blue).lineLimit(2)
}).foregroundColor(.gray)
})
}).widgetURL(URL(string: "widgetExtensionDemo://test2"))
case .systemLarge:
// 深度布局,屏幕深度
ZStack(alignment: .center, content: {
// 背景圖
Image("2").resizable().aspectRatio(contentMode: .fill)
// 水平
HStack(alignment: .center, spacing: 5, content: {
// 左側圖
Image("1").aspectRatio(contentMode: .fit).cornerRadius(10.0).frame(width: 200, height: 100, alignment: .leading)
// 垂直
VStack(alignment: .center, spacing: 5, content: {
// 右側文字
Text("小組件1").foregroundColor(.blue)
Text("小組件2").foregroundColor(.blue).lineLimit(2)
})
}).foregroundColor(.blue)
}).widgetURL(URL(string: "widgetExtensionDemo://test3"))
default:
// 深度布局,屏幕深度
ZStack(alignment: .center, content: {
// 背景圖
Image("2").resizable().aspectRatio(contentMode: .fit)
// 水平
HStack(alignment: .center, spacing: 5, content: {
// 左側圖
Image("1").frame(width: 80, height: 80, alignment: .center).aspectRatio(contentMode: .fit).cornerRadius(10.0)
// 垂直
VStack(alignment: .center, spacing: 5, content: {
// 右側文字
Text("小組件1").foregroundColor(.blue)
Text("小組件2").foregroundColor(.blue).lineLimit(2)
})
})
}).widgetURL(URL(string: "widgetExtensionDemo://test1"))
}
}
}