從實際問題看 SwiftUI 和 Widget 編程

Widget 的實現依賴 SwiftUI,SwiftUI 部分的內容推薦你們自行學習,給出了一些質量較高的文章,能夠配置 WWDC19/20 SwiftUI 相關 session 搭配食用。git

參考資料

Demo 工程:SwiftUIWidgetgithub

前言

iOS14 的 Widget 和 iOS14 以前的 Widget 已經完成了統一,以前老樣式的 Widget 只能經過在老版本上進行查看,後續僅支持 iOS14 目前的 Widget。只能使用 SwiftUI 進行開發。shell

Widget 核心

  • 快速、關聯性、個性化
  • 看一眼,就可以獲取到重點內容
  • 內容纔是最重要的
    • 相冊 Widget 注意到的話,會發現展現的照片老是某個時刻下最棒的那一張,而不是最新的。

Widget 不是 mini app,應該看做是把 app 的內容在主屏幕的映射關係。官方給出的數據,通常咱們會在一天的時間裏進入主屏幕超過 90 次,並在主屏幕上短暫停留。編程

Widget 類型

Widget 有三個尺寸,但不強迫每一個尺寸都實現,由於不是全部 app 都適合全尺寸 widget 展現,但推薦都實現(猜想就是要給用戶最大自由度。json

  • 不能滾動和不能添加開關等其它系統控件。
  • 不支持視頻和動圖。
  • 小組件並非在主屏幕上實時展現的。
    • 系統的時鐘 Widget 的事實刷新 UI 是個系統級 app 才能擁有的對待。
    • SwiftUI 中對 Text 組件新增了能夠實時展現時間的 API。
      • Text(Date(), style: .time)
  • 不要把小尺寸組件直接拉伸成中或者大尺寸小組件。
  • Small 尺寸組件只能接受單次點擊。
  • 小組件內部按照 16pt 設定佈局邊距。
  • 小組件內部有圓形素材,應該使用 11pt 邊距。
  • 小組件內部邊界有圓角時要作得跟小組件自己的圓角半徑同心。
    • 不一樣設備上的小組件自己圓角值不同,不能直接寫死圓角值。
    • SwiftUI 中提供了一個圓角容器。
  • 字體官方推薦使用 SF 系列,可自定義。
  • 不要放入 app logo 和 name。

Widget 如何成組?

控制容許用戶選擇的小組件類型swift

@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 值來返回不一樣的樣式。api

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 的入口。bash

換句話說,此時咱們進入到「Widget 搜索」,找到咱們的 app,只會看到一個 Widget。markdown

@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])
    }
}
複製代碼

@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<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  %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。

  • IntentCOnfiguration。容許用戶選擇配置,參考下圖。

其中 IntentConfiguration 可提供給用戶有限的「自由」,自行選擇對應 Widget 下須要展現的數據源。利用了基於 Intents.framework 框架實現,並能夠直接複用 SiriKit 的功能來達到 Widget 的智能化(後文再敘)。

配置 IntentConfiguration 的步驟以下:

  • 建立對應的 IntentConfiguration 文件。

  • 新增用戶可配置的數據類型

  • 配置新增數據類型相關信息

  • 在對應類型的 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<Entry>) -> ()) {
        // ...
        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}
複製代碼

placeholder方法中返回 widget 在初始化 loading 過程當中的佔位 UI。

  • 每個 widget 都必須提供。
  • 默認內容展現。
  • 沒有任何用戶相關數據。
  • 當系統沒法顯示你的小組件數據時會出現。
  • 沒法被告知何時應該展現佔位圖。這是系統行爲,系統須要的時候就會要求展現,例如用戶更換了 widget 尺寸等。

這個視圖是系統行爲,只要咱們使用的是標準 SwiftUI 組件,會自動根據組件類型,如 ImageText 結合咱們自定義的顏色和背景來自動完成佔位圖的設置,若是咱們不想要的系統自定義的話,也能夠在方法中自行返回自定義的佔位組件。

注意點:在構建 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<UIOpenURLContext *> *)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 視圖上獲得一個黃色背景紅叉:

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 來完成。

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<Entry>) -> ()) {
        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<Entry>) -> ()) {
        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。

在 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<Entry>) -> ()) {
        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 如何在「智能堆疊」中提升展現?

推薦看完 爲小組件添加智能和配置

基於 iOS12 引入的 Intent.framework,目前有兩種提升 Widget 在智能堆疊中展現的辦法。

  • 用戶行爲捐贈(系統推斷)
  • 數據源評分展現(評估函數判分)

用戶行爲捐贈

WWDC20 Session - 爲小組件添加智能和配置視頻截圖。

咱們能夠把一些自定義的關鍵組合信息構造出一個 intent 捐贈給系統,經過 Intent.framework,系統不但能夠把這些信息傳遞給咱們 app widget 還能夠傳遞到 spotlight 等其它依賴 Intent 的場景從而減小進入特定場景/app 的步驟。

轉換成咱們的產品視角,看成者天天都在 14 點查看本身的視頻播放量這一個指標數據,能夠在做者進入到指標頁面時,經過構造 Intent 實例進行捐贈給系統,當累計到必定次數(不定)後,系統會在天天用戶 14 點先後解鎖進入主屏時,在「智能堆疊」Widget 中自動翻滾到咱們加入其中的 Widget 並展現出對應的播放量 Widget。

數據源評分展現

若是咱們想要在特定時間主動突出小組件在智能堆疊上的展現機會,可使用「數據源評分展現」策略,在構造 Timeline 時能夠給不一樣的數據實體塞入不一樣的評分,從而達到在不一樣時間節點或特定時間節點下的突出展現。

轉換成咱們的產品視角,看成者新發布了一個視頻,可能想要在將來的一天、兩天甚至一週內關注視頻自己的播放量這一指標,咱們能夠經過固定分數和持續時間來達到提高展現,從而關閉其它數據源更新時的

struct Provider: IntentTimelineProvider {

    // ...
    func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        // ...
        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 內的數據源作的對比。

相關文章
相關標籤/搜索