iOS 中的扩展有多种,包括:Today,Share,Action,PhotoEditing,FinderSync,自定义键盘等等。这里要说的类型是 Today,就是在通知中心“今天”里面添加的 widget
对于 Today 插件,它应该:
- 确保内容永远是最新的
- 正确响应用户的操作
- 高效运行(特别是iOS插件必须合理使用内存,否则系统可能终止插件的运行)
新建 Widget Target
Extesion 都是依附于某个主体应用程序(containing app)中的一个单独的二进制包。所以它必须在现有的工程中来创建。它与主体程序之间可以通过某种方式间接通信,下面会提到。
通过 File->New->Target 在弹出菜单中选择 Today Extension 模板
新建完成后工程目录中会自动出现一个文件夹,里面有一些默认生成的文件:
- TodayViewController: widget 的主视图;
- MainInterface.storyboard: 对应 TodayViewController 的布局文件;
- Info.plist: widget 的配置文件
在 Info.plist 中有一个 key 指定了 Extension 的一些基本信息:
如果不想使用 storyBoard 文件来管理布局,那么就将 NSExtensionMainStoryboard
这个 key 移除,然后添加 key NSExtensionPrincipalClass
,并且用 controller 的类名作为 value:
但是我这样做的话程序会报错,至今仍未找到原因。无奈只能将 storyBoard 留下来先
编写 widget 布局
Today 的视图有限,所以我们的 widget 应该越小巧玲珑越好。况且本来 widget 就不应该太过繁重。widget 应该适应 Today 视图的宽度,通过增加高度来显示更多内容。
要控制 widget 的内容高度,可以通过两种方法,第一种是通过 AutoLayout;第二种则是用:1
self.preferredContentSize = CGSizeMake(SCREEN_SIZE.width, HEIGHT);
另外,NCWidgetProviding
协议里面的一个方法也会影响内容布局:1
2
3
4- (UIEdgeInsets)widgetMarginInsetsForProposedMarginInsets:(UIEdgeInsets)defaultMarginInsets
{
return UIEdgeInsetsMake(0, 0, 0, 0);
}
数据传输
方式一:自定义 URL Scheme,在 URL 中传递参数
- 在项目对应的 Target 中的 Info 页,添加一个 Scheme。Identifier 为自己项目的 Bundle Identifier;在 URLSchemes 一栏里填上自己想要的名称。
- 通过 myappscheme://?foo=1&bar=2 这种 URL 方式来唤起自己的 app,并且可以在 AppDelegate 中的回调捕获到这个 URL,然后便可以对 URL 中的参数进行分析处理:
1
2
3
4- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url
{
// Do something with the url here
}
方法二:使用 NSUserDefault
widget 与 containing App 共享一个 NSUserDefault。不过要注意的是,这里指的不是我们通常所用的标准 NSUserDefault。在 widget 中的 UserDefault 与 Containing App 中的 UserDefaul 是不一致的,这是一个坑。我们需要在苹果开发者网站里面新建一个 group,然后用这个 group 的名字组建一个 Suite Name 来初始化一个 NSUserDefault:1
[[NSUserDefaults alloc] initWithSuiteName:@”group.com.mycompany.myapp”]
这篇文章可以让你更加详细地了解这部分。
UIVbrancyEffect
它是跟 UIBlurEffect 一起随 iOS8 发布的。通常情况下它都要配合 UIBlurEffect 来一起使用。通常情况下会这样使用:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24- (void)setEffect
{
//先初始化一个 blur effect view
UIBlurEffect * effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
UIVisualEffectView * viewWithBlurredBackground = [[UIVisualEffectView alloc] initWithEffect:effect];
viewWithBlurredBackground.frame = self.view.bounds;
viewWithBlurredBackground.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self.view addSubview:viewWithBlurredBackground];
//然后再利用上面的 blur effect 初始化 vibrancy effect view
UIVibrancyEffect *vibrancyEffect = [UIVibrancyEffect effectForBlurEffect:effect];
UIVisualEffectView * viewInducingVibrancy = [[UIVisualEffectView alloc] initWithEffect:vibrancyEffect]; // must be the same effect as the blur view
viewInducingVibrancy.frame = viewWithBlurredBackground.frame;
viewInducingVibrancy.autoresizingMask = viewWithBlurredBackground.autoresizingMask;
[viewWithBlurredBackground.contentView addSubview:viewInducingVibrancy];
//最后往 effectView.contentView 里面添加内容
UILabel * vibrantLabel = [UILabel new];
vibrantLabel.font = [UIFont systemFontOfSize:120.0f];
vibrantLabel.text = @"Vibrant";
[vibrantLabel sizeToFit];
[viewInducingVibrancy.contentView addSubview:vibrantLabel];
}
vibrancy effect 依赖于颜色。任何添加到 contentView 的 subView 都需要实现
tintColorDidChange
方法,并且根据需要来更新自己的外观。UIImageView 对象有一种渲染模式UIImageRenderingModeAlwaysTemplate
来进行自动更新,UILabel 对象也是。
而在开发 Today Widget 的时候,想达到下图的中效果该怎么办呢。我们无法获取通知中心背景的 Blur Effect。
NotificationCenter frameWork 给我们提供了一个 Category,看一眼方法名就懂:1
2
3
4
5@interface UIVibrancyEffect (NotificationCenter)
+ (UIVibrancyEffect *)notificationCenterVibrancyEffect;
@end
于是可以在 widget 的 todayViewController 中这样来添加 effectView1
2
3
4
5
6
7
8
9- (void)viewDidLoad {
[super viewDidLoad];
UIVisualEffectView *effectView = [[UIVisualEffectView alloc] initWithEffect:[UIVibrancyEffect notificationCenterVibrancyEffect]];
//TODO: 设置 effectView 的 frame
[self.view addSubview:effectView];
//TODO: add subviews to contentView
}
这里有个小坑:当我直接将一个 button 添加到 contentView 中,button 的 title 会“显示不出来”。这是因为 button.titleLable 中的文字拥有跟 button 一样的 vibrancyEffect,如果给 button 的背景颜色设置一个 alpha 值,就可以看到 title:
那么问题来了,想要实现上图中 Chrome 浏览器 widget 的 button 一样的效果该怎么办呢。
用 drawInRect:
等函数来自己绘制吧。Github 上已经有人实现了这种风格的 button:AYVibrantButton。里面的 button title 跟 image 都是用方法来绘制的。看来貌似只有这个方法了哦。
更新插件状态
NCWidgetProviding 协议中有一个方法,会在某些时刻被调用,来给 widget 一个机会来更新自己的 UI,例如截屏的时候。1
2
3
4
5
6
7
8
9- (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);
}
更新完数据之后要根据数据的更新情况来调用 block 回调。
widget 的生命周期
NSWidgetProviding.h 中有这样的一段文字:
翻译翻译:widget 应该在
viewWillAppear:
中加载缓存起来的数据,好让它能匹配上次在viewWillDisappear
时的状态,这样在新数据到来的时候就可以丝滑顺畅地过渡。
在 widget 中的 controller,当它 dismiss 的时候会被 deallocated,所以不可以用 NSArray 来保存数据,可以选择用 NSUserDefault。
参考资料
以上