iOS widget(小部件)开发初探

1、前言

现在很多应用都有小部件功能,用起来非常方便,在用户安装包含Today小部件的应用后,他们可以将小部件添加到Today视图。当用户在“今日”视图中选择“编辑”时,通知中心会显示一个视图,允许用户添加,重新排序和删除小部件。
常见的有支付宝、日历和天气,那么我们也想为自己的应用增加widget功能该怎么办呢,那就继续往下看喽。
老版本(iOS9之前)的是直接下拉出现【今天】和【通知】两个选项,iOS10进行了更改,下拉是【通知】,右滑最左侧是小部件,所以下文提到的【今天】就是我们的小部件

iOS10之后
iOS9之前

2、准备工作

了解这个功能,当然官方文档App Extension Programming Guide是最值得读的了。
官方对小部件的一段介绍是:

App extensions in the Today view are called widgets. Widgets give users quick access to information that’s important right now. For example, users open the Today view to check current stock prices or weather conditions, see today’s schedule, or perform a quick task such as marking an item as done. Users tend to open the Today view frequently, and they expect the information they’re interested in to be instantly available.
“今日”视图中的附加应用信息称为小部件,小部件使用户能够快速访问现在非常重要的信息。例如,用户打开今日视图以检查当前股价或天气状况,查看今天的时间表,或者执行快速任务,例如将项目标记为已完成。用户倾向于经常打开“今日”视图,他们希望他们感兴趣的信息立即可用。

注意: 小部件是不支持键盘输入的

交互要求
确保今天的扩展点适合您想要提供的功能。最好的小部件为用户提供快速更新或启用非常简单的任务。如果您想要创建支持多步骤任务的应用扩展程序,或者帮助用户执行冗长的任务(如上传或下载内容),则“今日”扩展点不是正确的选择。

3、创建项目

  • 创建
    Xcode->File->New->Target->Today Extension 创建我们的Widget


    创建
  • 项目结构


    Widget
  • 项目配置
    项目默认是有storyboard的,这里我想使用纯代码,所以把他删除了,删除后我们要配置一下启动界面,在TodayWidget->Info.plist->Extension
    删除 NSExtensionMainStoryboard 选项
    增加 NSExtensionPrincipalClass,value 为 类的名字 TodayViewController
    这个时候你就可以用纯代码构建布局了

<key>NSExtension</key>
    <dict>
        <key>NSExtensionPointIdentifier</key>
        <string>com.apple.widget-extension</string>
        <key>NSExtensionPrincipalClass</key>
        <string>TodayViewController</string>
    </dict>
配置
  • 代码

创建布局什么的和平时开发一样,一些方法代码里也都有注释,下面主要说一下数据共享和打开app的方法

//TodayViewController.m
#import "TodayViewController.h"
#import <NotificationCenter/NotificationCenter.h>

@interface TodayViewController () <NCWidgetProviding>
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, strong) UILabel *timeLabel;
@property (nonatomic, assign) int count;
@end

@implementation TodayViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self initView];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    
}

- (void)viewWillAppear:(BOOL)animated {
    // 设置折叠还是展开
    // 设置展开才会展示,设置折叠无效,左上角不会出现按钮, ❓
    self.extensionContext.widgetLargestAvailableDisplayMode = NCWidgetDisplayModeExpanded;
}

// 展开/折叠监听
- (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode withMaximumSize:(CGSize)maxSize{
    
    if (activeDisplayMode == NCWidgetDisplayModeCompact) { //折叠
        // 折叠后的大小是固定的,目前测试的更改无效,默认高度应该是110
        self.preferredContentSize = CGSizeMake([UIScreen mainScreen].bounds.size.width, 100);
    }else { // 展开
        self.preferredContentSize = CGSizeMake([UIScreen mainScreen].bounds.size.width, 300);
    }
}

- (void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult))completionHandler {
    // Perform any setup necessary in order to update the view.
    
    // If an error is encountered, use NCUpdateResultFailed
    // If there's no update required, use NCUpdateResultNoData
    // If there's an update, use NCUpdateResultNewData

    completionHandler(NCUpdateResultNewData);
}

- (void)initView {
    // 和主应用的数据共享,获取主应用里的数据
    NSUserDefaults *sharedData = [[NSUserDefaults alloc] initWithSuiteName:@"group.rs.testGroup"];
    NSString *name = [sharedData objectForKey:@"name"];
    // 官方建议使用自动布局创建控件,这里是写的固定的
    UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width/2.0, 50)];
    label.text = [NSString stringWithFormat:@"姓名:%@",name];
    label.textAlignment = NSTextAlignmentCenter;
    label.textColor = [UIColor blueColor];
    [self.view addSubview:label];
    
    UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(self.view.frame.size.width/2.0, 0, self.view.frame.size.width/2.0, 50)];
    [btn addTarget:self action:@selector(btnAction) forControlEvents:UIControlEventTouchUpInside];
    [btn setTitle:[self readByFileManager] forState:UIControlStateNormal];
    [btn setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
    [self.view addSubview:btn];
    
    // 添加计时器
    _timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
    
    _timeLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 50, self.view.frame.size.width, 50)];
    _timeLabel.textAlignment = NSTextAlignmentCenter;
    _timeLabel.textColor = [UIColor redColor];
    [self.view addSubview:_timeLabel];
    _count = 100;
}

