iOS14 Widget 萬(wàn)字開(kāi)發(fā)指南,先人一步獲得頂級(jí)流量

2020 年 6 月 22 日,蘋(píng)果召開(kāi)了第一次線上的開(kāi)發(fā)者大會(huì) - WWDC20。這次發(fā)布會(huì)上宣布了ARM架構(gòu)Mac芯片(拳打Intel)、iOS 14 ATT(腳踢Facebook),可謂是一次載入史冊(cè)(我是爸爸)的發(fā)布會(huì)了,當(dāng)然還發(fā)布了被稱(chēng)為下一個(gè)頂級(jí)流量入口的Widget。

踩著八月的尾巴,本次我們就來(lái)探究一下Widget。

本文會(huì)從Widget初窺Widget開(kāi)發(fā)兩個(gè)維度和章節(jié)來(lái)探究一下Widget,
其中初窺章節(jié)會(huì)帶您簡(jiǎn)單的了解一下Widget,適合應(yīng)用決策者閱讀;
開(kāi)發(fā)章節(jié)會(huì)帶著您一步一步的完成設(shè)計(jì)開(kāi)發(fā)Widget,適合程序員閱讀。

Widget初窺

一、Widget是什么

In iOS 14, we have a dramatic new Home screen experience, one that is much more dynamic and personalized, with a focus on widgets.
The content is the focus.
This is very important: widgets are not mini-apps.
Think of this as more projecting content from your app onto the Home screen rather than full mini-apps filled with tiny little buttons.

一句話來(lái)說(shuō):Widget不是迷你應(yīng)用程序。而是一種新的主屏幕體驗(yàn),能快速提供用戶(hù)關(guān)心的內(nèi)容是重點(diǎn)

image


二、Widget的特點(diǎn)

  • Glanceable 一目了然
  • Relevant 早韭晚菘
  • Personalized 量體裁衣
0.png

要設(shè)計(jì)一個(gè)優(yōu)秀的Widget,就要先了解Widget的全部特點(diǎn),了然于胸

針對(duì)Apple提出的Glanceable、Relevant、Personalized分別用一個(gè)成語(yǔ)來(lái)形容就是一目了然早韭晚菘量體裁衣

簡(jiǎn)單來(lái)說(shuō)下這幾個(gè)特點(diǎn)


1、Glanceable | 一目了然,一覽無(wú)余

1.png

一個(gè)優(yōu)秀的Widget要一目了然,一覽無(wú)余。

普通人每天進(jìn)入“主屏幕”的次數(shù)超過(guò)90次,但是在主屏幕僅停留幾分鐘,就切換到其他App了。

所以Widget一定要充分利用狹小的屏幕展示最核心的信息,并且要簡(jiǎn)潔明了。設(shè)計(jì)新穎,便于快速瀏覽,高效是一個(gè)優(yōu)秀Widget的核心。

用戶(hù)不用思考這個(gè)Widget怎么使用,不需要點(diǎn)擊任何按鈕就可以獲得最關(guān)心的信息。


2、Relevant | 早韭晚菘

image

蘋(píng)果希望Widget可以和用戶(hù)緊密結(jié)合,與用戶(hù)的行為所關(guān)聯(lián),比如早上起床,用戶(hù)希望看一下天氣;中午恰飯,用戶(hù)希望有人推薦下附近的美食;晚高峰的時(shí)候,用戶(hù)希望了解一下行車(chē)路線;晚安的時(shí)候,希望記錄下次日的行程。

為此,蘋(píng)果系統(tǒng)提供了一個(gè)叫Smart Stacks(智能疊放)的功能,Smart Stacks是一個(gè)Widgets的集合。系統(tǒng)會(huì)根據(jù)每個(gè)人的習(xí)慣,自動(dòng)顯示用戶(hù)當(dāng)前時(shí)間點(diǎn)最需要的Widget。

<div style="align:center"><img src="https://guojunliu.github.io/images/widget/3.gif"/></div>


3、Personalized 量體裁衣

3.1 大小

Widget要能為用戶(hù)提供個(gè)性化的服務(wù),比如天氣Widget,需要能為不同的用戶(hù)提供不同細(xì)節(jié)的天氣情況。

4.png

