iOS小组件

基本知识

时间轴

小组件通过AppIntentTimelineProvider进行 UI 刷新

struct Provider: AppIntentTimelineProvider {
        func placeholder(in context: Context) -> SimpleEntry {
            // 添加占位的(选择添加的时候使用)
            // todo
        }
        func snapshot(for configuration: WidgetCfgIntent, in context: Context) async -> SimpleEntry {
            // 添加预览的时候会调用, 建议这里进行和timeline方法一样的数据处理。
            // todo
        }
        
        func timeline(for configuration: WidgetCfgIntent, in context: Context) async -> Timeline<SimpleEntry> {
          // 返回每个时间点的数据。
            var entries: [SimpleEntry] = []
           // 按需添加自己的时间片段
           // entries.append(entry)
            let nextTimeMin = 20
            let nextUpDate = Calendar.current.date(byAdding: .minute, value:nextTimeMin, to: .now) ?? Date(timeIntervalSince1970: Date().timeIntervalSince1970 + Double(nextTimeMin*60)) // 16-50分钟刷新一次, 不能设置时间太小,太小会被系统忽略
            return Timeline(entries: entries, policy: .after(nextUpDate))
        }
        // 推荐配置。
        func recommendations() -> [AppIntentRecommendation<WidgetCfgIntent>] {
           // todo
        }
    }

数据共享

与其他扩展一样,小组件可以通过Group 的UserDefault 共享数据。 还可以通过 SwiftData 共享数据。

交互

widgetURL:所有区域

Link: 不同元素

具有交互性的 Tog 或 Button(iOS 16):需要基于 AppIntent (与系统通用的 Intent 共用)参考https://developer.apple.com/documentation/widgetkit/adding-interactivity-to-widgets-and-live-activities

AppIntet 理解

Intent: 提供的一个小功能(通过入参、实现某个功能、返回什么结果),比如打开什么、记录什么

Entity:用来表示 App的内容,提供给 Intent 使用(对这个小功能的抽象数据)。

AppShortcut:用来包装 Intent,使之能被系统或者调用方发现。

APPIntent 如何返回数据


func perform() async throws -> some IntentResult & ReturnsValue<Int> {

// 如果需要返回其他类型,在 Int 处替换。let returnValue: Int = 1//在这里添加你要执行的代码return .result(value: returnValue)}



APPIntent 的参数通常用@Parameter标记, 支持基本常用值, 也可以继承AppEntityAppEnum–枚举进行自定义类型。

实现了 AppIntent 的功能默认自动提供给快捷指令使用。注意多语言和相关测试

小组件更新

主 APP 内:通过WidgetCenter刷新

// 通过 kind 刷新某个类型小组件
WidgetCenter.shared.reloadTimelines(ofKind: "com.mygame.gamestatus")
// 刷新所有小组件
WidgetCenter.shared.reloadAllTimelines()

系统刷新:

  • 通过时间轴刷新(15~50分钟一次,设置时间小了,系统也会忽略)
  • 执行交互型 Intent 后会触发刷新

内存限制:小组件最多支持30M, 如果超过会被系统 kill 。
如果小组件有图片数据,注意刷新时候不要过多持有,否则容易导致内存超限,小组件不能显示。

支持小组件类型

struct DemoWidget: Widget {
    var body: some WidgetConfiguration {
      // AppIntentConfiguration 为可以配置的小组件
        AppIntentConfiguration(kind: kind, intent: WidgetCfgIntent.self, provider: Provider()) { entry in
            SwiftUIWidgetEntryView(entry: entry)
                .widgetBackground(Color.container) // 添加自定义背景色(扩展方法),注意小组件最底层始终有一个底色。 无法做到像 Apple Home 那样的半透明背景效果(据说是只有系统才能用的 API)
        }
        .configurationDisplayName("展示名字")
        .description("描述")
        .supportedFamilies(supportFamilies) // 支持小组件类型。
        .disableContentMarginsIfNeeded() // 忽略边距(扩展方法)
    }
    private var supportFamilies:[WidgetFamily] {
        return [.systemSmall, .systemMedium, .systemLarge]
    }
    private var kind: String {
        return "com.xxxx.Widget"
    }
}

小组件配置

通过继承WidgetConfigurationIntent 实现。探索 App Intents 的功能更新 – 小专栏

一组一组配置

相关接口:https://developer.apple.com/documentation/appintents/intentitemcollection

struct DeviceDefaultProvider:DynamicOptionsProvider
// 注意这里需要返回 IntentItemCollection, 即分组显示
func results() async throws -> IntentItemCollection<DeviceEntity>  {
//        return ItemCollection(promptLabel: "----collection") {
//            ItemSection("title1", subtitle: "subtitle1", image: DisplayRepresentation.Image(named: "pic_xxxx")) {
//                DeviceEntity(deviceID: "111", name: "device111")
//                DeviceEntity(deviceID: "112", name: "device112")
//            }
//            ItemSection("title2", subtitle: "subtitle2", image: DisplayRepresentation.Image(named: "pic_em0xxxx")) {
//                DeviceEntity(deviceID: "211", name: "device211")
//                DeviceEntity(deviceID: "212", name: "device212")
//            }
//        }
}

效果图

IMG_6277

小组件扩展

import WidgetKit
import SwiftUI

extension View {
    /// 统一设置备件
    @ViewBuilder
    func widgetBackground(_ backgroundView: some View) -> some View {
        if Bundle.main.bundlePath.hasSuffix(".appex"){ // 小组件才生效
            if #available(iOS 17.0, *) {
                containerBackground(for: .widget) {
                    backgroundView
                }
            } else {
                background(backgroundView)
            }
        } else {
            background(backgroundView)
        }
    }
}


