iOS14Widget简介

简介

新的 WidgetKit 框架和 SwiftUI 关于 widget 新的 api,创建的 widget能满足 iOS,iPadOS,和 macOS 平台,能放在 Home screen 的任意位置。而且还拥有智能堆栈 Smart Stack ,集成siri的智能化推荐能力,能根据你使用时间,位置等因素,来智能显示组件。

  • Widget使用SwiftUI视图显示其内容,可视化能力强
  • 一个App 可以提供多种样式的 Widget,可更具不同情况来显示不同类型的Widget,用户可以选择将其最关心的放置在最重要的位置上,以便最方便的获取信息。
  • Widget和之前的TodayWidget是一个独立运行的程序,需要在项目中进行App Groups的设置才能使其与主程序互通数据。
  • 目前存在的问题,widget不支持连续的实时更新,只能通过timeline来设置时间轴进行更新。
  • Widget 只有大中小3种尺寸(systemLarge、systemMedium、systemSmall)。
image

项目构建

添加Widget到原工程

打开你的 Xcode 工程, 并且选择 File > New > Target,在 Application Extension 中选择 Widget Extension,输入 Widget 的名字,如果 Widget 提供了用户可配置的属性,请选中“ Include Configuration Intent ”复选框。

image

在这里简单介绍下Widget的配置信息

小部件扩展模板提供了一个符合小部件协议的初始小部件实现。Widget的body属性决定了该Widget是否具有用户可配置的属性。

创建widget时,包含配置意图「Include Configuration Intent」复选框决定了Xcode使用哪种配置。当选择这个复选框时,Xcode使用将使用默认设置进行配置。

Configuration有两种配置可供选择:

1.StaticConfiguration: 对于一个没有用户可配置属性的Widget。「例如,显示一般市场信息的股票市场Widget,或显示趋势标题的新闻Widget。」

2.IntentConfiguration。对于一个具有用户可配置属性的Widget来说,你可以使用SiriKit自定义意图来定义属性。您使用 SiriKit 自定义意图来定义属性。「例如,一个天气Widget需要一个城市的邮政编码或邮政编码,或者一个包裹跟踪Widget需要一个跟踪号码。」

以下代码创建一个常规的StaticConfiguration,不可配置的状态的 Widget:

struct TestWidget: Widget {
    let kind: String = "TestWidget"
    var body: some WidgetConfiguration { 
     StaticConfiguration(kind: kind,provider: Provider()) { entry in
            TestWidget EntryView(entry: entry)
        }
        .configurationDisplayName("name")
        .description("des")
        .supportedFamilies([.systemMedium])
    }
}
  • kind:识别Widget的字符串。这是您选择的标识符,应描述Widget所代表的内容。

  • Provider:符合TimelineProvider的对象。一个符合TimelineProvider的对象,它能产生一个时间线,告诉WidgetKit何时渲染Widget。时间线包含一个你定义的自定义TimelineEntry类型。时间线条目标识了你希望WidgetKit更新Widget内容的日期。在自定义类型中包含你的Widget的视图需要渲染的属性。

  • Placeholder:一个 SwiftUI 视图,WidgetKit 用来在第一次渲染Widget。占位符是您的Widget的通用表示,没有特定的配置或数据。

  • Content Closure(内容闭合):一个包含SwiftUI视图的封闭。WidgetKit调用它来渲染Widget的内容,从提供者那里传递一个TimelineEntry参数。

  • Custom Intent(自定义配置)。一个定义用户可配置属性的自定义意图。

  • 包括显示名称(name)、描述(description)和Widget支持的系列(families)。

Provider 时间线配置(Provide Timeline Entries)

struct Provider: TimelineProvider {
  
    public typealias Entry = SimpleEntry
  
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date())
    }

    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date())
        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()
        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)
    }
}

Provider会生成由时间线条目组成的时间线,每个条目都指定了更新小组件内容的日期和时间。

  • placeholder :系统让你的 view 自动渲染一个占位图。
  • getSnapshot:快照预览为了在小部件库中显示小部件,在你添加widget时候,预览的样式。当部件还没有从服务器获取状态时,Provider通过显示一个空状态来实现getSnapshot方法。
  • getTimeline:在请求初始快照后,WidgetKit调用getTimeline(for:with:completion:)向提供者请求一个常规的时间线。时间线由一个或多个时间线条目和一个重载策略组成,告知WidgetKit何时请求后续时间线。

Widget的刷新策略

Timeline里面有三种方式:atEnd,after(date),never

  • atEnd: timeline 中最后一个 entry 显示后更新。timelines 方法会重新调用。
  • after(date): 指定日期,重新更新timeline。
  • never:系统不会自动更新,除非我们主动通过 Widget Center Api 来更新。

保持组件状态为最新(Keeping a Widget Up To Date)

Widget使用SwiftUI视图来显示它们的内容。WidgetKit在一个单独的过程中代表您渲染视图。因此,即使小组件在屏幕上,小组件扩展也不会持续活跃。尽管widget并不总是处于活动状态,但有几种方法可以使其内容保持最新。

为可预测的事件生成一个时间轴
定义widget时,实现一个自定义的TimelineProvider。WidgetKit从你的provider那里获取一个时间线,并使用它来跟踪何时更新widget。时间线是一个TimelineEntry对象的数组。时间线中的每个条目都有日期和时间,以及小组件显示其视图所需的附加信息。除了时间线条目,时间线还指定了一个刷新策略,该策略告诉WidgetKit何时请求新的时间线.

