从实际问题看 SwiftUI 和 Widget 编程

Python实战社群

Java实战社群

长按识别下方二维码,按需求添加

扫码关注添加客服

进Python社群▲

扫码关注添加客服

进Java社群

从实际问题看 SwiftUI 和 Widget 编程_第1张图片

作者 | PJHubs 
来源 | http://pjhubs.com/,点击阅读原文查看作者更多文章

前言

iOS14 的 Widget 和 iOS14 之前的 Widget 已经完成了统一,之前老样式的 Widget 只能通过在老版本上进行查看,后续仅支持 iOS14 目前的 Widget。只能使用 SwiftUI 进行开发。

Widget 核心

  • 快速、关联性、个性化

  • 看一眼,就能够获取到重点内容

  • 内容才是最重要的

    • 相册 Widget 注意到的话,会发现展示的照片总是某个时刻下最棒的那一张,而不是最新的。

    • Widget 不是 mini app,应该看作是把 app 的内容在主屏幕的映射关系。官方给出的数据,一般我们会在一天的时间里进入主屏幕超过 90 次,并在主屏幕上短暂停留。

Widget 类型

Widget 有三个尺寸,但不强迫每个尺寸都实现,因为不是所有 app 都适合全尺寸 widget 展示,但推荐都实现(猜测就是要给用户最大自由度。

从实际问题看 SwiftUI 和 Widget 编程_第2张图片

  • 不能滚动和不能添加开关等其它系统控件。

  • 不支持视频和动图。

  • 小组件并不是在主屏幕上实时展示的。

    • 系统的时钟 Widget 的事实刷新 UI 是个系统级 app 才能拥有的对待。

    • SwiftUI 中对 Text 组件新增了可以实时展示时间的 API。

Text(Date(), style: .time)
  • 不要把小尺寸组件直接拉伸成中或者大尺寸小组件。

  • Small 尺寸组件只能接受单次点击。

  • 小组件内部按照 16pt 设定布局边距。

  • 小组件内部有圆形素材,应该使用 11pt 边距。

  • 小组件内部边界有圆角时要做得跟小组件本身的圆角半径同心。

    • 不同设备上的小组件本身圆角值不一样,不能直接写死圆角值。

    • SwiftUI 中提供了一个圆角容器。

  • 字体官方推荐使用 SF 系列,可自定义。

  • 不要放入 app logo 和 name。

Widget 如何成组?

控制允许用户选择的小组件类型

@main
struct PJWidget: Widget {
    private let kind: String = "PJWidget"

    public var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            PJWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("PJWidget")
        .description("2333")
        .supportedFamilies([.systemSmall, .systemMedium])
    }
}

在构建 entryView 时,根据当前选择的 widgetFamily 值来返回不同的样式。

struct PJWidgetEntryView: View {
    var entry: Provider.Entry
    @Environment(\.widgetFamily) var family

    @ViewBuilder
    var body: some View {
        switch family {
        case .systemSmall:
            PJAvatarView(entry.name)
        default:
            Text("PJHubs")
        }
    }
}

使用 Xcode Widget Extension 模版创建完后,会自动给默认 Widget 加上 @main 修饰符标记出当前 app Widget 的入口。

换句话说,此时我们进入到「Widget 搜索」,找到我们的 app,只会看到一个 Widget。

@main
struct SwiftUIWidgetDemo: Widget {
    let kind: String = "SwiftUIWidgetDemo"

    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
            SwiftUIWidgetDemoEntryView(entry: entry)
        }
        .configurationDisplayName("西瓜作者-数据日报")
        .description("你的数据精简日报")
        .supportedFamilies([.systemSmall])
    }
}

从实际问题看 SwiftUI 和 Widget 编程_第3张图片

@main
struct Widgets: WidgetBundle {
    @WidgetBundleBuilder
    var body: some Widget {
        SwiftUIWidgetDemo()
        SwiftUIWidgeMediumDemo()
        SwiftUIWidgeMediumFansDemo()
    }
}

注意:最多只允许塞入五个 Widget 样式。

Tips:What is @main?

