• iOS:应用程序扩展开发之Today扩展(Today Extesnsion)


    一、简介

    iOS应用程序扩展是苹果在iOS8推出的一个新特性,可以将自定义的功能和内容扩展到应用程序之外,在之后又经过不断地优化和更新,已经成为开发中不可或缺的功能之一。扩展也是一个Target项目,它运行在主机应用程序上,可以与主机应用程序实现资源共享,和宿主应用程序的Target项目是彼此独立的。系统提供的扩展有很多,Toady扩展就是其中之一,也被成为应用程序插件,它的作用是将今日发生的简单消息展示在系统的插件界面上。Toady扩展模板名称为Today Extension。图1是创建Today扩展,图2是扩展显示在插件界面上(可以通过点击Edit来添加或者移除扩展)。 

    二、创建

    按照上图1的方式创建一个Today Extension的Target后,系统会默认帮我们生成一个TodayViewController控制器类、MainInterface.storyBoard故事板、plist序列化文件,文件结构图如下:

    上图中红色圈内和箭头指向的配置就是系统通过MainInterface.storyBoard帮我们实现了一个基本的Toady插件UI布局,运行后可以直接显示在插件界面上。可是,有的时候开发者并不想使用系统的故事板来构建UI,系统支持自定义的,我们只需要修改plist配置即可。具体的配置是这样的:

    [1] 将NSExtensionMainStoryboard字段删除;

    [2] 添加NSExtensionPrincipalClass字段,修改value为控制器的类名。 

    [3] 在TodayViewController中的ViewDidLoad中设置preferredContentSize属性大小,用来调整widget界面UI的尺寸。

    配置如下图所示:

    //设置尺寸
    self.preferredContentSize = CGSizeMake([UIScreen mainScreen].bounds.size.width, 300); 

      

    三、分析

    TodayViewController类比较简单,就是一个VC类,它实现了系统提供的一个扩展协议<NCWidgetProviding>,可以在协议方法中实现对扩展的更新和状态监控。

    协议如下,都是可选的,开发者根据需要进行重写。

    //协议
    @protocol NCWidgetProviding <NSObject>
    
    @optional
    
    //当数据更新时调用的方法,系统会定期更新扩展
    - (void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult result))completionHandler;
    
    //监听显示模式(宽松型、紧奏型)和尺寸的改变,其中宽松和紧凑表示的是展开和折叠状态, iOS10开始才能使用
    - (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode withMaximumSize:(CGSize)maxSize __API_AVAILABLE(ios(10.0));
    
    //设置扩展UI边距,注意:使用StoryBoard时,若要所见即所得,则这个方法中需要返回UIEdgeInsetsZero; (iOS10 and later 不会再被调用,弃用了)
    - (UIEdgeInsets)widgetMarginInsetsForProposedMarginInsets:(UIEdgeInsets)defaultMarginInsets __API_DEPRECATED("This method will not be called on widgets linked against iOS versions 10.0 and later.", ios(8.0, 10.0));
    
    @end
    //扩展,都是iOS10开始才能使用
    @interface NSExtensionContext (NCWidgetAdditions)
    
    //设置widget折叠或展开状态
    @property (nonatomic, assign) NCWidgetDisplayMode widgetLargestAvailableDisplayMode __API_AVAILABLE(ios(10.0));
    
    //只读,widget状态
    @property (nonatomic, assign, readonly) NCWidgetDisplayMode widgetActiveDisplayMode __API_AVAILABLE(ios(10.0));
    
    //获取widget不同状态的尺寸
    - (CGSize)widgetMaximumSizeForDisplayMode:(NCWidgetDisplayMode)displayMode __API_AVAILABLE(ios(10.0));
    
    @end

    四、交互

    Today扩展是寄宿于主机应用程序上的, TodayViewController又是一个UIViewController类,系统支持Today扩展对UIViewController进行切换。也就是说,苹果在考虑提供给开发者在对UIViewController中添加各种展示控件这种便利的同时,也相应的提供给开发者通过Today扩展的widget从主机应用程序激活并打开宿主应用程序的机会。不过这个操作必须通过设置并调起scheme来实现。步骤如下:

    [1] 配置宿主应用程序的scheme;

    [2] 使用扩展的openURL打开宿主应用程序。

    交互如下:

    //扩展通过scheme打开主宿主应用程序
    [self.extensionContext openURL:[NSURL URLWithString:@"MainApp://"] completionHandler:nil];

    五、数据

    既然Today扩展能与宿主应用程序进行交互,那么肯定就存在数据通信的问题了。扩展与宿主目录应用程序位于不同的目录结构中,默认情况下,扩展与宿主应用程序的数据并不共享,代码也不能复用。例如在宿主目录应用程序中可能有网络请求、数据持久化存储等结构框架,在扩展中不可以直接使用,扩展需要提供自己的网络请求框架、数据持久化框架等。这些问题苹果都提供了解决方法,可以通过创建静态库的方式实现代码共享,通过APP Group和Scheme跳转实现数据共享。这里主要讲一下数据共享。注意:扩展和宿主应用程序的素材文件也是互相独立的,必须将扩展中的素材添加到扩展Target。

    方式一:通过配置scheme跳转来实现数据共享。可以将传递的数据配置到URL中,然后宿主应用程序通过AppDeleagte的代理方法application:openURL:options:获取数据,不过这个数据传递只能是单方向的。

     //打开主应用程序
    -(void)openMainApp {
        
    //共享数据
        NSString *schemeFormat = @"MainApp://action=openCarema?name=xiayuanquan";
        [self.extensionContext openURL:[NSURL URLWithString:schemeFormat] completionHandler:nil];
    }
    -(BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
        
        //从URL获取共享数据,截取数据
        NSLog(@"---------url = %@---------",url);
        
        return YES;
    }

    方式二:给扩展的Target和宿主应用程序的Target项目都开启APP Group,两者配置相同的appgroupIndentifier标识,分别生成后缀名为.entitlements文件。然后对于小数据推荐使用偏好进行双向传递共享数据,如图所示。

    //共享数据
    //使用偏好设置
    NSUserDefaults *defalut = [[NSUserDefaults alloc] initWithSuiteName:@"group.xiayuanquan"];
    [defalut setObject:@"xiayuanquan" forKey:@"name"];
    //从偏好设置获取共享数据
    NSUserDefaults *defalut = [[NSUserDefaults alloc] initWithSuiteName:@"group.xiayuanquan"];
    NSString *name1 = [defalut objectForKey:@"name"];
    NSLog(@"1------------name1=%@",name1);

    方式三:配置跟方式二一样,不过双向传递共享数据使用文件目录来实现。

    //共享数据
    //方式二:使用共享目录
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSURL *baseURL = [fileManager containerURLForSecurityApplicationGroupIdentifier:@"group.xiayuanquan"];
    NSURL *filePath = [baseURL URLByAppendingPathComponent:@"file"];
    NSData *data = [NSKeyedArchiver archivedDataWithRootObject:@"xiayuanquan" requiringSecureCoding:NO error:nil];
    [data writeToURL:filePath atomically:YES];
    //从共享目录获取共享数据
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSURL *baseURL = [fileManager containerURLForSecurityApplicationGroupIdentifier:@"group.xiayuanquan"];
    NSURL *filePath = [baseURL URLByAppendingPathComponent:@"file"];
    NSData *data = [NSData dataWithContentsOfURL:filePath];
    NSLog(@"2------------data=%@",data);

    六、适配

    从iOS10开始,苹果提供了NCWidgetDisplayMode展示模式,通过设置该模式来支持对widget进行折叠和展开。在这里,preferredContentSize就用到了。这个是用来设置widget的尺寸的。苹果对widget的尺寸有自己的标准,width为maxSize.width,height取值范围[110, maxSize.height]。这个maxSize可以在扩展协议<NCWidgetProviding>的协议方法也即widgetActiveDisplayModeDidChange:withMaximumSize中获取:,可以发现每一种机型maxSize不一样。

    // 6s模拟器下:
    // NCWidgetDisplayModeCompact模式下:{359.000000, 110.000000}
    // NCWidgetDisplayModeExpanded模式下:{359.000000, 528.000000}
    
    // 8 plus模拟器下:
    // NCWidgetDisplayModeCompact模式下:{304.000000, 110.000000}
    // NCWidgetDisplayModeExpanded模式下:{304.000000, 616.000000}

    折叠状态:widget的高为110,此时设置preferredContentSize无效; 

    展开状态:widget的高为开发者设置的preferredContentSize.height,但是如果preferredContentSize.height>maxSize.height,此时取值为maxSize.height。 

    适配iOS10,默认支持展开,设置如下: 

    //设置widget默认为可以展开,此时处于折叠状态
    #ifdef __IPHONE_10_0 //适配iOS10
       self.extensionContext.widgetLargestAvailableDisplayMode = NCWidgetDisplayModeExpanded;
    #endif

    七、范例

    【去掉MainInterface.storyBoard,采用纯代码实现】

    1、宿主应用程序AppDelegate

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        // Override point for customization after application launch.
        
        //存储共享数据
        //方式二:使用偏好设置
        NSUserDefaults *defalut = [[NSUserDefaults alloc] initWithSuiteName:@"group.xiayuanquan"];
        [defalut setObject:@"xiayuanquan" forKey:@"name"];
        
        //存储共享数据
        //方式三:使用共享目录
        NSFileManager *fileManager = [NSFileManager defaultManager];
        NSURL *baseURL = [fileManager containerURLForSecurityApplicationGroupIdentifier:@"group.xiayuanquan"];
        NSURL *filePath = [baseURL URLByAppendingPathComponent:@"file"];
        NSData *data = [NSKeyedArchiver archivedDataWithRootObject:@"xiayuanquan" requiringSecureCoding:NO error:nil];
        [data writeToURL:filePath atomically:YES];
        
        return YES;
    }
    
    -(BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
        
        //方式一:从URL获取共享数据,例如参数
        NSLog(@"---------url = %@---------",url);
        
        return YES;
        
    }

    2、Widget扩展TodayViewController

    //
    //  TodayViewController.m
    //  TodayExtension
    //  Created by 夏远全 on 2019/11/19.
    //
    
    #import "TodayViewController.h"
    #import <NotificationCenter/NotificationCenter.h>
    
    @interface TodayViewController () <NCWidgetProviding>
    
    @end
    
    @implementation TodayViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        [self config];
        [self createUI];
        [self fecthData];
    }
    
    //配置
    -(void)config {
        
        self.view.backgroundColor = [UIColor lightGrayColor]; //widget背景色为灰色
        self.preferredContentSize = CGSizeMake([UIScreen mainScreen].bounds.size.width, 300); //widget尺寸大小, 宽度实际取maxSize,width,高度[110, maxSize.height]
        
        //设置widget默认为可以展开,此时处于折叠状态
        #ifdef __IPHONE_10_0 //适配iOS10
            self.extensionContext.widgetLargestAvailableDisplayMode = NCWidgetDisplayModeExpanded;
        #endif
        
    }
    
    //创建UI
    -(void)createUI {
        
        CGFloat width = self.view.frame.size.width;
        CGFloat btnWidth = 100;
        UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake((width-btnWidth)/2, 0, btnWidth, 40)];
        button.backgroundColor = [UIColor greenColor];
        [button setTitle:@"OpenAPP" forState:UIControlStateNormal];
        [button setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
        [button addTarget:self action:@selector(openMainApp) forControlEvents:UIControlEventTouchUpInside];
        [self.view addSubview:button];
    }
    
    //打开主应用程序
    -(void)openMainApp {
        
        //传递共享数据
        //方式一:参数传递
        NSString *schemeFormat = @"MainApp://action=openCarema?name=xiayuanquan";
        [self.extensionContext openURL:[NSURL URLWithString:schemeFormat] completionHandler:nil];
        
    }
    
    //获取共享数据
    -(void)fecthData {
        
        //方式二:从偏好设置获取共享数据
        NSUserDefaults *defalut = [[NSUserDefaults alloc] initWithSuiteName:@"group.xiayuanquan"];
        NSString *name1 = [defalut objectForKey:@"name"];
        NSLog(@"1------------name1=%@",name1);
        
        //方式三:从共享目录获取共享数据
        NSFileManager *fileManager = [NSFileManager defaultManager];
        NSURL *baseURL = [fileManager containerURLForSecurityApplicationGroupIdentifier:@"group.xiayuanquan"];
        NSURL *filePath = [baseURL URLByAppendingPathComponent:@"file"];
        NSData *data = [NSData dataWithContentsOfURL:filePath];
        NSLog(@"2------------data=%@",data);
    }
    
    //当数据更新时调用的方法,系统会定期更新扩展
    - (void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult))completionHandler {
        
        //获取共享的数据,根据判断回调对应的block
        //NCUpdateResultNewData,
        //NCUpdateResultNoData,
        //NCUpdateResultFailed
        
        completionHandler(NCUpdateResultNoData);
    }
    
    
    //监听显示模式(宽松型、紧奏型)和尺寸的改变
    //NCWidgetDisplayModeCompact :  折叠
    //NCWidgetDisplayModeExpanded : 展开
    - (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode withMaximumSize:(CGSize)maxSize {
        
        //maxSize:
        //虽说是最大的Size,但苹果还是把Widget的高度范围限制在了[110 ~ maxSize]之间
        //如果设置高度小于110,那么default = 110;
        //如果设置高度大于开发者设置的preferredContentSize.Heiget,那么default = maxSize;
        //折叠状态下,苹果将高度固定为110,这个时候设置preferredContentSize属性无效。
        NSLog(@"width = %lf-------height = %lf",maxSize.width,maxSize.height);
        
        //可以更改状态
        if (activeDisplayMode == NCWidgetDisplayModeExpanded) {
            self.preferredContentSize = CGSizeMake([UIScreen mainScreen].bounds.size.width, 300);
        }
        else{
            self.preferredContentSize = CGSizeMake([UIScreen mainScreen].bounds.size.width, 100);
        }
    }
    
    //设置扩展UI边距,注意:使用StoryBoard时,若要所见即所得,则这个方法中需要返回UIEdgeInsetsZero; (iOS10 and later 不会再被调用)
    //- (UIEdgeInsets)widgetMarginInsetsForProposedMarginInsets:(UIEdgeInsets)defaultMarginInsets {
    //    return UIEdgeInsetsZero;
    //}
    
    @end

    3、打印和gif

    2019-11-20 16:22:31.074596+0800 TodayExtension[29668:1132736] 1------------name1=xiayuanquan
    2019-11-20 16:22:31.234435+0800 TodayExtension[29668:1132736] 2------------data={length = 149, bytes = 0x62706c69 73743030 d4010203 04050607 ... 00000000 00000068 }
    2019-11-20 16:22:31.234970+0800 TodayExtension[29668:1132736] maxSize.width = 359.000000-------maxSize.height = 110.000000 //折叠
    2019-11-20 16:22:38.117764+0800 TodayExtension[29668:1132736] maxSize.width = 359.000000-------maxSize.height = 528.000000 //展开

     

  • 相关阅读:
    VSCode插件PlatformIO仿真LVGL
    Keras速查_CPU和GPU的mnist预测训练_模型导出_模型导入再预测_导出onnx并预测
    一文彻底搞懂mybatis
    vue实现点击某个标签变换颜色
    js的一些时间处理方法:日期差、日期转换字符串格式、获取当前年月日、日期时间的加减:加年,加月,加日,加时,加分,加秒
    vue项目首次访问加载页面太慢问题
    文件上传浏览器请求头里面报错:413:Payload Too Large
    uniapp实现横向滚动
    前端实现网站首页:背景图加文字居中效果
    uniapp封装接口请求api
  • 原文地址:https://www.cnblogs.com/XYQ-208910/p/11897343.html
Copyright © 2020-2023  润新知