iOS 中的扩展有多种,包括:Today,Share,Action,PhotoEditing,FinderSync,自定义键盘等等。这里要说的类型是 Today,就是在通知中心“今天”里面添加的 widget


对于 Today 插件,它应该:

  • 确保内容永远是最新的
  • 正确响应用户的操作
  • 高效运行(特别是iOS插件必须合理使用内存,否则系统可能终止插件的运行)

####新建 Widget Target
Extesion 都是依附于某个主体应用程序(containing app)中的一个单独的二进制包。所以它必须在现有的工程中来创建。它与主体程序之间可以通过某种方式间接通信,下面会提到。


通过 File->New->Target 在弹出菜单中选择 Today Extension 模板


image


新建完成后工程目录中会自动出现一个文件夹,里面有一些默认生成的文件:

  1. TodayViewController: widget 的主视图;
  2. MainInterface.storyboard: 对应 TodayViewController 的布局文件;
  3. Info.plist: widget 的配置文件


image


在 Info.plist 中有一个 key 指定了 Extension 的一些基本信息:


image


如果不想使用 storyBoard 文件来管理布局,那么就将 NSExtensionMainStoryboard 这个 key 移除,然后添加 key NSExtensionPrincipalClass,并且用 controller 的类名作为 value:


image


但是我这样做的话程序会报错,至今仍未找到原因。无奈只能将 storyBoard 留下来先


image

####编写 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 中传递参数

  1. 在项目对应的 Target 中的 Info 页,添加一个 Scheme。Identifier 为自己项目的 Bundle Identifier;在 URLSchemes 一栏里填上自己想要的名称。
  2. 通过 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];
}

image


> vibrancy effect 依赖于颜色。任何添加到 contentView 的 subView 都需要实现 `tintColorDidChange` 方法,并且根据需要来更新自己的外观。UIImageView 对象有一种渲染模式 `UIImageRenderingModeAlwaysTemplate` 来进行自动更新,UILabel 对象也是。


而在开发 Today Widget 的时候,想达到下图的中效果该怎么办呢。我们无法获取通知中心背景的 Blur Effect。


image


NotificationCenter frameWork 给我们提供了一个 Category,看一眼方法名就懂:

1
2
3
4
5
@interface UIVibrancyEffect (NotificationCenter)

+ (UIVibrancyEffect *)notificationCenterVibrancyEffect;

@end

于是可以在 widget 的 todayViewController 中这样来添加 effectView

1
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:


image


那么问题来了,想要实现上图中 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 中有这样的一段文字:

image


>翻译翻译:widget 应该在 `viewWillAppear:` 中加载缓存起来的数据,好让它能匹配上次在 `viewWillDisappear` 时的状态,这样在新数据到来的时候就可以丝滑顺畅地过渡。


在 widget 中的 controller,当它 dismiss 的时候会被 deallocated,所以不可以用 NSArray 来保存数据,可以选择用 NSUserDefault。

####参考资料

  1. App Extension编程指南, CocoaChina
  2. Building a Today View Extension in iOS 8

以上