### iOS14 Widget開發(fā)踩坑(一)修正版-初識(shí)與刷新

iOS14 Widget開發(fā)踩坑(一)修正版-初識(shí)與刷新

前言

轉(zhuǎn)載:寫程序的檸檬精 原文

2020年10月3日修正版
在對(duì)Widget進(jìn)行開發(fā)了一個(gè)月后,解決了幾個(gè)問(wèn)題,對(duì)本文進(jìn)行重新編輯以糾正以前的錯(cuò)誤和適應(yīng)最新版本。

2020年10月15日修正版
對(duì)刷新和視圖有了新的理解,修改刷新部分。

這里記錄一些我在開發(fā)的過(guò)程中遇到的一些坑,希望對(duì)開發(fā)有用。本文涉及到的代碼都只是示例代碼,僅提供思路,并不能直接復(fù)制使用,需要有一些開發(fā)Today Widget的知識(shí),方便進(jìn)行對(duì)比。本文部分內(nèi)容引用自網(wǎng)絡(luò),如有侵權(quán)請(qǐng)聯(lián)系刪除。

開發(fā)須知

  1. WidgetExtension 使用的是新的WidgetKit不同于Today Widget,<mark style="box-sizing: border-box; outline: 0px; background-color: rgb(248, 248, 64); color: rgb(0, 0, 0); overflow-wrap: break-word;">它只能使用SwiftUI進(jìn)行開發(fā)</mark>,所以需要SwiftUI和Swift基礎(chǔ)。
  2. Widget只支持3種尺寸systemSmall (2x2)、 systemMedium (4x2)、 systemLarge(4x4)
  3. 默認(rèn)點(diǎn)擊Widget打開主應(yīng)用程序
  4. Widget類似于TodayWidget是一個(gè)獨(dú)立運(yùn)行的程序,需要在項(xiàng)目中進(jìn)行 App Groups 的設(shè)置才能使其與主程序互通數(shù)據(jù),這個(gè)以后會(huì)講。
  5. Apple官方已經(jīng)棄用Today Extension,Xcode12已經(jīng)不再提供Today Extension的添加,已經(jīng)有Today Widget的應(yīng)用則會(huì)顯示到一個(gè)特定的區(qū)域進(jìn)行展示。

準(zhǔn)備工作

部署環(huán)境

Widget的開發(fā)需要安裝Xcode 12以及iOS 14進(jìn)行。Apple官方下載鏈接

創(chuàng)建項(xiàng)目

正常的創(chuàng)建項(xiàng)目流程,我使用的是Swift語(yǔ)言、界面Storyboard,可以設(shè)置成自己習(xí)慣的配置,
Create a new Xcode project -> 填寫Product Name-> Next-> Create

作為一個(gè)ios開發(fā)者,遇到問(wèn)題的時(shí)候,有一個(gè)學(xué)習(xí)的氛圍跟一個(gè)交流圈子特別重要,對(duì)自身有極大幫助,眾人拾柴火焰高 這是一個(gè)我的iOS交流群:711315161,進(jìn)群密碼iOS 分享BAT,阿里面試題、面試經(jīng)驗(yàn),討論技術(shù), 大家一起交流學(xué)習(xí)成長(zhǎng)!希望幫助開發(fā)者少走彎路。

引入Widget Extension

  1. File -> New -> target-> Widget Extension ->Next
  2. 由于是加入一個(gè)新的Target,所以Widget的名字不能與項(xiàng)目名相同,也不能起成“Widget”(因?yàn)閃idget是一個(gè)已有的類名),刪除時(shí)不能只是刪除文件還要在項(xiàng)目的Targets中刪除,起已經(jīng)刪除過(guò)一次的名字會(huì)報(bào)找不到文件的錯(cuò)誤。
  3. 如果 Widget 支持用戶配置屬性(例如天氣組件,用戶可以選擇城市),就需要勾選Include Configuration Intent這個(gè)選項(xiàng),不支持的話不用勾選。建議勾選上,誰(shuí)知道以后會(huì)不會(huì)要求支持呢。
  4. 創(chuàng)建后,會(huì)自動(dòng)生成5個(gè)struct和自帶的方法

開始編寫

認(rèn)識(shí)代碼

預(yù)覽視圖-Previews

代碼運(yùn)行的預(yù)覽視圖是SwiftUI新特性,會(huì)將運(yùn)行成果顯示在右邊的視圖上且支持熱更新,但是會(huì)很卡,它不是Widget的必須部分,可以直接將其刪除或注釋。