extension WidgetConfiguration {
    func disableContentMarginsIfNeeded() -> some WidgetConfiguration {
        if #available(iOSApplicationExtension 17.0, *) {
            // 禁用边距
            return self.contentMarginsDisabled()
        } else {
            return self
        }
    }
}

小组件刷新失败

entities 查询返回数据空

背景:我在主 APP 修改了小组件显示数据源, 同时调用了WidgetCenter.shared.reloadAllTimelines刷新,但是回到小组件,数据依然没有变化,通过调试,控制台打印如下:

Error getting AppIntent from LNAction: AppIntent has missing parameter value for 'xxxx'. You may need to set a default value in the initializer of your @Parameter, or using the default method on your Query.
No AppIntent in timeline(for:with:)

上面这种情况属于 APP 内主动刷新失效, 但是系统的时间轴刷新有效。

通过反复测试发现,在桌面小组件自定义配置后才发生。

我的配置参数是基于 AppEntity 实现的自定义类型。

发现每次在调用 EntityQuery 的entities(for)方法,内部返回空数组,都会打印上面的错误。

struct DeviceEntityQuery: EntityQuery, Sendable {
    // 通过 ID 查询的会调用, (比如通过配置匹配,提供数据源等)
    func entities(for identifiers: [DeviceEntity.ID]) async throws -> [DeviceEntity] {
        //具体逻辑:通过 ID 过滤数据。 如果没匹配到,就返回空。
    }
}

APP主动刷新小组件 -> 通过自定义配置在 EntityQuery 方法中查询 -> 查询到,触发刷新小组件的生命周期方法,(没查询,比如返回空数组,则不会刷新小组件生命周期方法)

结论:在entities(for)的返回结果时,不要返回空数组,否则可能无法刷新。

解决方法:如果没有匹配到,不返回空数组,构建对应的 Entity 返回。

其他问题

验证策约

在 AppIntent 协议定义的属性中,包含

static var authenticationPolicy: IntentAuthenticationPolicy { get } //默认值为alwaysAllowed

该属性可以配置 Intent 运行验证策略
值分别为

/// 一直允许允许,包括锁屏情况下
case alwaysAllowed
/// 需要验证、通过类似 Apple Watch 验证都可以执行
case requiresAuthentication
/// 需要本地验证,即一定需要手机解锁,有Watch 在也需要解锁。
case requiresLocalDeviceAuthentication

注意这个配置是对Intent有效,设置alwaysAllowed后,实际测试发现提供给快捷指令使用时不需要解锁,小组件运行改 Intent 还是需要解锁。

widgetURL配置

现象:通过 WidgtURL装饰器配置的 URL 跳转异常。

原因:我在最底层配置得了 WidgetURL, 在子视图也配置了 Widget,导致跳转异常. 根据官网文档解释,如果有多层View 使用了 WidgetURL, 状态和跳转将是不可预料的(在 iOS18.0上,多数情况下都响应最底层的 WidgetURL)

解决方案:通过 Link 或者 Button(AppIntent 交互)处理连接。

Link(destination: URL(string: "xxxxx" ) {
// your view           
}

Summery 怎么多语言化

使用新的多语言文件 xcstrings

使用占位模式

Get the daylight duration on ${date} in ${location}

使用示例

Summary("Switch \(\.$device) \(\.$isOn)")// 可选, table: "Localizable1")

注意:变量名要一致

Next

将 App 控制扩展到系统级别: 将功能扩展到系统控制,通过类型小组件 AppIntent 的方式。
小组件适配屏幕主题色:iOS18后新增用户可以自定义屏幕主题色,会导致 APP 图标和小组件颜色跟随变化,如果小组件不兼容,会导致显示异常。

资料参考

Apple 官方指导:https://developer.apple.com/cn/documentation/widgetkit/

iOS 小组件系列教程:https://juejin.cn/post/7297513663435210771?searchId=2024082914404396F94C65488FE5528F7E

iOS 小组件开发全面总结 – 掘金

WidgetExamples – github

SwiftUI 控件 Demo – github

SwiftUI 细节学习参考 --肘子的记事本: 各种SwiftUI 控件、布局理解。

给 Widget 添加动画–一个特别的方法:利用 Font 或者 gif 图片分解来实现动画。

iOS 小组件动态配置是指在 iOS 操作系统中,用户可以根据个人喜好和需求,自由地配置小组件的功能和外观样式。iOS 14 及更高版本中推出了小组件功能,给用户带来了更加个性化的体验。 小组件动态配置的实现主要通过以下几个步骤: 1. 选择和添加小组件:用户长按主屏幕空白处,进入编辑状态后,可以选择添加新的小组件iOS 提供了多种内置样式的小组件,用户可以根据自己的需求选择合适的类型。 2. 配置小组件的大小和位置:用户可以自由调整小组件的大小和位置,以适应屏幕的布局和自身需求。通过拖拽、缩放和旋转,用户可以更加精确地控制小组件的显示效果。 3. 配置小组件的主题和内容:iOS 支持对小组件的外观进行个性化定制。用户可以选择不同的主题和颜色,以及不同的字体和背景风格,来创建独一无二的小组件样式。同时,还可以自定义小组件展示的内容,如天气、日历、媒体播放器等。 4. 配置小组件的交互功能:小组件也可以具备一定的交互性,用户可以根据需要配置小组件的可点击区域和相应的响应动作。比如,在音乐播放器小组件上,用户可以设置点击按钮来控制歌曲的播放、暂停和切换等功能。 总的来说,iOS 小组件动态配置给用户提供了更多个性化的选择和定制空间,让用户可以根据自己的喜好和习惯,定制出最适合自己的手机主屏幕布局。这种灵活的配置方式,使得用户可以更加高效地获取所需的信息和功能,提升了使用体验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值