下面是苹果官网给的一个显示角色健康水平的游戏小部件的例子。
当健康水平低于100%时,角色以每小时25%的速度恢复。例如,当角色的健康水平为25%时,需要3小时才能完全恢复到100%。下图显示了WidgetKit如何从provider那里请求时间线,在时间线条目中指定的每个时间渲染小部件。

func getTimeline(for configuration: CharacterSelectionIntent, with context: Self.Context, completion: @escaping (Timeline<Self.Entry>) -> ()) {
    let selectedCharacter = characher(for: configuration)
    let endDate = selectedCharacter.fullHealthDate
    let oneHour: TimeInterval = 60 * 60
    var currentDate = Date()
    var entries: [SimpleEntry] = []

    while currentDate < endDate {
        let relevance = TimelineEntryRelevance(score: Float(selectedCharacter.healthLevel))
        let entry = SimpleEntry(date: currentDate, character: selectedCharacter, relevance: relevance)
        currentDate += oneHour
        entries.append(entry)
    }
    let timeline = Timeline(entries: entries, policy: .atEnd)
    completion(timeline)
}

当WidgetKit最初请求时间轴时,provider会创建一个有四个条目的时间轴。第一个条目代表当前的时间(Now),之后是每小时一次的三个条目。在刷新策略设置为默认的atEnd的情况下,WidgetKit会在时间线条目中的最后一个日期之后请求一个新的时间线。当时间线中的每个日期到达时,WidgetKit会调用小组件的内容闭包显示结果。在最后一个时间线条目过后,WidgetKit会重复这个过程,要求提供者提供一个新的时间线。由于角色的健康度已经达到了100%,提供者会以当前时间的单一条目和刷新策略设置为never来回应。在这种设置下,WidgetKit不会要求另一条时间线,直到应用程序使用WidgetCenter告诉WidgetKit请求新的时间线。

当时间线改变时通知WidgetKit
当某件事情影响到小组件的当前时间线时,或者说当你要刷新Widget的时候,App可以告诉WidgetKit请求新的时间线。App可以告诉WidgetKit重新加载时间线并更新小组件的内容。要重载特定类型的widget,使用WidgetCenter

WidgetCenter.shared.reloadTimelines(ofKind: "com.testWidget")

kind参数包含与用于创建widget的WidgetConfiguration的值相同的字符串。当然也只有一个widget的话也可以用reloadAllTimelines。

如果widget具有用户可配置的属性,那么通过使用WidgetCenter来验证是否存在具有适当设置的widget,从而避免不必要的重新加载。

WidgetCenter.shared.getCurrentConfigurations { result in
    guard case .success(let widgets) = result else { return }
    if let widget = widgets.first(
        where: { widget in
            let intent = widget.configuration as? SelectCharacterIntent
            return intent?.character == characterThatReceivedHealingPotion
        }
    ) {
        WidgetCenter.shared.reloadTimelines(ofKind: widget.kind)
    }
}

如果只有一个Widget,你可以使用WidgetCenter来重新加载所有widget的时间线。

WidgetCenter.shared.reloadAllTimelines()

Link WidgetUrl

从 Widget 跳转到 App 指定界面,只需要用 SwiftUI Link 的方式,View 的外层包上一个 Link,destination 是设定好的 url,就能实现跳转了。

Link(destination: URL(string: topJump.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)!) {
  HStack() {
    Spacer()
      .frame(width: Len(15))
    // titile
    Text(topDesc)
      .fontWeight(.regular)
      .font(.system(size: Len(14)))
      .foregroundColor(Color(red: 51/255, green: 51/255, blue: 51/255, opacity: 1.0))
    Image(triangleIcon)
      .padding(.trailing, 4.0)
      .frame(width: Len(8), height: Len(8))
    Spacer()
  }
}

widgetURL:在View上加一个widgetURL ,URL是设定好的 url,就能实现跳转了

HStack() {
  Spacer()
    .frame(width: UMEWLen(15))
  // titile
  Text(newBean.topDesc)
    .fontWeight(.regular)
    .font(.system(size: Len(14)))
    .foregroundColor(Color(red: 51/255, green: 51/255, blue: 51/255, opacity: 1.0))
  Image(triangleIcon)
    .padding(.trailing, 4.0)
    .frame(width: Len(8), height: Len(8))
  Spacer()
}.widgetURL(URL(string: topJump.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)!)

Widget bundles

有时候我们会有多个样式不同种类的 Widget,就需要用 @WidgetBundleBuilder 把多个 Widget 放在一起

@main
struct MainBundle: WidgetBundle {
    @WidgetBundleBuilder
    var body: some Widget {
        oneWidget()
        twoWidget()
        threeWidget()
    }
}

Intent

在你的Xcode项目中,选择File > New File并选择SiriKit Intent Definition File。点击 "下一步 "并在提示时保存文件。Xcode创建一个新的.intenttdefinition文件,并将其添加到项目中。
Xcode从意图定义「intent definition file」文件中生成代码。要在一个目标「target」中使用这些代码。
-将intent定义文件作为目标的一个成员。
-通过添加 intent 的类名到 target 属性的 Supported Intents 部分来指定要包含在 target 中的特定 intents。

稍后会上传demo和自己遇到的一些坑
[NewWidget]https://github.com/moneyYouCai/NewWidget-iOS14

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269