為此Apple提供三種不同大小的小部件

  • systemSmall
  • systemMedium
  • systemLarge

其中systemSmall大小為2*2 Icon,systemMedium大小為4*2 Icon,systemLarge大小為4*4 Icon,具體的顯示效果如下

13.png

3.2 個(gè)性化配置

另一方面Widget需要能為不同城市的用戶(hù)提供當(dāng)?shù)氐奶鞖馇闆r。

為此Apple在創(chuàng)建Widget時(shí)為開(kāi)發(fā)者提供了兩種類(lèi)型:

  • StaticConfiguration :對(duì)于沒(méi)有用戶(hù)可配置屬性的窗口小部件,也就是用戶(hù)無(wú)需配置,展示的內(nèi)容只和用戶(hù)信息有關(guān)系。例如,顯示一般市場(chǎng)信息的股市窗口小部件,或顯示趨勢(shì)頭條的新聞窗口小部件。

  • IntentConfiguration :對(duì)于具有用戶(hù)可配置屬性的窗口小部件,也就是支持用戶(hù)配置及用戶(hù)意圖的推測(cè)。您使用SiriKit自定義意圖來(lái)定義屬性。例如,需要一個(gè)城市的郵政編碼的天氣小部件,或者需要一個(gè)跟蹤號(hào)的包裹跟蹤小部件。

image

需要說(shuō)明的是,IntentConfiguration并不需要編寫(xiě)代碼,只需要簡(jiǎn)單的配置,Xcode 會(huì)自動(dòng)幫你生成對(duì)應(yīng)的代碼和類(lèi)型。

3.3 黑暗模式

此外Widget還支持系統(tǒng)的黑暗模式

image


三、Widget的本質(zhì)

Widget的本質(zhì)是一系列靜態(tài)視圖堆疊而成的集合,不同的時(shí)間點(diǎn)展示不同的視圖

這里要引入Widget的核心Timeline

顧名思義,Timeline就是一條時(shí)間線,在對(duì)應(yīng)的時(shí)間點(diǎn)發(fā)生對(duì)應(yīng)的事件

許多Widget具有可預(yù)測(cè)的時(shí)間點(diǎn),在這些時(shí)間點(diǎn)更新其內(nèi)容是有意義的。例如,顯示天氣信息的小部件可能會(huì)在一整天內(nèi)每小時(shí)更新一次溫度。股市窗口小部件可以在公開(kāi)市場(chǎng)時(shí)間頻繁更新其內(nèi)容,但周末則不用完全更新。通過(guò)提前計(jì)劃這些時(shí)間,生成不同的視圖放入時(shí)間線中,WidgetKit會(huì)在適當(dāng)?shù)臅r(shí)間到來(lái)時(shí)自動(dòng)刷新您的窗口小部件。

這也決定了Widget基本上不能實(shí)時(shí)更新

另外值得一提的是,WidgetKit會(huì)把 Timelines 所定義的Views 結(jié)構(gòu)信息緩存到磁盤(pán),然后在刷新的時(shí)候才通過(guò) JIT 的方式來(lái)渲染。這使得系統(tǒng)可以在極低電量開(kāi)銷(xiāo)下為眾多 Widgets 處理 Timelines 信息。

image


四、用戶(hù)交互

不好意思,沒(méi)有交互?。?!

為了實(shí)現(xiàn)以上的特點(diǎn),Apple也移除限制了Widget的一些功能

  • 不能交互
  • 不能播放動(dòng)畫(huà)
  • 不能播放視頻
  • 不支持滾動(dòng)
  • 不支持主動(dòng)刷新視圖

唯一支持的只有用戶(hù)點(diǎn)擊Widget喚起主App

5.png

其中點(diǎn)擊喚起主App有兩種方案,分別是:

  • widgetURL
  • Link

widgetURL喚起App的點(diǎn)擊區(qū)域是Widget的所有區(qū)域,這種方案適合簡(jiǎn)單元素,單一邏輯的小部件

image

對(duì)于systemSmall類(lèi)型的小部件,只支持widgetURL喚起方式

針對(duì)systemMediumsystemLarge還可以使用更細(xì)分的Link喚起方式,這種喚起方式能讓小部件通過(guò)不同元素的點(diǎn)擊喚起App的不同頁(yè)面,讓開(kāi)發(fā)者有更多的施展空間