说起 @main 大家可能会先想到之前的 @UIApplicationMain 这个修饰词,说到 @UIApplicationMain 可能又会想到 main.swift 或者 main.m 等等这些文件。总的来说,它们之间是存在某种神秘联系的!我们来写一个简单的 Swift 代码:

class demoSwift {
    class func test() {
        print("world!")
    }
}
demoSwift.test()

此时使用 swiftc demo.swift 后会得到一个可执行文件,看上去 Swift 的语法让新上手的同学令人感到愉快,不会再有类 C 系那种必须写一系列又臭又长的 main 函数初始化流程,但本质上真的不用写了吗?
我们来看看中间代码。

# 查看生成的中间代码
swiftc demo.swift -emit-sil
sil_stage canonical

import Builtin
import Swift
import SwiftShims

class demoSwift {
  class func test()
  @objc deinit
  init()
}

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer>>):
  %2 = metatype $@thick demoSwift.Type            // user: %4
  // function_ref static demoSwift.test()
  %3 = function_ref @$s4demo0A5SwiftC4testyyFZ : $@convention(method) (@thick demoSwift.Type) -> () // user: %4
  %4 = apply %3(%2) : $@convention(method) (@thick demoSwift.Type) -> ()
  %5 = integer_literal $Builtin.Int32, 0          // user: %6
  %6 = struct $Int32 (%5 : $Builtin.Int32)        // user: %7
  return %6 : $Int32                              // id: %7
} // end sil function 'main'

// ... 以下省略

可以看到,所谓的对新人友好都是假的,全都是编译期间 swiftc 做的自动化插入,自动给我们的方法插入了与之前类似的流程,如果我们需要多文件编译依赖,要有一个 main.swift 作为入口文件进行索引其他文件进行编译。

@UIApplicationMain 出现后,我们不再需要 main.swift 文件来做入口切割,可通过自定义类并加上该标记即可,这个好处在 Swift 5.3 中正式推广到语言层面,我们仅需使用 @main 即可标记出 Swift 文件的入口,不再是 Cocoa 特性,进而替代掉了 @UIApplicationMain。

Widget 用户如何配置数据?

Widget 提供了用户可配置数据源的方式,可以通过此类方式来绕过 Widget 成组后最大上限五个的限制。提供两种配置方式

  • StaticConfiguration。用户不可自定义数据源,参考头条 Widget。

从实际问题看 SwiftUI 和 Widget 编程_第4张图片

  • IntentCOnfiguration。允许用户选择配置,参考下图。

从实际问题看 SwiftUI 和 Widget 编程_第5张图片

其中 IntentConfiguration 可提供给用户有限的“自由”,自行选择对应 Widget 下需要展示的数据源。利用了基于 Intents.framework 框架实现,并可以直接复用 SiriKit 的功能来达到 Widget 的智能化(后文再叙)。

配置 IntentConfiguration 的步骤如下:

  • 创建对应的 IntentConfiguration 文件。

从实际问题看 SwiftUI 和 Widget 编程_第6张图片

  • 新增用户可配置的数据类型

从实际问题看 SwiftUI 和 Widget 编程_第7张图片

  • 配置新增数据类型相关信息

从实际问题看 SwiftUI 和 Widget 编程_第8张图片

  • 在对应类型的 Widget 中判断数据视图

struct SwiftUIWidgetDemoMediumEntryView : View {
    var entry: Provider.Entry

    @ViewBuilder
    var body: some View {
        switch entry.configuration.countType {
        case .money:
            // 此处需传入数据源
            MediumWidgetFansView()
        default:
            // 此处需传入数据源
            MediumWidgetView()
        }
    }
}

Tips: What is @ViewBuilder

从实际问题看 SwiftUI 和 Combine 编程 已说明,可以前往了解。

Widget 如何跳转到对应的页面?

预览视图