// NSFileManager 读取数据
- (NSString *)readByFileManager {
    NSError *error = nil;
    NSURL *containUrl = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.rs.testGroup"];
    containUrl = [containUrl URLByAppendingPathComponent:@"group.data"];
    NSString *text = [NSString stringWithContentsOfURL:containUrl encoding:NSUTF8StringEncoding error:&error];
    return text;
}

- (void)timerAction {
    if (_count > 0){
        _count -= 1;
    }else {
        _count = 100;
    }
    _timeLabel.text = [NSString stringWithFormat:@"倒计时:%ds",_count];
}

// 点击按钮打开主app
- (void)btnAction {
    [self.extensionContext openURL:[NSURL URLWithString:@"TodayWidget://"] completionHandler:^(BOOL success) {
        
    }];
}

@end

运行后的效果


运行效果图

4、 调起app

因为 extension 和 主app 是两个完全独.立的进程,所以它们之间不不能直接通信(不能像应用内部点击按钮,跳转到指定页面)。为了了实现 Widget 调起 app,这里通过 openURL 的方式来启动 主app。

  • 添加URL Schemes
    在 主app 里配置 Targets->MCWidgetDemo-> Info->Url Types->+
    如下图 设置 URL Schemes 为 TodayWidget


    配置
  • 在 Widget中打开

// 点击按钮打开主app
- (void)btnAction {
    [self.extensionContext openURL:[NSURL URLWithString:@"TodayWidget://"] completionHandler:^(BOOL success) {
        
    }];
}
  • 主应用中的监听
// AppDelegate.m
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options{
    if ([url.scheme isEqualToString:@"TodayWidget"]){
        //这里监听到是通过widget打开,可以进行发送通知等操作
        return YES;
    }
    return NO;
}

5、数据共享

扩展程序一般都不是脱离宿主程序单独运行的,难免需要和宿主程序进行数据交互。由于拓展与宿主应用是两个完全独立的App,并且iOS应用基于沙盒的形式限制,所以一般的共享数据方法都是实现不了数据共享,这里就需要使用App Groups(App Groups这是iOS8新开放的功能,在OS X上早就可用了。它主要用于同一Group下的App共享同一份读写空间,以实现数据共享)。

通过 App Groups 提供的同一 group 内 app 共同读写区域,可以用 NSUserDefaults 和NSFileManager 两种方式实现 extension 和 主app 之间的数据共享。

  • 创建 App Groups
    在开发者网站注册一个App Groups,点击加号,填入名字和id一路确认即可得到下图App Groups。


    QQ20180403-103336.png
  • 在主程序和扩展程序中分别设置打开App Group,设置一个group的名称,这里要保证宿主APP和扩展APP的groupName要是相同的。


    主应用
widget
  • 利用NSUserDefaults数据共享
    在主应用中存储数据
 NSUserDefaults *sharedData = [[NSUserDefaults alloc] initWithSuiteName:@"group.rs.testGroup"];
    [sharedData setValue:@"Mr Right" forKey:@"name"];
    [sharedData synchronize];

在widget中读取数据

NSUserDefaults *sharedData = [[NSUserDefaults alloc] initWithSuiteName:@"group.rs.testGroup"];
    NSString *name = [sharedData objectForKey:@"name"];

注意:保存读取数据的时候必须指明group id;

  • 利用NSFileManager共享数据
    在主应用中存储数据
// NSFileManager 存储数据
- (void)saveFile {
    NSError *error = nil;
    NSURL *containUrl = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.rs.testGroup"];
   containUrl = [containUrl URLByAppendingPathComponent:@"group.data"];
    NSString *text = @"打开app";
    BOOL result = [text writeToURL:containUrl atomically:YES encoding:NSUTF8StringEncoding error:&error];
    if (result){
        NSLog(@"save success");
    }else {
        NSLog(@"error:%@", error);
    }
}

在widget中读取数据

// NSFileManager 读取数据
- (NSString *)readByFileManager {
    NSError *error = nil;
    NSURL *containUrl = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.rs.testGroup"];
    containUrl = [containUrl URLByAppendingPathComponent:@"group.data"];
    NSString *text = [NSString stringWithContentsOfURL:containUrl encoding:NSUTF8StringEncoding error:&error];
    return text;
}

6、总结

至此,小部件的简单开发算是完成了,后续可能还有发布的证书配置,网络请求等情况,我还没有尝试,等实际应用了再进行补充,希望能对你有所帮助,笔者也是第一次尝试,如果有哪里不对的,请指正。
最后附上Demo地址

7、参考链接

本文简书地址---- >>>>

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

推荐阅读更多精彩内容

  • 在说widget开发前,先来了解下APP Extensions和App Groups: 一、关于App Exten...
    P大迷妹阅读 4,344评论 1 10
  • 可以感觉到自己学的东西得到验证,感觉真的好。今天有两次,真的是舒服啊~ 所以说,所有学问都不是无用之学。这会成为你...
    桧枫阅读 182评论 0 0
  • 不要因为鱼有刺,就放弃鱼的美味;不要因为生活有瑕疵,就以为生活并不美。生活本来就是有选择的活着,只要懂得放下那些不...
    守护我的爱阅读 152评论 0 0
  • 一、 最近碰到这么一对邻居,张三李四对门而居,张三是建筑设计师,李四是政府公务员,说来都是在社会上有头有脸的人物,...
    草草啖盐说蜜阅读 381评论 0 2
  • 阿G先生和猫阅读 250评论 0 2