9.png

舉個(gè)簡(jiǎn)單的例子,widgetURL可應(yīng)用于天氣小部件,博客小部件,點(diǎn)擊直達(dá)App;

Link可用于備忘錄和日歷小部件,點(diǎn)擊不同的備忘錄和日期直接跳轉(zhuǎn)到對(duì)應(yīng)的備忘錄詳情和待辦詳情頁(yè)面

10.png


Widget初窺總結(jié)

Widget的出現(xiàn)猶如在一潭死水的iOS桌面上泛起了一片漣漪,一定會(huì)有很多App來(lái)爭(zhēng)奪這塊肥肉一般的流量入口。

但是仔細(xì)研究一下會(huì)發(fā)現(xiàn),Apple這次推出的Widget非??酥疲](méi)有非常激進(jìn),

俗話說(shuō):喜歡是放肆,但愛(ài)就是克制。這里不得不再次引用Apple在Widget介紹中出現(xiàn)頻率最高的話widgets are not mini-apps,因?yàn)閃idget在設(shè)計(jì)之初就是為了能使用最少的成本,向用戶(hù)提供最核心的信息。為了盡可能的減少用戶(hù)成本(電量,網(wǎng)絡(luò)等)和提高用戶(hù)體驗(yàn),Apple在技術(shù)層面上做了很多限制,限制了非常多的功能,大大削弱了Widget的地位和重要程度,也降低了開(kāi)發(fā)者實(shí)現(xiàn)的熱情和積極性

其實(shí)每年Apple更新的新技術(shù)只有很少的一部分能應(yīng)用到App上,希望這次的Widget能有動(dòng)力讓大家結(jié)合自己的App,給自己的App帶來(lái)更多的流量,也能給用戶(hù)帶來(lái)更好的體驗(yàn)。




Widget開(kāi)發(fā)

重頭戲來(lái)啦,接下來(lái)讓我們一步一步設(shè)計(jì)編寫(xiě)出優(yōu)秀的小部件吧

開(kāi)始之前,首先我們要介紹下Widget的開(kāi)發(fā)語(yǔ)言,Apple特別指定了小部件只能使用SwiftUI來(lái)開(kāi)發(fā)

一、SwiftUI

image

現(xiàn)在iOS主流的開(kāi)發(fā)語(yǔ)言還是Objective-C,那Apple為什么要選擇2019 WWDC發(fā)布迄今為止只有一年的SwiftUI呢?

首先,從一開(kāi)始就將小部件實(shí)現(xiàn)多平臺(tái)化是Apple的一個(gè)目標(biāo),SwiftUI在跨設(shè)備展示的能力上是一把大殺器;

其次SwiftUI還使自動(dòng)布局和暗模式等功能變得非常容易,降低了適配等開(kāi)發(fā)成本,對(duì)不需要太多元素的小部件來(lái)說(shuō),SwiftUI重點(diǎn)關(guān)注布局的特點(diǎn)無(wú)疑是最合適的;

從另一方面來(lái)講,只有使用 SwiftUI 才能達(dá)到我們上邊說(shuō)的對(duì)于 Widget 的限制。如果可以使用 Objective-C UIKit 的話,我們強(qiáng)大的開(kāi)發(fā)者可能會(huì)想出無(wú)數(shù)的黑科技來(lái)忽略Apple真的小部件的限制。比如開(kāi)發(fā)無(wú)法使用 UIViewRepresentable 來(lái)橋接 UIKit;

最后Apple也夾帶了自己的私心,Apple今年已經(jīng)將 Swift 語(yǔ)言和 SwiftUI 的重要程度提升到了一個(gè)新的高度,Swift已經(jīng)可以獨(dú)立于Foundtion框架,那么對(duì)應(yīng)的SwiftUI也應(yīng)該不依賴(lài)于UIKit框架了,強(qiáng)行使用SwiftUI可以使開(kāi)發(fā)人員盡可能容易地將其學(xué)習(xí)其內(nèi)容并應(yīng)用于iOS,iPadOS和macOS,

畢竟5月份卡位第20位Objective-C在6月份已經(jīng)跌出了前20

image