struct Provider: IntentTimelineProvider {
    // NOTE: 小组件占位视图,第一次添加或 loading 状态中的视图
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), count: 0, image: nil, configuration: ConfigurationIntent())
    }

    // NOTE: 第一次添加或小组件第一次被展示时调用
    func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date(), count: 0, image: nil, configuration: configuration)
        completion(entry)
    }
                
    func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline) -> ()) {
        // ...
        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

placeholder方法中返回 widget 在初始化 loading 过程中的占位 UI。

  • 每一个 widget 都必须提供。

  • 默认内容展示。

  • 没有任何用户相关数据。

  • 当系统无法显示你的小组件数据时会出现。

  • 无法被告知什么时候应该展示占位图。这是系统行为,系统需要的时候就会要求展示,例如用户更换了 widget 尺寸等。

从实际问题看 SwiftUI 和 Widget 编程_第9张图片

这个视图是系统行为,只要我们使用的是标准 SwiftUI 组件,会自动根据组件类型,如 Image、Text 结合我们自定义的颜色和背景来自动完成占位图的设置,如果我们不想要的系统自定义的话,也可以在方法中自行返回自定义的占位组件。

注意点:在构建 PlaceHolderView 时,Session 中所给的 isPlaceHoler 通过属性的方式去做已经不行了,得通过以下方式来进行(如果我们需要预览的话):

PJWidgetEntryView(entry: SimpleEntry(date: Date())).redacted(reason: .placeholder)

跳转

Widget 的目的非常简单,目前在 Widget 上所做的事情,全都是为了引导用户可以轻松的点击小组件和通过 deepLink 跳转到我们的 app 中。而从 Widget 跳转到 app 中针对不同类型的 Widget 有共有两种跳转方式。

  • .widgetURL

三种类型的小组件均可使用该方式进行跳转。

struct SmallWidgetView: View {
    var body: some View {
        VStack(alignment:.leading) {
        Text("PJHubs")
        }
        .widgetURL(URL(string: "urlschema://pjhubsWidgetURL"))
    }
}
  • Link

只有 Medium 和 Large 类型的小组件可以使用该方式进行跳转。

Small 类型小组件编译没问题,点击后无回调。

struct MediumWidgetView: View {
    var body: some View {
        Link(destination:URL(string: "urlschema://pjhubsLink")!) {
            VStack(alignment:.leading) {
                Text("PJHubs")
            }
        }
    }
}
  • SceneDelegate.m 中的内容为:

可以直接复用以往主工程通过消息通知 push 的逻辑流程来打开 widget 上挂载的 schema。

#import "SceneDelegate.h"
#import "WidgetURLViewController.h"
#import "LinkViewController.h"

@implementation SceneDelegate

- (void)scene:(UIScene *)scene openURLContexts:(NSSet *)URLContexts {
    if (URLContexts.allObjects.count != 0) {
        UIOpenURLContext *urlContext = URLContexts.allObjects.firstObject;
        NSURL *url = urlContext.URL;
        if ([url.absoluteString isEqualToString:@"urlschema://pjhubsWidgetURL"]) {
            [self.window.rootViewController presentViewController:[WidgetURLViewController new] animated:YES completion:nil];
        }
        
        if ([url.absoluteString isEqualToString:@"urlschema://pjhubsLink"]) {
            [self.window.rootViewController presentViewController:[LinkViewController new] animated:YES completion:nil];
        }
    }
    
}

@end

注意点

  • 调试 Widget Deep Link 跳转时需要切换到 app target 下进行调试,一直在用 Widget target 调,发现断点一直走不进去,才猛的想起来,我在 widget target 里能断在 app target 里才奇了怪了。

  • 更新 widget 的内容后,需要 build 一遍 widget target,然后再回到 app target 走 app 生命周期相关方法。

Tips: 如果 OC View 想要被使用在 SwiftUI 中。

首先创建或确定要被引入 SwiftUI 中的 OC 视图,下文以 OCView 替代。

#import "OCView.h"

@implementation OCView

- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        [self initView];
    }
    return self;
}

- (void)initView {
    self.backgroundColor = [UIColor whiteColor];
    
    UILabel *textLabel = [[UILabel alloc] init];
    textLabel.text = @"这是 OC View";
    textLabel.font = [UIFont systemFontOfSize:30];
    [textLabel sizeToFit];
    textLabel.frame = CGRectMake((self.frame.size.width - textLabel.frame.size.width)/2, (self.frame.size.height - textLabel.frame.size.height)/2, textLabel.frame.size.width, textLabel.frame.size.height);
    [self addSubview:textLabel];
}

