揭秘 iOS App Extension 开发 —— Today

作者: Cyandev | 来源:发表于2016-06-13 20:55 被阅读6458次

    从 iOS 8 开始,苹果引入了全新的 App Extension,涉及到方方面面,例如今日面板、键盘、内容拦截器、分享动作等。但是官方对于 App Extension 的开发指南少之又少,入门起来会有很多坑。所以我准备写一系列文章来帮助大家更好入门 App Extension 的开发,也能少走弯路。

    何为 App Extension?

    顾名思义,它是一种扩展,很类似于一些大型软件(好吧,现在可能是个应用都可以有)的插件机制。App Extension 事实上并不是你应用的插件,而是系统的插件,其生命周期是由系统来管理的,所以如果你想做什么坏事还是行不通的...但是 App Extension 分发的载体是应用,也就是说如果你只是单纯想做一个今日面板插件,也需要有个主程序,你的主程序可以什么都不做,也可以提供一些基本的设置和数据。

    App Extension 和主程序的关系?

    可以说没有什么关系,基本上就是两个独立的程序,你的主程序既不可以访问 App Extension 的代码,也不可以访问其存储空间,这完完全全就是两个进程、两个程序。这时你可能会问,我擦,都不能交互那有什么卵用??别急,后面我会讲到如何做一些交互。

    App Extension 可以干什么?不可以干什么?

    基本上什么都能干,有人不是在今日面板里把 Chrome 的恐龙小彩蛋硬塞进去了吗?还有拿输入法当浏览器作分屏多任务的...只有你想不到,没有你做不到......诶等等,有些还是做不到的。比如,内存有限制,App Extension 的可用内存远不如常规应用,以至于如果你真想做游戏,还是掂量掂量你的资源占用问题能不能解决吧。而且还不能访问 UIApplication,因为它的容器应用是系统,你拿系统的 UIApplication 想干嘛...(当然,你可以用递归查找 UIResponder 的方法拿到 UIApplication,但是我没试过)再次,你不能执行长时间的操作,你的 App Extension 可能随时被系统 Kill 掉,who knows?

    还有更多不可用的 API 可以看这个苹果官方文档:Understand How an App Extension Works

    开始创建一个 App Extension

    首先看一下我们要做的东西,是一个简单 Todo 应用,主程序长这个样:


    今日面板插件长这个样:


    界面都很简单啦~
    主程序实现其实很简单,就是 Table View 的使用以及数据持久化,这里就不着重讲了。但注意,我们要留出一个接口给今日面板,假设这里我们要在今日面板里显示前 4 条待办事项,我们必须要单独将这 4 条存在一个主程序和扩展都能访问的地方。后面我会说怎么做。

    Tips:


    苹果的 HIG 明确指出,不要在今日面板里使用可以滚动的 Scroll View,而是要完全展开,因此对于多条数据,我们要不就分页,要不就只显示前几项。

    下面,我们就为工程创建一个 Today Extension:


    一路下一步,输入一个子项目名,点 『Finish』就完成 Today Extension 的添加了。

    这个子项目的初始目录结构如下:


    P.S. 那个 entitiements 文件是后来创建的,一开始不会有

    然后我们在 Storyboard 里把大致界面拖出来,如果画布大小不合适可以在这调整一下,但是也就是调整了预览效果,真实的大小不能在 IB 里修改。


    那我们怎么修改视图在今日面板里的大小呢?答案是修改 View Controller 的 preferredContentSize 属性,不理会 width,调整 height 到合适的大小即可,因为宽度总是和屏幕宽度相同的。

    在这个例子中,我使用 44 * 项目数量 - 1 来作为视图高度,因为一个标准 Table View Cell 的高度是 44,然后减掉最后一个条目分割线的高度就是我们理想的合适高度。

    主程序向 App Extension 共享数据

    我们在主程序里创建了待办事项,怎么才能让 App Extension 获取到呢?由于两者代码和数据都不互通,所以我们 可以理所当然的想到用 App Group 来解决。首先在主程序中创建一个 App Group:

    然后在 App Extension 里添加这个 App Group 即可。
    这样,我们就可以用 NSUserDefaults 通过这个 App Group 交流数据了。

    还记得我说过要拿出所有数据的前四条放到今日面板中展示吗?下面我们就来实现这个功能:


    当主应用的数据变化后就调用这个方法来更新快照数据。

    下面我们主要来看 Today Extension 怎么实现,首先看看这两个方法:


    其中第一个方法是系统告诉 Extension 需要更新了,当你更新完毕之后通过 block 回调告诉系统你完成了还有做了什么,通常我们就告诉系统我们更新数据了即可(就是给 block 传 NCUpdateResultNewData 枚举项作为参数)。

    其中第二个方法是返回一个内补大小,如果不实现,默认情况视图左侧会有一定的缩进。当然,苹果还是希望你不要修改默认的内补~

    然后我们实现数据的读取:


    P.S. 第三行写错了,不要管它
    其实也很简单,就是从 App Group 的配置里拿出前 4 项的快照,然后更新一下 Table View 即可。这个方法在 viewDidLoad 或者 widgetPerformUpdateWithCompletionHandler: 中调用都可以。

    到这我们看看效果,选中 Today Extension 的那个 Scheme 点击调试按钮,弹出下面的对话框:



    选择我们的主程序,点击 『Run』。

    App Extension 调起主程序并执行动作

    当我们的 Todo List 是空的情况下,我们希望在今日面板里展示一个按钮,点击后可以快速进入创建 Todo 的界面,就像这样:


    由于我们访问不了主程序的代码,所以只剩下一条路可以选,那就是 URL Scheme

    首先,我们给主程序注册一个 URL Scheme:


    然后响应按钮点击:


    由于 App Extension 访问不了 UIApplication,因此不能用它的 openURL:,但是我们可以用 extensionContext 来打开 URL,用法和效果是一样的。

    回到主程序,我们处理 URL 的打开:


    这里我用 Notification 的方式告知指定 View Controller 来执行相应动作,当然你也可以用你自己喜欢的方式,这里最复杂也就是处理路由,现在也有很多方法实现,我这里就不深究了。

    下面看看效果(不好意思,图没做好,不动请在新窗口中打开):


    好了,到这我们就基本打通主程序和 App Extension 的相互通信了,是不是也很简单呢?

    最后,一个小提醒

    由于通知中心的界面是一大块 UIVisualEffectView ,并且具体参数调整过,所以插件的背景色最好保持透明,主要文字颜色最好是白色,次要文字的颜色最好是 lightTextColor,这样能适应毛玻璃下的 Vibrancy 效果。

    今日面板每个插件的高度计算和 UITableView 自适应高度的计算方式一致,如果你没有设置 preferredContentSize,或者把它设为 CGSizeZero 了,就表示你想采用自适应高度,那么系统就会根据你设计的 Auto Layout 来确定适合的高度。如果你想这么做的话,直接参考 UITableViewCell 在 iOS 8 以后自适应高度的方式即可。

    完了~希望大家支持!

    相关文章

      网友评论

      • kidd风:没有Demo,差评
      • 卟师:我能转载分享吗
        我会标注上作者和出处的
      • 寂灭天骄小童鞋:请问一下,如何不让3dTouch显示widget,我查的咋说这是系统自带的。。。可是支付宝的3d touch 并没有显示,想问下你知道咋实现的不,求解
        Cyandev:@Magic_YY 嗯,确实,不过我还没研究这个问题...
      • zero000:您好 我想问下today extension的高度有限制吗
        Cyandev:@zero000 应该有,但是够用了,能申请下一屏多的空间
      • zero000:遇到问题了,我写的widget工程没有刷新,能否看下您的这个方法:-widgetPerformUpdateWithCompletionHandler:里面都做了什么操作吗?
        Cyandev:这是后台被动调用的,里面主要放一些数据更新的操作就可以,viewDidLoad 也可以做数据更新。
      • 背着吉他去流浪:有个问题, 在真机上不显示怎么解决?
      • 李乾坤David:有没有Share Extension篇?
        背着吉他去流浪:我自己写的了一篇, 有兴趣你看看

        http://www.jianshu.com/p/0bf735c9e502
        李乾坤David:@Cyandev 好的!
        Cyandev:@李乾坤David 这几天考试比较忙,过些时间继续更新
      • 大罗Rnthking:老兄,开篇的Xcode8 写成了 IOS8么,这是个大错误呀。
        walter520:@SuperSpiderMan 你再看看你现在的Xcode版本
        Cyandev:@SuperSpiderMan 就是 iOS 8……
      • 清眸如画:你好,请问在不启动主程序的情况下,可以让extension 实时更新内容吗?
        Cyandev:@奋斗的月 对啊,就是不启动主程序,你在扩展里直接更新数据不就行了嘛。但是通知中心被关闭时你的扩展就被挂起了
        清眸如画:@Cyandev 我说的是在不启动主程序的情况下?我想到的是在extension中一直跑一个线程来实时更新数据
        Cyandev:@奋斗的月 可以,通常做法是把数据层单独拿出来作为 Private Framework,然后主程序和扩展都同时引用,这样能减少代码重复量
      • d9bdc65a4790:楼主大人,求个demo :sunglasses:
      • YViVi:可否放demo上来学习一下
      • Code_Ninja:我的today widget上放了一个按钮,点击按钮的时候触发事件:
        ```
        - (IBAction)testBtnAction:(id)sender {
        NSLog(@"按钮点击");
        [self.extensionContext openURL:[NSURL URLWithString:@"publicTopic://clickbtn"] completionHandler:nil];
        }
        ```
        可是实际上,点击了按钮只是打开了主程序,主程序中的方法openUrl:没走,这是为什么呢?
        还有就是,这个today widget开发怎么这么难调试呢,为什么断点断不到呢?
        Cyandev:@Code_Ninja 这个写的也应该是没问题的,如果要发通知给 VC 的话,最好等 VC 的 viewDidLoad: 方法执行完再发通知,你可以用列队或者 GCD after 的方式处理一下通知的发送。因为应用刚启动的时候可能 VC 还没被创建,通知就已经发送了,所以自然接收不到了。
        Code_Ninja:@Cyandev 不是吧,我这里怎么是选中App Extension的Scheme运行,在弹出的选择容器应用的表单中选择Today才可以断点,选择我自己的主程序断点不走,点击按钮也没反应。
        另外我在appdelegate中的代码如下:
        -(BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<NSString *,id> *)options
        {
        [[NSNotificationCenter defaultCenter]postNotificationName:@"test" object:nil];
        NSLog(@"test")
        return YES;
        }
        Cyandev:@Code_Ninja App Extension 实际上是可以打断点的,你需要选择 App Extension 那个 Scheme 然后运行,会弹出一个选择容器应用的表单,选择你的主程序,这样就可以调试了。另外,上面那个问题可能是你 AppDelegate 写的有问题。
      • lexiaoyao20:不错,涨姿势了

      本文标题:揭秘 iOS App Extension 开发 —— Today

      本文链接:https://www.haomeiwen.com/subject/tqwydttx.html