這里要重點(diǎn)說(shuō)明一下,Widget只要使用任何 UIKit 的元素就會(huì)直接 Crash


二、將小部件目標(biāo)添加到您的應(yīng)用

窗口小部件擴(kuò)展模板提供了創(chuàng)建窗口小部件的起點(diǎn)。單個(gè)小部件擴(kuò)展可以包含多種小部件。例如,一個(gè)體育應(yīng)用程序可能有一個(gè)顯示團(tuán)隊(duì)信息的小部件,另一個(gè)顯示游戲時(shí)間表的小部件。一個(gè)小部件擴(kuò)展可以包含兩個(gè)小部件。盡管建議將所有窗口小部件包含在一個(gè)窗口小部件擴(kuò)展中,但如有必要,可以添加多個(gè)擴(kuò)展。

  1. 在Xcode中打開(kāi)您的應(yīng)用程序項(xiàng)目,然后選擇“文件”>“新建”>“目標(biāo)”。

  2. 從“應(yīng)用程序擴(kuò)展”組中,選擇“窗口小部件擴(kuò)展”,然后單擊“下一步”。

  3. 輸入您的包名。

  4. 如果窗口小部件提供了用戶(hù)可配置的屬性,請(qǐng)選中Include Configuration Intent復(fù)選框。

單擊完成。

image

重點(diǎn):小部件不僅支持Swift項(xiàng)目,同樣也支持Objective-C項(xiàng)目,OC小伙伴不用擔(dān)心啦

創(chuàng)建完小部件之后,我們會(huì)多出一個(gè)SmileEverydayWidget.swift文件,這已經(jīng)是一個(gè)可以run起來(lái)的小部件了,因?yàn)槲覀兘酉聛?lái)要逐個(gè)方法來(lái)分析,所以先將文件全文展示如下

//
//  SmileEverydayWidget.swift
//  SmileEverydayWidget
//
//  Created by steve on 2020/8/28.
//

import WidgetKit
import SwiftUI

struct Provider: TimelineProvider {
    public typealias Entry = SimpleEntry

    public func snapshot(with context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date())
        completion(entry)
    }

    public func timeline(with 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)
    }
}

struct SimpleEntry: TimelineEntry {
    public let date: Date
}

struct PlaceholderView : View {
    var body: some View {
        Text("Placeholder View")
    }
}

struct SmileEverydayWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        Text(entry.date, style: .time)
    }
}

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

    public var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider(), placeholder: PlaceholderView()) { entry in
            SmileEverydayWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
    }
}

struct SmileEverydayWidget_Previews: PreviewProvider {
    static var previews: some View {
        /*@START_MENU_TOKEN@*/Text("Hello, World!")/*@END_MENU_TOKEN@*/
    }
}

這里的概念和代碼比較多,接下來(lái)我們一個(gè)一個(gè)來(lái)解釋


三、Widget API

首先我們從帶有main字段的方法來(lái)說(shuō)起,

@main
struct SmileEverydayWidget: Widget {
    private let kind: String = "com.steve.liu.smileEverydayWidget"

    public var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider(), placeholder: PlaceholderView()) { entry in
            SmileEverydayWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
    }
}

大家都知道帶有main標(biāo)識(shí)的方法都是程序的入口

這段代碼使用SwiftUI聲明了一個(gè)名為SmileEverydayWidget的小部件,其中StaticConfiguration是小部件的初始化方法,它有幾個(gè)參數(shù):

  1. kind
  2. provider
  3. placeholder
image

其中

kind標(biāo)識(shí)小部件的字符串,并且應(yīng)描述小部件所代表的內(nèi)容。即小部件的包名

provider時(shí)間線提供者

PlaceholderView占位視圖

同時(shí)也提供了一些方法,例如

  • configurationDisplayName()設(shè)置小部件顯示的名稱(chēng)
  • description()設(shè)置小部件的描述
  • supportedFamilies()設(shè)置小部件支持的尺寸

這里有一個(gè)重點(diǎn),為了使某個(gè)應(yīng)用程序的窗口小部件出現(xiàn)在窗口小部件庫(kù)中,用戶(hù)必須在安裝該應(yīng)用程序后至少啟動(dòng)一次包含該窗口小部件的應(yīng)用程序。


四、WidgetEntryView