@end

创建一个 OCWidgetView.swift 文件,用于封装 SwiftUI 视图。

import Foundation
import SwiftUI


struct OCWidgetView: UIViewRepresentable {
    func makeUIView(context: Context) -> OCView {
        return OCView()
    }
    
    func updateUIView(_ uiView: OCView, context: Context) {
        
    }
}

SwiftUI 中提供了 Coordinator 这个理念来作为 OCView 可能存在的各种 delegate 相关回调事件,在 SwiftUI 中同样可以进行使用,在此不做展开。

此时就可以在 SwiftUI 中引入 OCWidgetView 了!

struct MediumWidgetFansView: View {
    var body: some View {
        VStack(alignment:.leading) {
            OCWidgetView()
        }
    }
}

Widget 的 UI 部分只能使用 SwiftUI 框架下的 UI 组件,不能使用任何 UIKit 相关的组件,就算用 SwiftUI 包一层也不行(UIViewRepresentable),强行使用的话,会在 Widget 视图上得到一个黄色背景红叉:

从实际问题看 SwiftUI 和 Widget 编程_第10张图片

Widget 如何更新数据?

刷新时机

Timeline 刷新

Widget 需要通过 Timeline 来进行数据刷新,但其刷新的时机由系统控制,但有时我们设置了刷新间隔时间也不一定会在该时间点进行刷新。

如果我们完全依赖 Widget 自身的数据更新策略,每次间隔 1s 刷新数据,每次更新时拉取 5 个数据,设置 Timeline policy为 .atEnd,也即当 timeline 中的数据用完后立即拉取下一条数据,则最短也许要 1min 时间才能拉取下一条 timeline(自测)。

  • atEnd: 拉取到最后一个数据后重新拉取。

  • atAfter: 在指定时间后以一定时间间隔拉取数据。

  • never:该 timeline 不需要刷新。

不是都需要一次在 timeline 中构造好多个数据实体,可以一次返回一个,刷新间隔设置为 1min(或其它时间),这样比较适合对数据实时性要求较高的产品。系统并不少按照我们所规定的那样执行逻辑,系统考虑的因素用官方的话语来说,要结合耗电量等等问题综合给到不同 Widget 的刷新时机和此时,但总的来说,经常被查看的 widget 会获得更多的刷新机会。

主动刷新

如果我们想要在 app 内主动同步 widget 上所展示的消息,或在当前时刻必须刷新,如“开言英语” Widget 用户登录前后的 UI 表现不同等,在这种需求背景下,我们可以使用 WidgetKit 的 WidgetCenter API 来完成。

从实际问题看 SwiftUI 和 Widget 编程_第11张图片

WidgetCenter.shared.reloadAllTimelines()。reloadAllTimelines 方法会重新 load 所属 app 内的所有已配置的 Widget,重新拉取 Timeline。

需要注意的是,WidgetKit 为 Swift Only,想要在 OC 工程中使用该方法刷新 Widget Timeline 得通过 Swift 包一层,且要求 app 处于活跃状态。

import WidgetKit

@objcMembers class PJWidgetCenter: NSObject {
    class func reloadWidgetTimeline() {
        WidgetCenter.shared.reloadAllTimelines();
        WidgetCenter.shared.reloadTimelines(ofKind: "What kind of widget?")
    }
}

支持所有 widget 刷新或某一个 widget。

#import "ViewController.h"
#import "SwiftUIWidget-Swift.h"

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [PJWidgetCenter reloadWidgetTimeline];
}

数据来源

getTimeline 方法支持异步操作,我们如果需要动态的走网络请求拉取构造 timeline 数据,可以直接丢出一个异步回调。

struct Provider: IntentTimelineProvider {

    // ...
    func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline) -> ()) {
        networkHandler {
            let timeline = Timeline(entries: $0, policy: .atEnd)
            completion(timeline)
        }
    }

    func networkHandler(completion: @escaping ([SimpleEntry]) -> Void) {
        URLSession.shared.dataTask(with: URL(string: "http://pjhubs.com")!) { (data, response, error) in
            let originalDict = try? JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments) as? NSDictionary
            print(originalDict as Any)
            completion([SimpleEntry(date: Date(), count: 1234, image: UIImage(named: "avatar")!, configuration: ConfigurationIntent())])
        }.resume()
    }
}

