首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

iOS14新特性-WidgetKit开发与实践

iOS14新特性-WidgetKit小部件 通过桌面编辑添加,将小部件放在iOS主屏幕或macOS通知中心上,使用户可以随时访问应用中的内容。同时小部件也可以保持更新,因此用户始终可以一目了然地获得最新信息,同时点击区域可以Deep Link跳转到主APP任意界面中。

在2020年苹果发布会推出Widget之后,贝壳就第一时间做出了尝试, 期间苹果中国提供了很多支持与帮助,目前已在贝壳和链家APP上线。

小部件具有三种不同的大小(小,中和大),可以显示各种信息。用户可以个性化小部件以查看特定于其需求的详细信息,并以最适合他们的方式安排其小部件。

不同分辨率机型,三种卡片的尺寸也不同:

1 如何开发WidgetKit?

前期准备:

Xcode 12 及Bate版, iOS 14 及Bate版, 了解SwiftUI控件

1.1 创建Widget

首先,File->New->Target:

有两种配置可供选择:

  • StaticConfiguration: 对于一个没有用户可配置属性的Widget。

例如,显示一般市场信息的股票市场Widget,或显示趋势标题的新闻Widget。

  • IntentConfiguration: 对于一个具有用户可配置属性的Widget来说,你可以使用SiriKit自定义意图来定义属性。您使用 SiriKit 自定义意图来定义属性。

例如,一个天气Widget需要一个城市的邮政编码或邮政编码,或者一个包裹跟踪Widget需要一个跟踪号码。

下图中「Include Configuration Intent」复选框决定了Xcode使用哪种配置。选择Include Configuration Intent 表示支持用户配置;不需要,则不勾选。

1.2 Widget初始化配置

对象解析

  • kind

识别Widget的字符串。

如果包含多个widget后可作为唯一的标识符。

  • Provider

符合TimelineProvider的对象。

一个符合TimelineProvider的对象,它能产生一个时间线,告诉WidgetKit何时渲染Widget。

时间线包含一个你定义的自定义TimelineEntry类型。

时间线条目标识了你希望WidgetKit更新Widget内容的日期。

在自定义类型中包含你的Widget的视图需要渲染的属性。

  • Placeholder

一个 SwiftUI 视图,WidgetKit 用来在第一次渲染Widget。

占位符是您的Widget的通用表示,没有特定的配置或数据。

  • Content Closure(内容闭合)

一个包含SwiftUI视图的封闭。

WidgetKit调用它来渲染Widget的内容,从提供者那里传递一个TimelineEntry参数。

函数解析

1) placeholder

占位视图,在数据加载前展示,在xcode12 bate3和bate4中有所有所变化:

代码语言:javascript
复制
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date(), model: LJWidgetModel.preview_widget)
        completion(entry)
}

2) getSnapshot

快照,在添加组件库中展示

代码语言:javascript
复制
 func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) {
        let date = Calendar.current.date(byAdding: .hour, value:12, to: Date()) ?? Date()
        LJWidgetAPI.loadData{ (model, error) in
            guard let model = model else {
                let timeline = Timeline(entries: [SimpleEntry(date: date, model: LJWidgetModel.preview_widget)], policy: .after(date))
                completion(timeline)
                return
            }
            let timeline = Timeline(entries: [SimpleEntry(date: date, model: model)], policy: .after(date))
            completion(timeline)
        }
    }

3) getTimeline

时间轴,控制刷新时机

代码语言:javascript
复制
struct SimpleEntry: TimelineEntry {
    let date: Date
    let model: LJWidgetModel
}

4) SimpleEntry

数据模型,类似model

代码语言:javascript
复制
struct SimpleEntry: TimelineEntry {    let date: Date    let model: LJWidgetModel}

5) WidgetEntryView

主内容,展示区分小中大卡片 ,可根据family来区分

代码语言:javascript
复制
struct LJWidgetEntryView : View {
    var entry: Provider.Entry
    @Environment(\.widgetFamily) var family


    @ViewBuilder
    var body: some View {


        switch family {
        case .systemSmall:
            let small = entry.model.small
            LJWidgetSmall(small)
                .previewLayout(.sizeThatFits)
        case .systemMedium:
            let medium = entry.model.medium
           LJWidgetMedium(medium)
            .previewLayout(.sizeThatFits)


        case .systemLarge:
            let large = entry.model.large
            LJWidgetLarge(large)
                .previewLayout(.sizeThatFits)


        @unknown default:
            Text("unknown")
        }
    }
}

6) Widget

主界面控制器。kind为标识符

代码语言:javascript
复制
struct LJWidget: Widget {
    let kind: String = "LJWidget"
    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            LJWidgetEntryView(entry: entry)
        }
      .configurationDisplayName("My Widget")
        .description("This is an example widget.")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
    }
}
Widget_Previews  struct LJWidgetLarge_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            LJWidgetLarge(LJWidgetLargeModel.preview)
                .frame(width: 329.0, height: 345.0)
                .previewLayout(.sizeThatFits)
                .colorScheme(.light)
            LJWidgetLarge(LJWidgetLargeModel.preview)
                .frame(width: 329.0, height: 345.0)
                .previewLayout(.sizeThatFits)
                .colorScheme(.dark)
        }
    }

Preview 预览