WidgetEntryView就是使用SwiftUI布局的小部件視圖

struct SmileEverydayWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        Text(entry.date, style: .time)
    }
}

例如這個(gè)小部件視圖就簡(jiǎn)單的展示了當(dāng)前的時(shí)間

接下來(lái)我們可以將默認(rèn)的布局更改為我們自己想要的布局,例如我在設(shè)置了顯示文本的字體和小部件的背景圖

struct SmileEverydaWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        
        return Text(entry.message)
            .background(Image(entry.backgroundImageStr))
            .font(.callout)
    }
}


五、PlaceholderView

每種小部件都需要提供占位符UI。

占位符UI是窗口小部件的默認(rèn)內(nèi)容。

它應(yīng)該代表您的小部件類(lèi)型,但僅此而已。

此用戶(hù)界面中不應(yīng)有任何用戶(hù)數(shù)據(jù)。

17.png

想象一下

如果在用戶(hù)的主屏幕上出現(xiàn)如下的場(chǎng)景,那么你的小部件離被移除可能已經(jīng)不遠(yuǎn)了

image


六、TimelineEntry

我們知道小部件是按照時(shí)間線來(lái)展示的,TimelineEntry時(shí)間線上的一個(gè)個(gè)條目

struct SimpleEntry: TimelineEntry {
    public let date: Date
}

TimelineEntry有一個(gè)必須有的屬性就是date,也就是這個(gè)條目在時(shí)間線上的具體時(shí)間

另外開(kāi)發(fā)者可以在TimelineEntry里自定義各種屬性,用來(lái)給小部件視圖提供數(shù)據(jù)

例如我在TimelineEntry里自定義了messagebackgroundImageStr屬性,用來(lái)顯示小部件上的文字和背景圖片

struct SimpleEntry: TimelineEntry {
    public let date: Date
    public let message: String
    public let backgroundImageStr : String
}

七、TimelineProvider

TimelineProvider 是一個(gè)提供了上述我們所說(shuō)的TimelineEntry集合的對(duì)象

image

我們來(lái)看下具體的代碼:

struct Provider: TimelineProvider {
    public typealias Entry = SimpleEntry

    public func snapshot(with context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date())
        completion(entry)
    }

    public func timeline(with 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)
    }
}

其中包含兩個(gè)方法

  • snapshot()
  • timeline()

1、snapshot 快照

為了在小部件庫(kù)中顯示小部件,WidgetKit 要求提供者提供預(yù)覽快照, 即snapshot(),這個(gè)方法里主要提供了一些示例數(shù)據(jù),最好是真實(shí)數(shù)據(jù),用于時(shí)間線不能展示的時(shí)候展示給用戶(hù)

需要說(shuō)明的是,快照是系統(tǒng)需要快速顯示單個(gè)條目的位置。

因此,您的擴(kuò)展程序必須盡快返回視圖,因?yàn)檫@樣做時(shí),用戶(hù)會(huì)在iOS上漂亮的Widget Gallery中看到真正的Widget。

這不是我們?cè)谠O(shè)計(jì)時(shí)必須提供的屏幕截圖或圖像。這是用戶(hù)在iOS,iPadOS和macOS上真正的小部件體驗(yàn)。

在大多數(shù)情況下,時(shí)間軸的第一個(gè)條目和快照可以作為同一條目返回,因此,在“小工具庫(kù)”中看到的就是用戶(hù)將其添加到設(shè)備中時(shí)得到的內(nèi)容。

image

例如我們?cè)赪idget Gallery中添加電池小部件時(shí),小部件此時(shí)在Widget Gallery中展示的就是當(dāng)前設(shè)備電池信息的實(shí)時(shí)數(shù)據(jù)的快照,而不是一些虛假的數(shù)據(jù),這個(gè)時(shí)候小部件的數(shù)據(jù)是什么樣子,用戶(hù)添加到主屏幕上之后小部件的數(shù)據(jù)就是什么樣子,從而提高用戶(hù)的體驗(yàn)。

對(duì)比

