iOS14Widget简介

作者: Money_YC | 来源:发表于2020-09-13 18:44 被阅读0次

    简介

    新的 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

    相关文章

      网友评论

        本文标题:iOS14Widget简介

        本文链接:https://www.haomeiwen.com/subject/otcoektx.html