注意请求间隔、Timeline 更新时间和数据转换等问题。

数据共享

以在 Widget 展示用户头像举例,在以往的开发经历中,我们都不希望有同步操作阻塞主线程从而造成 app 卡顿,故在 Widget 中我们也会自然而然的在“图片展示”这一环节中套用异步请求资源的思路去做,但这在 Widget 中是不被允许的,我们需要转变一个思路。

以下这种把图片资源延后到 UI 层的做法可以拉取成功,但不会被加载。

struct SmallWidgetView: View {
    @State var networkImage: UIImage?

    var body: some View {
        VStack(alignment:.leading) {
            HStack {
                Image(uiImage: self.networkImage ?? UIImage(named: "avatar")!)
                    // ...
                    .onAppear(perform: getNetworkImage)                
            }       
           // ...
        }
        // ...
    }

    func getNetworkImage() {
        URLSession.shared.dataTask(with: URL(string: "https://tu.sioe.cn/gj/qiege/image.jpg")!) { (data, _, _) in
            self.networkImage = UIImage(data: data!)
        }.resume()
    }
}

解决这一问题目前有三种方法但都是一种思路,核心就是把图片的加载过程从异步转化为同步,这个同步的过程可以是在 getTimeline 初始化时间线时,也可以是在构造 Widget UI 层逻辑时。

  • UserDefault

  • FileManager

  • CoreData

以下为在 getTimeline 初始化时间线时的事例:

struct SmallWidgetView: View {
    var uiImage: UIImage?

    var body: some View {
        VStack(alignment:.leading) {
            HStack {
                Image(uiImage: uiImage ?? UIImage(named: "avatar")!)
                    // ...
            }       
           // ...
        }
        // ...
    }   
}
struct Provider: IntentTimelineProvider {
    // ...
    func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline) -> ()) {
        var entries: [SimpleEntry] = []
        let currentDate = Date()
        for hourOffset in 0 ..< 3 {
            let entryDate = Calendar.current.date(byAdding: .second, value: hourOffset, to: currentDate)!
            // NOTE: 在处理 timeline 时就把资源加载好
            var image: UIImage? = nil
            if let imageData = try? Data(contentsOf: URL(string: "https://tu.sioe.cn/gj/qiege/image.jpg")!) {
                image = UIImage(data: imageData)
            }
            let entry = SimpleEntry(date: entryDate, count: Int.random(in: 0...100), image: image, configuration: configuration)
            entries.append(entry)
        }

        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

Widget 在最初放出的 beta 版本中是可以支持图片资源的异步回调的,但后来又改成了目前的这种只能通过同步的方式进行资源获取。

如果出现不同类型的 widget 需要复用图片资源,可以使用系统内轻量级 cache 方法(如:NSCache等)来完成在 A 类型 Widget 下已经加载完成的图片资源,后续用户再手动添加 B 类型 Widget 后可以加速 Widget 渲染。

当我们需要从 app target 传递数据到 widget target 时,可以组成 App Groups,通过 UserDefualt 来完成数据传递,注意两个 target 都需要增加 app groups。

从实际问题看 SwiftUI 和 Widget 编程_第12张图片

在 app target 中设置测试代码,10s 后刷新 widget 的显示内容,以此来模拟真实 app 中主工程触发某个网络事件,等待延时后同步数据给 Widget。

App target

#import "ViewController.h"
#import "SwiftUIWidget-Swift.h"

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [PJWidgetCenter reloadWidgetTimeline];
    });
}
@end

Swift 处理过程(非必需)

import WidgetKit
import Foundation

@objcMembers class PJWidgetCenter: NSObject {
    class func reloadWidgetTimeline() {
        if let userDefaults = UserDefaults(suiteName: "group.com.pjhubs.swiftuiwidge") {
            userDefaults.setValue("2333", forKey: "integer")
        }
        WidgetCenter.shared.reloadAllTimelines();
    }
}

Widget