小部件里有兩個(gè)比較類(lèi)似的概念,PlaceholderViewsnapshot,都是一種占位解決方案,不同的是PlaceholderView是在主屏幕上無(wú)法快速獲取數(shù)據(jù)時(shí)的一種占位視圖,不至于顯示loading或者白屏給用戶(hù)看;而snapshot主要用于Widget Gallery中,用來(lái)提高用戶(hù)體驗(yàn)的,一般來(lái)說(shuō),snapshot就是時(shí)間線的第一幀

2、timeline

在請(qǐng)求初始快照后,WidgetKit調(diào)用timeline以請(qǐng)求提供者的常規(guī)時(shí)間軸。時(shí)間軸由一個(gè)或多個(gè)時(shí)間軸條目TimelineEntry以及一個(gè)重載策略ReloadPolicy組成,該重載策略通知WidgetKit何時(shí)請(qǐng)求后續(xù)時(shí)間軸。

關(guān)于重載策略,提供了以下幾種策略

  • atEnd: 是指 Timeline 執(zhí)行到最后一個(gè)時(shí)間片的時(shí)候再刷新。
  • atAfter: 是指在某個(gè)時(shí)間以后有規(guī)律的刷新
  • never:是指以后不需要刷新了。什么時(shí)候需要重新刷新需要 App 重新告知 Widget

根據(jù)上邊的分析,我們可以將TimelineProvider改造如下

struct Provider: TimelineProvider {
    public typealias Entry = SimpleEntry

    public func snapshot(with context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let date = Date()
        let message = "蒹葭蒼蒼,白露為霜。所謂伊人,在水一方。"
        let backgroundImageStr = "bg7"
        
        
        let entry = SimpleEntry(date: date, message: message, backgroundImageStr: backgroundImageStr)
        completion(entry)
    }

    public func timeline(with context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []
        
        var currentDate = Date()
        var nextUpdateDate = Calendar.current.date(byAdding: .second, value: 3, to: currentDate)!
        
        let message = "蒹葭蒼蒼,白露為霜。所謂伊人,在水一方。\n溯洄從之,道阻且長(zhǎng);溯游從之,宛在水中央。\n蒹葭凄凄,白露未晞。所謂伊人,在水之湄。"
        let backgroundImageStr = "bg"
        
        
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
        var currentDateStr = ""
        var nextUpdateDateStr = ""

        let longUuid = UUID().uuidString
        let range: Range = longUuid.range(of: "-")!
        let location: Int = longUuid.distance(from: longUuid.startIndex, to: range.lowerBound)
        let uuid = longUuid.prefix(location)
        
        for i in 1 ..< 10 {
            
            var msg = ""
            
            currentDate = nextUpdateDate;
            nextUpdateDate = Calendar.current.date(byAdding: .second, value: 3, to: currentDate)!
            
            currentDateStr = formatter.string(from: currentDate)
            nextUpdateDateStr = formatter.string(from: nextUpdateDate)
            
            msg.append(message)
            msg.append("\n時(shí)間軸ID " + uuid);
            msg.append("\n時(shí)間軸第" + String(i+1) + "個(gè)視圖")
            msg.append("\n本次視圖開(kāi)始時(shí)間 " + currentDateStr)
            msg.append("\n下次視圖開(kāi)始時(shí)間 " + nextUpdateDateStr)
            
            let entry = SimpleEntry(date: currentDate, message: msg, backgroundImageStr: backgroundImageStr+String(i))
            
            entries.append(entry)
            print(String(i))
        }
        
        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

這段代碼里我們提供了一個(gè)古詩(shī)詞的快照

并生成了一個(gè)時(shí)間線,其中時(shí)間線里包含了10個(gè)entry,且每個(gè)entry 間隔10s

entry文本拼接顯示了

  • 展示文本(詩(shī)經(jīng))
  • 時(shí)間軸ID
  • 本次視圖開(kāi)始時(shí)間
  • 下次視圖開(kāi)始時(shí)間

下邊我們來(lái)看一下真實(shí)run出來(lái)的效果

21.gif

到此為止我們就有了一個(gè)可以在主屏幕上展示的小部件了

并且能根據(jù)時(shí)間線展示不同的視圖了。


七、Reload Timeline 使小部件保持最新

為了使我們的小部件能隨時(shí)提供最新的,而不是過(guò)期的信息,我們需要不時(shí)的對(duì)小部件進(jìn)行更新。

我們已經(jīng)知道了小部件的本質(zhì)是一系列的視圖堆疊,那么更新小部件就是更新這些視圖。

28.png

比如一個(gè)有三個(gè)視圖的小部件,預(yù)測(cè)了現(xiàn)在和未來(lái)3小時(shí)的天氣預(yù)報(bào),這個(gè)小部件顯示步驟如下:

image

但是有一個(gè)很重要的問(wèn)題就是時(shí)間線是我們預(yù)測(cè)出來(lái)的,是預(yù)測(cè)就會(huì)有偏差。比如天氣預(yù)報(bào),預(yù)報(bào)2小時(shí)后有雨,但是隨著天氣的變化,2小時(shí)后變成晴天了,這個(gè)時(shí)候我們?nèi)绻桓滦〔考系臅r(shí)間線,就會(huì)在2小時(shí)后給用戶(hù)提供錯(cuò)誤的信息。

為此我們需要在有信息變化的時(shí)候重新顯示新的視圖,如下圖:

image

為了是我們的小部件信息準(zhǔn)確無(wú)誤,首先我們需要了解下小部件是如何刷新的

很不幸,Widget 的刷新完全由 WidgetCenter控制。開(kāi)發(fā)者無(wú)法通過(guò)任何 API 去主動(dòng)刷新 Widget 的頁(yè)面,只能告知 WidgetCenter,Timeline 需要刷新了

所以我們不能直接刷新小部件的視圖,而是要通過(guò)生成一個(gè)新的時(shí)間線來(lái)替換舊的時(shí)間線,Reload Timeline 并不是直接刷新 Widget,而是 WidgetCenter 重新向 Widget 請(qǐng)求下一階段的數(shù)據(jù)。

其中Reload Timeline分為兩種方式

