可配置小组件
注意:
- AppIntentConfiguration 及 WidgetConfigurationIntent 需要 iOS17 及以后版本
- iOS17 之前需要使用 SiriKit 意图定义文件和 Intents Extension
在创建自定义iOS小组件时,如果你希望用户在添加小组件时能够自定义参数,你需要使用 App Intents
框架(iOS16 引入)来实现动态的配置。
这通常是在 WidgetConfigurationIntent
(iOS17引入)的基础上,定义自定义的参数类型和选项,允许用户在配置小组件时提供自定义数据。
比如日历组件,当你长按小组件时,可以选择编辑小组件按钮:
添加配置意图
iOS17 系统及以上版本,可以通过实现 WidgetConfigurationIntent
协议来完成小组件自定义配置,这种方式是纯代码实现,不需要添加意图定义文件,也不需要添加意图扩展。
1.创建项目时配置
如果需要添加类似的配置,需要在创建小组件的时候把 Include Configuration Intent 勾选上。
创建完后会多出一个 AppIntent 文件,这就是用于配置的文件:
2.手动添加
创建一个 AppIntent.swift 文件,按照如下改写
import WidgetKit
import AppIntents
struct ConfigurationAppIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Configuration"
static var description = IntentDescription("This is an example widget.")
@Parameter(title: "Favorite Emoji", default: "😃")
var favoriteEmoji: String
}
import WidgetKit
import SwiftUI
struct Provider: AppIntentTimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), configuration: ConfigurationAppIntent())
}
func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> SimpleEntry {
SimpleEntry(date: Date(), configuration: configuration)
}
func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline<SimpleEntry> {
var entries: [SimpleEntry] = []
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)
}
return Timeline(entries: entries, policy: .atEnd)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationAppIntent
}
struct MyWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack {
Text("Time:")
Text(entry.date, style: .time)
Text("Favorite Emoji:")
Text(entry.configuration.favoriteEmoji)
}
}
}
struct MyWidget: Widget {
let kind: String = "MyWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in
MyWidgetEntryView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
}
}
extension ConfigurationAppIntent {
fileprivate static var smiley: ConfigurationAppIntent {
let intent = ConfigurationAppIntent()
intent.favoriteEmoji = "😀"
return intent
}
fileprivate static var starEyes: ConfigurationAppIntent {
let intent = ConfigurationAppIntent()
intent.favoriteEmoji = "🤩"
return intent
}
}
#Preview(as: .systemSmall) {
MyWidget()
} timeline: {
SimpleEntry(date: .now, configuration: .smiley)
SimpleEntry(date: .now, configuration: .starEyes)
}
配置
1.文本类型
struct ConfigurationAppIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Configuration"
static var description = IntentDescription("This is an example widget.")
@Parameter(title: "Favorite Emoji", default: "😃")
var favoriteEmoji: String
}
2.枚举类型
enum TemperatureUnit: String, CaseIterable, AppEnum {
case celsius = "Celsius"
case fahrenheit = "Fahrenheit"
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Temperature Unit"
static var caseDisplayRepresentations: [TemperatureUnit : DisplayRepresentation] = [
.celsius: "Celsius (°C)",
.fahrenheit: "Fahrenheit (°F)"
]
}
enum WidgetBackgroundColor: String, CaseIterable, AppEnum {
case blue = "Blue"
case green = "Green"
case orange = "Orange"
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Background Color"
static var caseDisplayRepresentations: [WidgetBackgroundColor : DisplayRepresentation] = [
.blue: "Blue",
.green: "Green",
.orange: "Orange"
]
var color: Color {
switch self {
case .blue: return .blue
case .green: return .green
case .orange: return .orange
}
}
}
struct ConfigurationAppIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Configuration"
static var description = IntentDescription("This is an example widget.")
@Parameter(title: "Temperature Unit", default: TemperatureUnit.celsius)
var temperatureUnit: TemperatureUnit
@Parameter(title: "Background Color", default: WidgetBackgroundColor.blue)
var backgroundColor: WidgetBackgroundColor
}
3.Bool类型
struct ConfigurationAppIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Configuration"
static var description = IntentDescription("This is an example widget.")
@Parameter(title: "Show Humidity", default: false)
var showHumidity: Bool
}
4.AppEntity
struct WeatherLocation: AppEntity {
let id: String
let name: String
let latitude: Double
let longitude: Double
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Weather Location"
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "(name)")
}
static var defaultQuery = WeatherLocationQuery()
}
struct WeatherLocationQuery: EntityQuery {
func entities(for identifiers: [WeatherLocation.ID]) async throws -> [WeatherLocation] {
return identifiers.compactMap { id in
switch id {
case "london": return WeatherLocation(id: "london", name: "London", latitude: 51.5074, longitude: -0.1278)
case "new-york": return WeatherLocation(id: "new-york", name: "New York", latitude: 40.7128, longitude: -74.0060)
case "tokyo": return WeatherLocation(id: "tokyo", name: "Tokyo", latitude: 35.6762, longitude: 139.6503)
default: return nil
}
}
}
func suggestedEntities() async throws -> [WeatherLocation] {
return [
WeatherLocation(id: "london", name: "London", latitude: 51.5074, longitude: -0.1278),
WeatherLocation(id: "new-york", name: "New York", latitude: 40.7128, longitude: -74.0060),
WeatherLocation(id: "tokyo", name: "Tokyo", latitude: 35.6762, longitude: 139.6503)
]
}
}
struct ConfigurationAppIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Configuration"
static var description = IntentDescription("This is an example widget.")
@Parameter(title: "Location")
var location: WeatherLocation?
}
5.ParameterSummary(参数摘要)
struct ConfigurationAppIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Configuration"
static var description = IntentDescription("This is an example widget.")
@Parameter(title: "Favorite Emoji", default: "😃")
var favoriteEmoji: String
@Parameter(title: "Temperature Unit", default: TemperatureUnit.celsius)
var temperatureUnit: TemperatureUnit
@Parameter(title: "Location")
var location: WeatherLocation?
static var parameterSummary: some ParameterSummary {
Summary("Show weather for (.$location) in (.$temperatureUnit)")
}
}
使用
import WidgetKit
import SwiftUI
struct Provider: AppIntentTimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), configuration: ConfigurationAppIntent())
}
func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> SimpleEntry {
SimpleEntry(date: Date(), configuration: configuration)
}
func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline<SimpleEntry> {
var entries: [SimpleEntry] = []
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)
}
return Timeline(entries: entries, policy: .atEnd)
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationAppIntent
}
struct MyWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack {
Text(entry.configuration.location?.name ?? "Select Location")
.font(.headline)
if let location = entry.configuration.location {
Text(formattedTemperature())
.font(.title2)
Text(formattedCoordinates(location))
.font(.caption)
} else {
Text("No location selected")
.font(.caption)
}
}
}
func formattedTemperature() -> String {
let temperature = 25.0
switch entry.configuration.temperatureUnit {
case .celsius:
return String(format: "%.1f°C", temperature)
case .fahrenheit:
let fahrenheit = temperature * 9/5 + 32
return String(format: "%.1f°F", fahrenheit)
}
}
func formattedCoordinates(_ location: WeatherLocation) -> String {
return String(format: "Lat: %.2f, Lon: %.2f", location.latitude, location.longitude)
}
}
struct MyWidget: Widget {
let kind: String = "MyWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in
MyWidgetEntryView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.supportedFamilies([.systemMedium])
}
}
extension ConfigurationAppIntent {
fileprivate static var smiley: ConfigurationAppIntent {
let intent = ConfigurationAppIntent()
intent.favoriteEmoji = "😀"
return intent
}
fileprivate static var starEyes: ConfigurationAppIntent {
let intent = ConfigurationAppIntent()
intent.favoriteEmoji = "🤩"
return intent
}
}
#Preview(as: .systemSmall) {
MyWidget()
} timeline: {
SimpleEntry(date: .now, configuration: .smiley)
SimpleEntry(date: .now, configuration: .starEyes)
}
效果
低版本兼容(iOS17以下)
iOS17系统以下版本,通过创建 SiriKit 意图定义文件和意图扩展程序,来实现小组件自定义配置。
1.依赖intentdefinition file
将 WeatherWidget.intentdefinition 依赖到项目(从原文下载,这里不支持上传),使用默认的或者配置新的
2.配置Intent
注意:
配置Intent时,名称不要带Intent, 系统在生成配置文件时会自动加上Intent,
如ConfigureWeather最终生成的为ConfigureWeatherIntent
配置新Intent: 选中文件 -> New Intent 创建一个新Intent
实现组件
import WidgetKit
import SwiftUI
extension View {
@ViewBuilder
func widgetBackground(_ backgroundView: some View) -> some View {
if #available(iOS 17.0, *) {
containerBackground(for: .widget) {
backgroundView
}
} else {
background(backgroundView)
}
}
}
extension WidgetConfiguration {
func adoptableWidgetContentMargin() -> some WidgetConfiguration {
if #available(iOSApplicationExtension 15.0, *) {
print("15.0 以上系统")
return contentMarginsDisabled()
} else {
print("15.0 以下系统")
return self
}
}
}
struct WeatherWidget: Widget {
let kind: String = "WeatherWidget"
var body: some WidgetConfiguration {
IntentConfiguration(kind: kind, intent: ConfigureWeatherIntent.self, provider: WeatherProvider()) { entry in
WeatherWidgetEntryView(entry: entry)
}
.configurationDisplayName("Weather Widget")
.description("Display weather information for a selected location.")
.supportedFamilies([.systemSmall, .systemMedium])
.adoptableWidgetContentMargin()
}
}
struct WeatherWidgetEntryView: View {
var entry: WeatherProvider.Entry
var body: some View {
VStack {
Text(entry.configuration.location ?? "Select Location")
.font(.headline)
Text(formattedTemperature(entry.configuration))
.font(.title2)
}
.widgetBackground(Color.white)
}
func formattedTemperature(_ configuration: ConfigureWeatherIntent) -> String {
return String(format: "%.1f°F", configuration.temperatureUnit ?? 0.0)
}
}
struct WeatherProvider: IntentTimelineProvider {
func placeholder(in context: Context) -> WeatherEntry {
WeatherEntry(date: Date(), configuration: ConfigureWeatherIntent())
}
func getSnapshot(for configuration: ConfigureWeatherIntent, in context: Context, completion: @escaping (WeatherEntry) -> ()) {
let entry = WeatherEntry(date: Date(), configuration: configuration)
completion(entry)
}
func getTimeline(for configuration: ConfigureWeatherIntent, in context: Context, completion: @escaping (Timeline<WeatherEntry>) -> ()) {
let entry = WeatherEntry(date: Date(), configuration: configuration)
let timeline = Timeline(entries: [entry], policy: .atEnd)
completion(timeline)
}
}
struct WeatherEntry: TimelineEntry {
let date: Date
let configuration: ConfigureWeatherIntent
}
struct WeatherWidget_Previews: PreviewProvider {
static var previews: some View {
WeatherWidgetEntryView(entry: WeatherEntry(date: Date(), configuration: ConfigureWeatherIntent()))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}
参考链接
developer.apple.com/cn/document…
本文同步自微信公众号 “程序员小溪” ,这里只是同步,想看及时消息请移步我的公众号,不定时更新我的学习经验。
阅读全文
下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.shuli.cc/?p=20948,转载请注明出处。
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.shuli.cc/?p=20948,转载请注明出处。
评论0