struct Provider: IntentTimelineProvider {
    @AppStorage("integer", store: UserDefaults(suiteName: "group.com.pjhubs.swiftuiwidge"))
    var intString: String = ""

    //...        
    func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline) -> ()) {
        var entries: [SimpleEntry] = []
        let currentDate = Date()
        for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .second, value: hourOffset, to: currentDate)!
            // ...
            let entry = SimpleEntry(date: entryDate, count: Int(intString)!, image: image, configuration: configuration)
            entries.append(entry)
        }
        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

Widge 如何在「智能堆叠」中提高展示?

推荐看完 为小组件添加智能和配置 https://developer.apple.com/videos/play/wwdc2020/10194/

基于 iOS12 引入的 Intent.framework,目前有两种提高 Widget 在智能堆叠中展示的办法。

  • 用户行为捐赠(系统推断)

  • 数据源评分展示(评估函数判分)

用户行为捐赠

WWDC20 Session - 为小组件添加智能和配置视频截图。

从实际问题看 SwiftUI 和 Widget 编程_第13张图片

我们可以把一些自定义的关键组合信息构造出一个 intent 捐赠给系统,通过 Intent.framework,系统不但可以把这些信息传递给我们 app widget 还可以传递到 spotlight 等其它依赖 Intent 的场景从而减少进入特定场景/app 的步骤。

转换成我们的产品视角,当作者每天都在 14 点查看自己的视频播放量这一个指标数据,可以在作者进入到指标页面时,通过构造 Intent 实例进行捐赠给系统,当累计到一定次数(不定)后,系统会在每天用户 14 点前后解锁进入主屏时,在「智能堆叠」Widget 中自动翻滚到我们加入其中的 Widget 并展示出对应的播放量 Widget。

从实际问题看 SwiftUI 和 Widget 编程_第14张图片

数据源评分展示

如果我们想要在特定时间主动突出小组件在智能堆叠上的展示机会,可以使用「数据源评分展示」策略,在构造 Timeline 时可以给不同的数据实体塞入不同的评分,从而达到在不同时间节点或特定时间节点下的突出展示。

转换成我们的产品视角,当作者新发布了一个视频,可能想要在未来的一天、两天甚至一周内关注视频本身的播放量这一指标,我们可以通过固定分数和持续时间来达到提升展示,从而关闭其它数据源更新时的

struct Provider: IntentTimelineProvider {

    // ...
    func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline) -> ()) {
        // ...
        for hourOffset in 0 ..< 3 {
           // ...
            if (hourOffset == 1) {
                let revelance = TimelineEntryRelevance(score: 2000, duration: 60);
                let entry = SimpleEntry(date: entryDate, count: 2000, image: image, configuration: configuration, relevance: revelance)
                entries.append(entry)
            } else {
                let revelance = TimelineEntryRelevance(score: 10, duration: 0);
                let entry = SimpleEntry(date: entryDate, count: Int(intString)!, image: image, configuration: configuration, relevance: revelance)
                entries.append(entry)
            }
        }

        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

需要注意的是,我们给在第二分钟时要展现的数据分数 Relevance 分数设置为 2000,其它数据的分数设置为 10 分,此时运行 Widget 并等待到第二分钟,智能堆叠的 Widget 并不会一定翻转到我们的 Widget 上,但 Widget 上的数据是确确实实被更新了的,同时也说明了系统并不会一定认为当前数据比同一个 Widget 下的其它数据评分高,就一定为在智能堆叠上必须展示我们的 Widget,只是说在智能堆叠执行翻转时,我们的 Widget 会获得比其它 Widget 可能会获得更高的展示机会。

并且该评分也仅仅只是和当前 Widget 内的数据源做的对比。

从实际问题看 SwiftUI 和 Widget 编程_第15张图片

程序员专栏 扫码关注填加客服 长按识别下方二维码进群

近期精彩内容推荐:  

 几句话,离职了

 中国男性的私密数据大赏,女生勿入!

 为什么很多人用“ji32k7au4a83”作密码?

 一个月薪 12000 的北京程序员的真实生活 !


在看点这里好文分享给更多人↓↓

你可能感兴趣的:(java,ios,编程语言,python,移动开发)