  • System Reloads
  • App Reloads

1、System Reloads

這個(gè)行為由系統(tǒng)主動(dòng)發(fā)起,會(huì)調(diào)用一次 Reload Timeline 向 Widget 請(qǐng)求下一階段刷新的數(shù)據(jù)。系統(tǒng)除了會(huì)按時(shí)發(fā)起 System Reloads 之外,還會(huì)動(dòng)態(tài)決策每個(gè)不同的 TimeLine 的 System Reloads 的頻次。比如被點(diǎn)擊次數(shù)很大程度上直接決定了 System Reloads 的頻率,點(diǎn)擊率越高,更新頻次越快,當(dāng)然還有一些由于設(shè)備環(huán)境變化觸發(fā)的行為也會(huì)觸發(fā) System Reloads,比如設(shè)備時(shí)間進(jìn)行了變更。

42.png

很顯然這種方案不能很好的解決我們上邊的問(wèn)題

2、App Reloads

這種行為指的是App主動(dòng)通知小部件,你需要更新信息了。這里邊根據(jù)App的當(dāng)前的前后臺(tái)狀態(tài)又分為兩種方式

  • 應(yīng)用在前臺(tái)運(yùn)行
  • 應(yīng)用在后臺(tái)運(yùn)行

當(dāng)應(yīng)用在前臺(tái)運(yùn)行的時(shí)候,App 可以直接使用WidgetCenter的 API 來(lái) Reload Timeline;而當(dāng)應(yīng)用處于后臺(tái)時(shí),可以使用后臺(tái)推送(Background Notification)來(lái) Reload Timeline。

image

除了這些,給Timeline設(shè)定合適的刷新策略也是很重要的手段

合理的組合使用這些刷新機(jī)制,能夠極大的提高Widget信息的準(zhǔn)確性

46.png


八、交互

前邊我們說(shuō)過(guò),widget和app交互有兩種方式SwiftUI widgetURL APISwiftUI Link API

這兩種方式的本質(zhì)都是URL Schemes,只要監(jiān)聽(tīng)SceneDelegatescene:openURLContexts:就可以了

由于Schemes大家都太熟悉了,關(guān)于如何高效快速準(zhǔn)確的傳遞參數(shù),這里就不展開(kāi)講了。

image


九、設(shè)計(jì)漂亮的小部件

如果你已經(jīng)看到了這里,并且已經(jīng)理解了上述的講解,你已經(jīng)具備了開(kāi)發(fā)小部件的能力。

那么有哪些關(guān)鍵點(diǎn)能給自己的小部件錦上添花呢?

去除額外的App信息:系統(tǒng)會(huì)在小部件下方自動(dòng)顯示你的應(yīng)用名稱(chēng),因此你無(wú)需在內(nèi)容中重復(fù)App的名稱(chēng),Icon,而是要通過(guò)顏色,布局和圖像來(lái)聯(lián)系您的App

簡(jiǎn)潔的描述。小部件庫(kù)中顯示的描述可以幫助人們理解每個(gè)小部件的功能。從動(dòng)作動(dòng)詞開(kāi)始描述通常效果很好;例如,“查看當(dāng)前天氣狀況和位置預(yù)測(cè)”或“跟蹤即將舉行的活動(dòng)和會(huì)議”。避免包含不必要的短語(yǔ)來(lái)引用窗口小部件本身,例如“此窗口小部件顯示...”,“使用此窗口小部件...”或“添加此窗口小部件”。

舒適的信息密度:一覽無(wú)余。當(dāng)內(nèi)容顯得稀疏時(shí),小部件可能看起來(lái)是多余的;當(dāng)內(nèi)容太密集時(shí),小部件將無(wú)法瀏覽。如果要包含很多信息,請(qǐng)避免讓小部件成為難以解析的項(xiàng)的拼貼。尋求整理內(nèi)容的方法,以便人們可以立即掌握關(guān)鍵部分,并以更長(zhǎng)的時(shí)間查看相關(guān)細(xì)節(jié)。您可能還考慮創(chuàng)建一個(gè)較大的小部件,并尋找可以用圖形替換文本而又不會(huì)失去清晰度的位置。

明智地使用顏色:豐富,美麗的色彩吸引眼球,但它們絕不能阻止人們一眼就吸收小部件的信息。使用顏色可以增強(qiáng)小部件的外觀,而不會(huì)與小部件的內(nèi)容競(jìng)爭(zhēng)。

使用系統(tǒng)字體,支持系統(tǒng)功能:例如 支持黑暗模式;使用SF Pro和使用系統(tǒng)字體;文本可縮放。

image

設(shè)計(jì)一個(gè)真實(shí)的預(yù)覽以顯示在小部件庫(kù)中:突出顯示小部件的外觀和功能可幫助人們做出明智的決定,并鼓勵(lì)他們添加小部件。您可以在小部件預(yù)覽中顯示真實(shí)數(shù)據(jù),但是如果數(shù)據(jù)生成或加載所需的時(shí)間太長(zhǎng),請(qǐng)顯示真實(shí)的模擬數(shù)據(jù)。

設(shè)計(jì)占位符內(nèi)容,以幫助人們識(shí)別您的小部件。小部件在加載數(shù)據(jù)時(shí)顯示占位符內(nèi)容。通過(guò)將UI的靜態(tài)部分與代表實(shí)際內(nèi)容的半透明形狀結(jié)合起來(lái),可以創(chuàng)建有效的預(yù)覽。例如,您可以使用不同寬度的矩形來(lái)建議文本行,并使用圓環(huán)或正方形代替字形和圖像。

image

圖片適配屏幕尺寸:確保圖片在大部件和小部件下都不會(huì)壓縮

image


十圍之木,始生如蘗

簡(jiǎn)單的總結(jié)一下

一個(gè)優(yōu)秀的小部件是完全可以提高用戶(hù)體驗(yàn),成為很好的流量入口,給App帶來(lái)巨大的商業(yè)價(jià)值。

但是要設(shè)計(jì)一個(gè)優(yōu)秀的小部件也并非易事。

本文拋磚引玉,希望大家能設(shè)計(jì)出更多優(yōu)秀的小部件。

本次的Widget指北到這里就結(jié)束了,萬(wàn)字不易,多多傳播。

喜歡我你就關(guān)注我,

有話說(shuō)你就評(píng)論我,

都不干你就點(diǎn)個(gè)贊

image


Demo


參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,055評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 175,365評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,561評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,346評(píng)論 6 404
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 54,889評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評(píng)論 3 439
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,118評(píng)論 0 286
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,637評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,558評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,739評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評(píng)論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,980評(píng)論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,362評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,619評(píng)論 1 280
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,347評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,702評(píng)論 2 370