### iOS14 Widget开发踩坑(一)修正版-初识与刷新

iOS14 Widget开发踩坑(一)修正版-初识与刷新

前言

转载:写程序的柠檬精 原文

2020年10月3日修正版
在对Widget进行开发了一个月后,解决了几个问题,对本文进行重新编辑以纠正以前的错误和适应最新版本。

2020年10月15日修正版
对刷新和视图有了新的理解,修改刷新部分。

这里记录一些我在开发的过程中遇到的一些坑,希望对开发有用。本文涉及到的代码都只是示例代码,仅提供思路,并不能直接复制使用,需要有一些开发Today Widget的知识,方便进行对比。本文部分内容引用自网络,如有侵权请联系删除。

开发须知

  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进行开发</mark>,所以需要SwiftUI和Swift基础。
  2. Widget只支持3种尺寸systemSmall (2x2)、 systemMedium (4x2)、 systemLarge(4x4)
  3. 默认点击Widget打开主应用程序
  4. Widget类似于TodayWidget是一个独立运行的程序,需要在项目中进行 App Groups 的设置才能使其与主程序互通数据,这个以后会讲。
  5. Apple官方已经弃用Today Extension,Xcode12已经不再提供Today Extension的添加,已经有Today Widget的应用则会显示到一个特定的区域进行展示。

准备工作

部署环境

Widget的开发需要安装Xcode 12以及iOS 14进行。Apple官方下载链接

创建项目

正常的创建项目流程,我使用的是Swift语言、界面Storyboard,可以设置成自己习惯的配置,
Create a new Xcode project -> 填写Product Name-> Next-> Create

作为一个ios开发者,遇到问题的时候,有一个学习的氛围跟一个交流圈子特别重要,对自身有极大帮助,众人拾柴火焰高 这是一个我的iOS交流群:711315161,进群密码iOS 分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!希望帮助开发者少走弯路。

引入Widget Extension

  1. File -> New -> target-> Widget Extension ->Next
  2. 由于是加入一个新的Target,所以Widget的名字不能与项目名相同,也不能起成“Widget”(因为Widget是一个已有的类名),删除时不能只是删除文件还要在项目的Targets中删除,起已经删除过一次的名字会报找不到文件的错误。
  3. 如果 Widget 支持用户配置属性(例如天气组件,用户可以选择城市),就需要勾选Include Configuration Intent这个选项,不支持的话不用勾选。建议勾选上,谁知道以后会不会要求支持呢。
  4. 创建后,会自动生成5个struct和自带的方法

开始编写

认识代码

预览视图-Previews

代码运行的预览视图是SwiftUI新特性,会将运行成果显示在右边的视图上且支持热更新,但是会很卡,它不是Widget的必须部分,可以直接将其删除或注释。

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

数据提供-Provider

Provider是Widget最重要的部分,它决定了小组件的placeholder/getSnapshot/getTimeline这三种数据的显示。在项目创建时勾选了Include Configuration Intent后的话,Provider继承自IntentTimelineProvider支持用户自主编辑,没有勾选则继承自TimelineProvider不支持用户自主编辑。这个以后会讲到。


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 方法是提供一个预览数据,可以让用户看到该组件的一个大致情况,是长什么样、显示什么数据的,可以写成固定数据,国外的文章里叫它
“fake information” ,就是这个界面显示的样子:(以iWidget为例子)

snapshot显示的地方

getTimeline 方法就是Widget在桌面显示时的刷新事件,返回的是一个Timeline实例,其中包含要显示的所有条目:预期显示的时间(条目的日期)以及时间轴“过期”的时间。
因为Widget程序无法像天气应用程序那样“预测”它的未来状态,因此只能用时间轴的形式告诉它什么时间显示什么数据。

数据模型-SimpleEntry

Widget的Model,其中的Date是TimelineEntry的属性,是保存的是显示数据的时间,不可删除,需要自定义属性在它下面添加即可:

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

界面-MainWidgetEntryView

Widget显示的View,在这个View上编辑界面,显示数据,也可以自定义View之后在这里调用。

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

入口-MainWidget

Widget 的主入口函数,可以设置Widget的标题和说明,规定其显示的View、Provider、支持的尺寸等信息。

@main
struct MainWidget: Widget {
    let kind: String = "MainWidget"// 标识符,不能和其他Widget重复,最好就是使用当前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 就是第一个坑,<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是无法主动更新数据的!!!</mark>
Today小组件是可以主动获取最新的数据,由程序直接控制,但 iOS 14 的小组件却不是,系统只会向小组件询问一系列的数据,并根据当前的时间将获取到的数据展示出来。由于代码不是主动运行的,这使它更偏向于<mark style="box-sizing: border-box; outline: 0px; background-color: rgb(248, 248, 64); color: rgb(0, 0, 0); overflow-wrap: break-word;">静态的</mark>信息展示,连动画和视频也都是被禁止的。
这就意味着,我们只能提前为小组件写好下一个时间该展示什么数据,并制作成时间线,让系统去读取展示。但是我们可以通过以闭包的方式进行正常的数据请求和填充来实现自动请求并刷新数据。这样的刷新方式与我平时开发时的思维相差较大,导致我犯了很多错误。

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

官方的示例代码的意思是:显示从现在开始的5个小时的每个小时的时间,再显示完之后又重新运行一次getTimeline。理解了这个方法的意思后才可以写出自己想要的效果。
所以,我们只需要控制刷新时间的Calendar.ComponentValue与entries中元素的个数,并设置TimeLinepolicy 就可以控制Widget的刷新时间,次数和方法。但是经过我的测试,getTimeline最高的刷新频率是5分钟一次,高于这个频率是不起作用的。我们在填充entries时应该为其填充5分钟内需要显示的数据。

例子:实现一个按秒刷新的时钟,为了每一秒尽可能的准确刷新就应该向entries提供0-299这300秒的300个时间数据,View展示时转换成具体到秒的字符串展示即可,运行一个周期后再次获取5分钟的时间数据。这就导致秒钟显示会有一定的偏差1~3s,高频率的刷新也会导致耗电量的增加。

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

主程序刷新和第二个坑

在主程序内,我们可以使用WidgetKit提供的WidgetCenter来管理小组件,其中的reloadTimelines来强制刷新一次我们指定的小组件,或者reloadAllTimelines来刷新所有的小组件。

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

如果你的主程序是Oojective-C编写的,那么你就需要使用OC调用Swift的方法来写,混编配置方式详见参考文档 《混编之oc调用swift》,因为WidgetKit没有写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, *)

就是第二个坑。
其一,加这个判断是因为Widget只能在这三个条件其中的一个满足的下运行,没有加这一句在打包时会出现报错,这个解决方法是从Apple Developer的问题反馈中找到的。

其二,WidgetKitiOS 14才新出的,因为我们的项目要向下支持到iOS 10,所以要加上版本判断才能编译打包。

参考文献

本人新手,如果有写错的地方欢迎指正,期待和大家一起交流开发,建议先看完官方的说明文档再去找相关的网络资料。

《Creating a Widget Extension》
《Keeping a Widget Up To Date》
《从开发者的角度看 iOS 14 小组件》
《iOS14WidgetKit开发实战1-4》
《iOS14 Widget 开发相关及易报错地方处理》
《How to create Widgets in iOS 14 in Swift》
《SwiftUI-Text》
《混编之oc调用swift》

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容