struct MainWidget_Previews: PreviewProvider {
    static var previews: some View {
        MainWidgetEntryView(entry: SimpleEntry(date: Date()))
            .previewContext(WidgetPreviewContext(family: .systemSmall))
    }
}

數(shù)據(jù)提供-Provider

Provider是Widget最重要的部分,它決定了小組件的placeholder/getSnapshot/getTimeline這三種數(shù)據(jù)的顯示。在項(xiàng)目創(chuàng)建時(shí)勾選了Include Configuration Intent后的話,Provider繼承自IntentTimelineProvider支持用戶自主編輯,沒(méi)有勾選則繼承自TimelineProvider不支持用戶自主編輯。這個(gè)以后會(huì)講到。


struct Provider: TimelineProvider {
    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)
    }
}

getSnapshot 方法是提供一個(gè)預(yù)覽數(shù)據(jù),可以讓用戶看到該組件的一個(gè)大致情況,是長(zhǎng)什么樣、顯示什么數(shù)據(jù)的,可以寫成固定數(shù)據(jù),國(guó)外的文章里叫它
“fake information” ,就是這個(gè)界面顯示的樣子:(以iWidget為例子)

snapshot顯示的地方

getTimeline 方法就是Widget在桌面顯示時(shí)的刷新事件,返回的是一個(gè)Timeline實(shí)例,其中包含要顯示的所有條目:預(yù)期顯示的時(shí)間(條目的日期)以及時(shí)間軸“過(guò)期”的時(shí)間。
因?yàn)閃idget程序無(wú)法像天氣應(yīng)用程序那樣“預(yù)測(cè)”它的未來(lái)狀態(tài),因此只能用時(shí)間軸的形式告訴它什么時(shí)間顯示什么數(shù)據(jù)。

數(shù)據(jù)模型-SimpleEntry

Widget的Model,其中的Date是TimelineEntry的屬性,是保存的是顯示數(shù)據(jù)的時(shí)間,不可刪除,需要自定義屬性在它下面添加即可:

struct SimpleEntry: TimelineEntry {
    public let date: Date
    xxxxx
}

界面-MainWidgetEntryView

Widget顯示的View,在這個(gè)View上編輯界面,顯示數(shù)據(jù),也可以自定義View之后在這里調(diào)用。

struct MainWidgetEntryView : View {
    var entry: Provider.Entry
    var body: some View {
        xxxxxx
    }
}

入口-MainWidget

Widget 的主入口函數(shù),可以設(shè)置Widget的標(biāo)題和說(shuō)明,規(guī)定其顯示的View、Provider、支持的尺寸等信息。

@main
struct MainWidget: Widget {
    let kind: String = "MainWidget"http:// 標(biāo)識(shí)符,不能和其他Widget重復(fù),最好就是使用當(dāng)前Widgets的名字。

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            MainWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")//Widget顯示的名字
        .description("This is an example widget.")//Widget的描述
    }
}

遇到的坑

getTimeline 就是第一個(gè)坑,<mark style="box-sizing: border-box; outline: 0px; background-color: rgb(248, 248, 64); color: rgb(0, 0, 0); overflow-wrap: break-word;">iOS14 Widget是無(wú)法主動(dòng)更新數(shù)據(jù)的?。。?lt;/mark>
Today小組件是可以主動(dòng)獲取最新的數(shù)據(jù),由程序直接控制,但 iOS 14 的小組件卻不是,系統(tǒng)只會(huì)向小組件詢問(wèn)一系列的數(shù)據(jù),并根據(jù)當(dāng)前的時(shí)間將獲取到的數(shù)據(jù)展示出來(lái)。由于代碼不是主動(dòng)運(yùn)行的,這使它更偏向于<mark style="box-sizing: border-box; outline: 0px; background-color: rgb(248, 248, 64); color: rgb(0, 0, 0); overflow-wrap: break-word;">靜態(tài)的</mark>信息展示,連動(dòng)畫和視頻也都是被禁止的。
這就意味著,我們只能提前為小組件寫好下一個(gè)時(shí)間該展示什么數(shù)據(jù),并制作成時(shí)間線,讓系統(tǒng)去讀取展示。但是我們可以通過(guò)以閉包的方式進(jìn)行正常的數(shù)據(jù)請(qǐng)求和填充來(lái)實(shí)現(xiàn)自動(dòng)請(qǐng)求并刷新數(shù)據(jù)。這樣的刷新方式與我平時(shí)開發(fā)時(shí)的思維相差較大,導(dǎo)致我犯了很多錯(cuò)誤。

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)
    }

