应用程序小组件是一个微型的应用程序视图,可以嵌入其他应用程序(例如主屏幕)中并接收定期更新。本文档介绍了开发iOS小组件。
创建证书
iOS中小组件(Widget)是一个独立的应用,可以看做是一个独立的App(宿主App的拓展程序),所以我们需要对Widget单独创建证书。
创建宿主App证书。
该部分操作资料很丰富,此处不做介绍,可自行查找相关资料。
创建Widget证书。
创建Widget证书的操作与创建宿主App证书的操作类似,但需注意以下几点。
Widget的Bundle Id是以宿主App为基础扩展的,例如宿主App为
com.companyName.AppName
,则Widget的格式应该为com.companyName.AppName.WidgetName
。创建证书的时候,需勾选App Group配置项。
创建Widget
在xcode中,选择 ,创建Today。
查看创建后的目录结构。
设置Widget工程的开发方式。
工程默认为storyboard开发方式,如果想使用纯代码方式,则需要进行以下操作。
在NSExtensionMainStoryboard选项 ,增加NSExtensionPrincipalClass选项,value为类的名字IMSWidgetTestViewController,如下图所示。
中,删除设置Widget的展开与折叠效果,示例如下。
- (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; if (/*折叠展开判断 */) { self.extensionContext.widgetLargestAvailableDisplayMode = NCWidgetDisplayModeExpanded; } else { self.extensionContext.widgetLargestAvailableDisplayMode = NCWidgetDisplayModeCompact; } } - (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode withMaximumSize:(CGSize)maxSize { switch (activeDisplayMode) { case NCWidgetDisplayModeCompact: { self.preferredContentSize = maxSize; break; } case NCWidgetDisplayModeExpanded: { self.preferredContentSize = CGSizeMake(self.view.bounds.size.width, 210); break; } default: break; } }
说明展开的高度可以自行设置,不超过系统最大值即可。
系统不支持折叠高度的修改。
刷新数据,建议使用系统提供的方法,示例如下。
配置小组件与宿主App的跳转功能。
Extension和宿主App是两个完全独立的进程,它们之间不能直接通信(即无法通过单击应用内部按钮跳转到指定页面)。为了实现Widget调起宿主App,这里通过openURL的方式来启动宿主App。
在宿主App里选择 ,添加URL Schemes。
下图为设置示例,设置URL Schemes为TodayWidget。
配置代码跳转地址(openURL)。完整地址为:”URL Schemes” + “://” + “宿主App Bundle Id”,如下图所示。
设置Widget和宿主App交互通信
因为Widget的独立性,宿主App要与Widget之间相互通信,需要通过App Group来实现。
创建App Group。
前往开发者网站注册一个App Group,填入名字和id,并根据界面提示操作,即可得到下图类似的App Group。
在 下,配置App Group。
在宿主App和扩展程序(Widget)的App Group中,分别设置group名称。需确保宿主App和Widget的groupName相同,并且与在开发者网站注册的App Groups保持一致。
配置Widget和宿主App之间交互通信。
使用NSUserDefaults或者NSFileManager方式都可以实现Widget和宿主App之间交互通信。此处介绍如何使用NSUserDefaults方式实现交互通信。
存数据
取数据
生活物联网平台SDK使用指导
下面主要介绍TodayExtension的开发过程,其余Widget开发请参照Apple官方文档自行完成。
引入SDK。
设置Profile。
iOS推荐使用Cocoapods引入,分别对宿主App Target和Widget Target引入SDK。因为Widget是独立的应用,所以两个Target都需要各自引入编译所需的SDK,多个小组件,就配置多份Profile。配置示例如下。
target “WidgetTargetName1” do pod 'IMSApiClient', '1.6.0' pod 'IMSAuthentication', '1.4.1' end target “WidgetTargetName2” do pod 'IMSApiClient', '1.6.0' pod 'IMSAuthentication', '1.4.1' end
查看小组件开发必备SDK列表。
小组件开发必备SDK列表 【1】通用请求SDK pod 'IMSApiClient', '1.6.0' pod 'IMSAuthentication', '1.4.1' 【2】设备小组件相关SDK # 物 pod 'IMSThingCapability', '1.7.5' # 长连接 pod 'IMSMobileChannel', '1.6.7'
执行pod update,并编译工程。
编译成功后,选择Widget的target,运行小组件工程。
说明因为Widget的独立,安全图片也需要导入一份到Widget的Target下,否则会报错。
初始化宿主App配置。
初始化宿主App的IMSAuthentication,示例代码如下。
// 设置需要更新的Credential至AppGroup中 [[IMSCredentialManager sharedManager] addCredentialStoreWithAppGroupName:AppGroupName];
把ApiClient的信息,写入对应的AppGroup共享区域中。
// 宿主App初始化IMSApiClient完成后,把ApiClient的信息,写入到对应的AppGroup共享区域中 [[IMSConfiguration sharedInstance] storeConfigToAppGroup:AppGroupName];
配置TodayExtension,配置示例代码如下。
+ (void)initialize { // 初始化APIClient [IMSConfiguration initWithAppGroupName:AppGroupName]; // 初始化身份认证 [IMSCredentialManager initWithAppGroupName:AppGroupName]; // 注册RequestClient的代理 IMSIoTAuthentication *iotAuthDelegate = [[IMSIoTAuthentication alloc] initWithCredentialManager:IMSCredentialManager.sharedManager]; [IMSRequestClient registerDelegate:iotAuthDelegate forAuthenticationType:IMSAuthenticationTypeIoT]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; // 根据AppGroup共享区域存储的信息 // 防止出现未打开宿主App、已经初始化Extension的APIClient [[IMSConfiguration sharedInstance] synchronizeConfigFromAppGroup]; // 从UserDefaults更新Credential [[IMSCredentialManager sharedManager] synchronizeCredentialFromAppGroup]; }
调用接口,示例代码如下。
IMSIoTRequestBuilder *builder = [[IMSIoTRequestBuilder alloc] initWithPath:@"/uc/path/xxxx" apiVersion:@"1.0.0" params:@{}]; [builder setScheme:@"https"]; IMSRequest *request = [[builder setAuthenticationType:IMSAuthenticationTypeIoT] build]; __weak typeof(self) weakSelf = self; [IMSRequestClient asyncSendRequest:request responseHandler:^(NSError * _Nullable error, IMSResponse * _Nullable response) { if (response.code == 401) { [self loginOut]; } if (error) { NSLog(@"request error = %@",error); } else { NSLog(@"request success"); } }]; }];
实时判断宿主App登录状态,示例代码如下。
- (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; // 根据AppGroup共享区域存储的信息、配置Host、环境、语言、安全图片 // 防止出现未打开宿主App、已经初始化Extension的APIClient [[IMSConfiguration sharedInstance] synchronizeConfigFromAppGroup]; // 从UserDefaults更新Credential [[IMSCredentialManager sharedManager] synchronizeCredentialFromAppGroup]; // 通过Credential是否存在来判断登录态 if ([IMSCredentialManager sharedManager].credential) { // 已登录 } else { // 未登录 } }
配置小组件显示名称的多语言。
使用宿主App的
[IMSConfiguration sharedInstance].language
更新语言信息,信息需要重新保存到group中。// 把ApiClient的信息,写入到对应的AppGroup共享区域中 [[IMSConfiguration sharedInstance] storeConfigToAppGroup:AppGroupName];
设置多语言。
选中TodayExtension的Target,选择
,新建 strings 文件(名称请使用InfoPlist)。创建完成后,选中InfoPlist.strings文件,单击Localize,添加多语言。
多语言设置后的界面如下。
更改小组件的显示名称。
选中某种语言,修改该语言下小组件的显示名称(小组件名字是系统语言控制的,这个不随App更改)。
设备小组件&场景小组件接口文档和调用过程
设备小组件和场景小组件在开发过程中使用的接口文档(参见场景服务)和调用示例如下。
宿主App相关的接口
场景小组件
【1】获取已经被添加到小组件的场景list path:/living/appwidget/list version:1.0.0 params:@{} 【2】全量场景查询 path:/living/scene/query version:1.0.1 params = @{@"catalogId": @"0", @"pageNo": @(pageNo), @"pageSize": @(pageSize) } 【3】更新场景小组件 path:/living/appwidget/create version:1.0.0 params = @{@"sceneIds": @[]}
设备小组件
【1】获取已经被加到小组件的设备list path:/iotx/ilop/queryComponentProduct version:1.0.0 params:@{} 【2】获取设备的属性列表(目前属性多语言需要入参时传递) path:/iotx/ilop/queryComponentProperty version:1.0.0 params = @{@"productKey":productKey, @"iotId":iotId, @"query":@{@"dataType":@"BOOL”, @"I18Language":@"zh-CN"} } 【3】小组件列表更新 path:/iotx/ilop/updateComponentProduct version:1.0.0 params:更改后的设备list
TodayExtension相关接口
场景小组件
【1】获取已经被添加到小组件的场景list path:/living/appwidget/list version:1.0.0 params:@{} 【2】执行场景 path:/scene/fire version:1.0.1 params:@{@"sceneId":sceneId}
设备小组件
【1】获取已经被加到小组件的设备list path:/iotx/ilop/queryComponentProduct version:1.0.0 params:@{} 【2】设备小组件,有本地通信和云端通信逻辑,需要集成宿主APP中的长连接绑定 & 订阅,监听长连接正常连接 【3】设备状态变更,需要自行定位/thing/properties 和 /thing/status 的topic,监听状态变更,刷新UI 【4】选中设备,指定ThingShell设置设备属性,通过物的模型,变更属性 【5】如果订阅过Topic,设置【4】成功后,也会收到云端的状态变更通知
设备小组件核心参考代码
【1】长连接绑定 & 订阅(相关SDK参见长连接通道SDK) IMSConfiguration * imsconfig = [IMSConfiguration sharedInstance]; LKAEConnectConfig * config = [LKAEConnectConfig new]; config.appKey = imsconfig.appKey; config.authCode = imsconfig.authCode; // 指定长连接服务器地址。 (默认不填,SDK会使用默认的地址及端口。默认为国内华东节点。不要带 "协议://",如果置为空,底层通道会使用默认的地址) config.server = @"" // 开启动态选择Host功能。 (默认 NO,海外环境请设置为 YES。此功能前提为 config.server 不特殊指定。) config.autoSelectChannelHost = NO; [[LKAppExpress sharedInstance]startConnect:config connectListener:self];// self 需要实现 LKAppExpConnectListener 接口 } 【2】注册下行Listener #pragma mark - 注册下行Listener static NSString *const IMSiLopExtensionDidReceiveUpdateAttributeSuccess = @"LAMPPANEL_DIDRECEIVE_UPDATE_ATTRIBUTE_SUCCESS"; static NSString *const IMSiLopExtensionDidReceiveUpdateDeviceStateSuccess = @"LAMPPANEL_DIDRECEIVE_UPDATE_DEVICE_STATE_SUCCESS"; @class TodayViewController; @interface IMSWidgetDeviceListener : NSObject <LKAppExpDownListener> @end @implementation IMSWidgetDeviceListener - (void)onDownstream:(NSString * _Nonnull)topic data:(id _Nullable)data { IMSAppExtensionLogVerbose(@"小组件 onDownstream topic : %@", topic); IMSAppExtensionLogVerbose(@"小组件 onDownstream data : %@", data); NSDictionary * replyDict = nil; if ([data isKindOfClass:[NSString class]]) { NSData * replyData = [data dataUsingEncoding:NSUTF8StringEncoding]; replyDict = [NSJSONSerialization JSONObjectWithData:replyData options:NSJSONReadingMutableLeaves error:nil]; } else if ([data isKindOfClass:[NSDictionary class]]) { replyDict = data; //这里添加云端处理! if (data) { if ([topic isEqualToString:@"/thing/properties"]) { [[NSNotificationCenter defaultCenter] postNotificationName:IMSiLopExtensionDidReceiveUpdateAttributeSuccess object:self userInfo:data]; } if ([topic isEqualToString:@"/thing/status"]) { [[NSNotificationCenter defaultCenter] postNotificationName:IMSiLopExtensionDidReceiveUpdateDeviceStateSuccess object:self userInfo:data]; } } } if (replyDict == nil) { return; } } - (BOOL)shouldHandle:(NSString * _Nonnull)topic { // 需要设什么topic,返回什么topic if ([topic isEqualToString:@"/thing/properties"] || [topic isEqualToString:@"/thing/status"]) { return YES; } return NO; } @end 【3】增加代理监听、增加属性 @interface IMSWidgetDeviceController () < LKAppExpConnectListener> // 本地控制 @property (nonatomic, strong) IMSWidgetDeviceListener *imsWidgetDeviceListener; 【4】在ViewDidLoad 中增加监听 - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view from its nib. // 监听云端设备属性变更 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(dididReceiveUpdateAttributeNoti:) name:IMSiLopExtensionDidReceiveUpdateAttributeSuccess object:nil]; // 监听云端设备状态变更 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(dididReceiveUpdateDeviceStateNoti:) name:IMSiLopExtensionDidReceiveUpdateDeviceStateSuccess object:nil]; } 【5】自行处理下行通知 // 云端属性数据下发 - (void)dididReceiveUpdateAttributeNoti:(NSNotification *)info { } // 云端状态数据下发 - (void)dididReceiveUpdateDeviceStateNoti:(NSNotification *)info { } 【6】更改属性方法 IMSThing *thingShell = [kIMSThingManager buildThing:iotId]; [[thingShell getThingActions] setProperties:@{propertyIdentifierName:value} responseHandler:^(IMSThingActionsResponse * _Nullable response) { if (response.success) { // 成功 } else { // 失败 } }]; 【7】释放资源 - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; // 移除长链接相关 [[LKAppExpress sharedInstance] removeConnectListener:self]; [[LKAppExpress sharedInstance] removeDownStreamListener:self.imsWidgetDeviceListener]; } - (void)dealloc { [kIMSThingManager destroyThing:self.thingShell]; [[NSNotificationCenter defaultCenter] removeObserver:self]; }