界面开发是SwiftUI (Apple要求),可参考:https://developer.apple.com/documentation/widgetkit/creating-a-widget-extension

1.3 Widget刷新机制

  • 自动刷新

如下图,本身维护一个时间轴,在创建时填充不同时间节点,当到达时间节点位置时,触发刷新。after会在消耗完时间点后,再次填充,保持循环运行。

  • 手动刷新

一种是push notification推送来更新widget,另一种则是客户端内通过调用接口主动 reload。

2 OC项目和Swift混编

如果原有老项目为OC项目,想要实现和Swift的相互调用,需用到桥接文件,另外针对引入Swift依赖库,会引起包体积增加,大小大概7~8M左右,可以在ipa包内查看:

2.1 Swift引用OC代码

创建xxx(工程名)-Bridging-Header头文件, 并在Build Setting -> Objective-C Bridging Header 设置其路径

这样我们在桥接文件内,通过 import “xxx.h”引用OC的组件库,就可以在Swift使用了。

2.2 OC引用Swift代码

桥接文件不需要手动创建,系统帮我创建好了,可以查看xxx-swift,在文件夹查找不到,但是引用头文件后,可以点击进入查看:

3 主APP和Widget间通信

因为Widget为新的Target项目,和之前主项目是两个进程的关系,所以如果想要资源共享,比如登录状态值token, 网络配置等,就需要用到共享区域来实现通信。苹果提供的共享方式有:

方式一:APPGroup 方式

1) 配置好证书,是Gourp功能正常使用中,主APP和Widget保持统一key

当我们配置完以后,会在文件目录下多出来一个.entitlements的文件。

2)主APP写下数据

代码语言:javascript
复制
//Main App 通过TextField来向共享文件appGroup.txt中写入数据
- (void)textFieldDidEndEditing:(UITextField *)textField {
   //获取App Group的共享目录    
NSURL *groupURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.com.simon.app.test"];   
 NSURL *fileURL = [groupURL URLByAppendingPathComponent:@"appGroup.txt"];  //写入文件   
 [textField.text writeToURL:fileURL atomically:YES encoding:NSUTF8StringEncoding error:nil];}

3)Widget 内读取App Group的共享目录

代码语言:javascript
复制
NSURL *groupURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.com.simon.app.test"];   
NSURL *fileURL = [groupURL URLByAppendingPathComponent:@"appGroup.txt"];    //读取文件     
NSString *str = [NSString stringWithContentsOfURL:fileURL encoding:NSUTF8StringEncoding error:nil];   self.shareLabel.text = str;

方式二:KeyChain Sharing

KeyChain可将用户信息加密存储在钥匙串中,保证用户信息的安全性;另外多个应用可通过keyChain共享用户信息。

1)同样我们需要配置keychan的key, 保持和主APP一致,省事的是少了证书的配置

2)通过对钥匙串的读写来操作 (一般由SAKeychain库管理)比较方便,注意一点:实现共享的只是在钥匙串的缓存数据,如果一旦加载到内存中,它的修改不受主APP的影响了。

4 常见问题

问题1 . 选择Intent (widget 配置),会出现configtion查找不到

解决:

方法一: 若不使用Inetent, 在生成widget时不勾选即可,避免这类问题;

方法二:见https://developer.apple.com/forums/thread/653910 (在官网提出后,目前没有官方人员回答,不过有其他答案,未验证)

问题2 . Xcode 12 bate3 运行贝壳的widget未显示preview

解决:Build systems: 选择新的编译方式

问题3 . 由于WidgetKit使用一些Swift的新特性,所以版本需要修改成Swift5.0

问题4 . Swift 桥接OC组件库

解决:

1) 生成LJShell-Bridging-Header.h桥接文件,

2)在 Build setting中找到Objective-C Bridging Header 设置对应路径 $(SRCROOT)/LianJiaShell/LJShell-Bridging-Header.h

3)将 Build Settings 中的 Defines Module 选项设置为 YES

问题5 . 引入OC组件库报查找不到

解决:在podfile中在LJWidget的target引入对应 pod xxx

问题6 . 使用Widget Preview功能

bate版本目前发现在OC工程下混编情况,prview无法使用,可以尝试新建个swift工程来编写swiftUI,使用preview功能

问题7 . Error: Multiple commands produce

解决:

方法一:

不使用New Build System,在File > Project/Workspace Settings中的Share Project/Workspace Settings 里build system 将New Build System(Default)切换成Legacy build system。

方法二:在 target -> Build phase > Copy Bundle Resource 中找到info.plist,移除

问题8 . dyld: Library not loaded:

代码语言:javascript
复制
dyld: Library not loaded: /System/Library/Frameworks/WidgetKit.framework/WidgetKit

  Referenced from: /var/containers/Bundle/Application/EA42E025-6CFA-4C90-950E-50D28255B4DA/LJShell.app/LJShell

  Reason: image not found

解决:https://developer.apple.com/forums/thread/126506

5 参考文献

  • WidgetKit:

https://developer.apple.com/documentation/widgetkit

  • SwiftUI:

https://developer.apple.com/tutorials/swiftui/creating-and-combining-views

本文转载自公众号贝壳产品技术(ID:beikeTC)。

原文链接

iOS14新特性-WidgetKit开发与实践

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/78C4JjbgNMpHZWEJiD0m
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券