小组件开发最佳实践(iOS)

更新时间:2023-11-29 03:18:44

应用程序小组件是一个微型的应用程序视图,可以嵌入其他应用程序(例如主屏幕)中并接收定期更新。本文档介绍了开发iOS小组件。

创建证书

iOS中小组件(Widget)是一个独立的应用,可以看做是一个独立的App(宿主App的拓展程序),所以我们需要对Widget单独创建证书。

  1. 创建宿主App证书。

    该部分操作资料很丰富,此处不做介绍,可自行查找相关资料。

  2. 创建Widget证书。

    创建Widget证书的操作与创建宿主App证书的操作类似,但需注意以下几点。

    • WidgetBundle Id是以宿主App为基础扩展的,例如宿主Appcom.companyName.AppName,则Widget的格式应该为com.companyName.AppName.WidgetName配置证书示例

    • 创建证书的时候,需勾选App Group配置项。勾选App Groups

创建Widget

  1. xcode中,选择File > New > Target > Today Extension,创建Today。

    创建today

  2. 查看创建后的目录结构。

    目录结构

  3. 设置Widget工程的开发方式。

    工程默认为storyboard开发方式,如果想使用纯代码方式,则需要进行以下操作。

    TodayWidget > Info.plist > Extension中,删除NSExtensionMainStoryboard选项 ,增加NSExtensionPrincipalClass选项,value为类的名字IMSWidgetTestViewController,如下图所示。

    配置示例

  4. 设置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;
        }
    }
    说明
    • 展开的高度可以自行设置,不超过系统最大值即可。

    • 系统不支持折叠高度的修改。

  5. 刷新数据,建议使用系统提供的方法,示例如下。

    - (void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult))completionHandler {
        completionHandler(NCUpdateResultNewData);
    }
    说明

    此处刷新有可能执行失败,这是目前Apple存量的问题,可通过延迟来解决。详细请参见延时的原因临时解决方案

  6. 配置小组件与宿主App的跳转功能。

    Extension和宿主App是两个完全独立的进程,它们之间不能直接通信(即无法通过单击应用内部按钮跳转到指定页面)。为了实现Widget调起宿主App,这里通过openURL的方式来启动宿主App。

    1. 在宿主App里选择Targets > MCWidgetDemo > Info > Url Types,添加URL Schemes。

      下图为设置示例,设置URL SchemesTodayWidget

      配置URL Schemes

    2. 配置代码跳转地址(openURL)。完整地址为:”URL Schemes” + “://” + “宿主App Bundle Id”,如下图所示。

      配置跳转地址

设置Widget和宿主App交互通信

因为Widget的独立性,宿主App要与Widget之间相互通信,需要通过App Group来实现。

  1. 创建App Group。

    前往开发者网站注册一个App Group,填入名字和id,并根据界面提示操作,即可得到下图类似的App Group。App Group

  2. Target > Signing & Capabilities > App Group下,配置App Group。

    在宿主App和扩展程序(Widget)的App Group中,分别设置group名称。需确保宿主AppWidgetgroupName相同,并且与在开发者网站注册的App Groups保持一致。配置示例

  3. 配置Widget和宿主App之间交互通信。

    使用NSUserDefaults或者NSFileManager方式都可以实现Widget和宿主App之间交互通信。此处介绍如何使用NSUserDefaults方式实现交互通信。

    • 存数据存数据

    • 取数据取数据

生活物联网平台SDK使用指导

下面主要介绍TodayExtension的开发过程,其余Widget开发请参照Apple官方文档自行完成。

  1. 引入SDK。

    1. 设置Profile。

      iOS推荐使用Cocoapods引入,分别对宿主App TargetWidget 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
    2. 查看小组件开发必备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'
    3. 执行pod update,并编译工程。

      编译成功后,选择Widgettarget,运行小组件工程。

      说明

      因为Widget的独立,安全图片也需要导入一份到WidgetTarget下,否则会报错。

  2. 初始化宿主App配置。

    1. 初始化宿主AppIMSAuthentication,示例代码如下。

      // 设置需要更新的Credential至AppGroup中
      [[IMSCredentialManager sharedManager] addCredentialStoreWithAppGroupName:AppGroupName];
    2. ApiClient的信息,写入对应的AppGroup共享区域中。

      // 宿主App初始化IMSApiClient完成后,把ApiClient的信息,写入到对应的AppGroup共享区域中
      [[IMSConfiguration sharedInstance] storeConfigToAppGroup:AppGroupName];
  3. 配置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];
    }
  4. 调用接口,示例代码如下。

     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");
                }
    
            }];
        }];
  5. 实时判断宿主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 {
            // 未登录
        }
    }
  6. 配置小组件显示名称的多语言。

    1. 使用宿主App [IMSConfiguration sharedInstance].language更新语言信息,信息需要重新保存到group中。

      // 把ApiClient的信息,写入到对应的AppGroup共享区域中
      [[IMSConfiguration sharedInstance] storeConfigToAppGroup:AppGroupName];
    2. 设置多语言。

      选中TodayExtensionTarget,选择 New > File > String File,新建 strings 文件(名称请使用InfoPlist)。

      创建完成后,选中InfoPlist.strings文件,单击Localize,添加多语言。

      设置多语言

      多语言设置后的界面如下。

      多语言

    3. 更改小组件的显示名称。

      选中某种语言,修改该语言下小组件的显示名称(小组件名字是系统语言控制的,这个不随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;
      }
      @end3】增加代理监听、增加属性
      @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];
      }
  • 本页导读 (0)
  • 创建证书
  • 创建Widget
  • 设置Widget和宿主App交互通信
  • 生活物联网平台SDK使用指导
  • 设备小组件&场景小组件接口文档和调用过程