官方的示例代碼的意思是:顯示從現(xiàn)在開始的5個(gè)小時(shí)的每個(gè)小時(shí)的時(shí)間,再顯示完之后又重新運(yùn)行一次getTimeline。理解了這個(gè)方法的意思后才可以寫出自己想要的效果。
所以,我們只需要控制刷新時(shí)間的Calendar.ComponentValue與entries中元素的個(gè)數(shù),并設(shè)置TimeLinepolicy 就可以控制Widget的刷新時(shí)間,次數(shù)和方法。但是經(jīng)過(guò)我的測(cè)試,getTimeline最高的刷新頻率是5分鐘一次,高于這個(gè)頻率是不起作用的。我們?cè)谔畛?strong>entries時(shí)應(yīng)該為其填充5分鐘內(nèi)需要顯示的數(shù)據(jù)。

例子:實(shí)現(xiàn)一個(gè)按秒刷新的時(shí)鐘,為了每一秒盡可能的準(zhǔn)確刷新就應(yīng)該向entries提供0-299這300秒的300個(gè)時(shí)間數(shù)據(jù),View展示時(shí)轉(zhuǎn)換成具體到秒的字符串展示即可,運(yùn)行一個(gè)周期后再次獲取5分鐘的時(shí)間數(shù)據(jù)。這就導(dǎo)致秒鐘顯示會(huì)有一定的偏差1~3s,高頻率的刷新也會(huì)導(dǎo)致耗電量的增加。

func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
    var currentDate = Date()
    // 每5分鐘刷新一次
    let refreshDate = Calendar.current.date(byAdding: .minute, value: 5, to: currentDate)!
    var arr:[SimpleEntry] = []
    var tempDate = Date()
        for idx in 0...300 {
            tempDate = Calendar.current.date(byAdding: .second, value: idx, to: currentDate)!
            let tempEntry = SimpleEntry(date: tempDate)
            arr.append(tempEntry)
        }
        let timeline = Timeline(entries: arr, policy: .after(refreshDate))
        completion(timeline)
    }

主程序刷新和第二個(gè)坑

在主程序內(nèi),我們可以使用WidgetKit提供的WidgetCenter來(lái)管理小組件,其中的reloadTimelines來(lái)強(qiáng)制刷新一次我們指定的小組件,或者reloadAllTimelines來(lái)刷新所有的小組件。

WidgetCenter.shared.reloadTimelines(ofKind: "xxx")
WidgetCenter.shared.reloadAllTimelines()

如果你的主程序是Oojective-C編寫的,那么你就需要使用OC調(diào)用Swift的方法來(lái)寫,混編配置方式詳見(jiàn)參考文檔 《混編之oc調(diào)用swift》,因?yàn)?strong>WidgetKit沒(méi)有寫OC版本。

import WidgetKit
@objcMembers class WidgetTool: NSObject {
    @available(iOS 14, *)
    @objc func refreshWidget(sizeType: NSInteger) {
        #if arch(arm64) || arch(i386) || arch(x86_64)
        WidgetCenter.shared.reloadTimelines(ofKind: "xxx")
        #endif
    }
}

上面代碼中的

 #if arch(arm64) || arch(i386) || arch(x86_64)
        xxxx
 #endif

@available(iOS 14, *)

就是第二個(gè)坑。
其一,加這個(gè)判斷是因?yàn)?strong>Widget只能在這三個(gè)條件其中的一個(gè)滿足的下運(yùn)行,沒(méi)有加這一句在打包時(shí)會(huì)出現(xiàn)報(bào)錯(cuò),這個(gè)解決方法是從Apple Developer的問(wèn)題反饋中找到的。

其二,WidgetKitiOS 14才新出的,因?yàn)槲覀兊捻?xiàng)目要向下支持到iOS 10,所以要加上版本判斷才能編譯打包。

參考文獻(xiàn)

本人新手,如果有寫錯(cuò)的地方歡迎指正,期待和大家一起交流開發(fā),建議先看完官方的說(shuō)明文檔再去找相關(guān)的網(wǎng)絡(luò)資料。

《Creating a Widget Extension》
《Keeping a Widget Up To Date》
《從開發(fā)者的角度看 iOS 14 小組件》
《iOS14WidgetKit開發(fā)實(shí)戰(zhàn)1-4》
《iOS14 Widget 開發(fā)相關(guān)及易報(bào)錯(cuò)地方處理》
《How to create Widgets in iOS 14 in Swift》
《SwiftUI-Text》
《混編之oc調(diào)用swift》

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容