widget是iOS8时推出的窗口小部件功能,窗口小部件在Android上早已大行其道。记得当年用过的第一部Android是深圳出产的国产机,当时滑过三四个屏幕的应用,还能继续再滑三四个屏幕的窗口小部件。用的最多的窗口小部件就是日历了,屏幕上一目了然。
Apple直到iOS8才加入窗口小部件,而且可自定义程度远远没有Android开放。
本文记录了开发widget的步骤,以及遇到的一些问题。
开发环境:Xcode8.2.1,swift3.0
创建widget
widget可以理解为一个独立的项目,虽然形式上看来像是附属于app的一部分功能,其实并不是,widget想获取app的数据,还需要做数据共享。
File
-> New
-> Target
选择iOS里的Today Extension
习惯使用纯代码布局,喜欢用storyboard的不需要下面的info.plist修改。在新创建的widget项目文件夹中删除MainInterface.storyboard
,修改info.plist里的NSExtension
字段:
- 删除
NSExtensionMainStoryboard
字段 - 添加
NSExtensionPrincipalClass
字段,Value 为TodayViewController
(TodayViewController
是自定义控制器,)
修改info.plist的结果如下
问题1.widget崩溃
经过上述修改,用纯代码布局widget,用OC开发是没有问题的,swift3.0中widget会崩溃,并打印下面的错误。
1 | *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** setObjectForKey: object cannot be nil (key: 56A34ADC-7A43-43B0-A924-171F803DD305)' |
StackOverflow有人遇到同样的问题:Today Extension Crashes before launching on iOS 8.1.2,但解答好像并没有效果。
在一篇博客中我找到了解决办法,博客地址
1 | Since (at the time of this writing) Xcode cannot find Swift classes as extension principal classes, we also would have to add the following line to our TodayViewController: |
文中说目前为止Xcode找不到swift类作为拓展主题类(其实到我写这篇文章的时候,还是找不到),这可能是一个bug。解决办法是需要在widget控制器TodayViewController
中添加:
1 | TodayViewController) ( |
博文更新中说,可以更改Embedded Content Contains Swift Code
这个设置为yes,但是在Xcode8.2.1中,这个设置已经没有了,取而代之的是Always Embed Swift Standard Libraries
,亲测主项目的targets
和widget的targets
中修改这个设置的Bool值,都还是会崩溃。
widget折叠
iOS10之后才有的widget折叠。
1 | if #available(iOSApplicationExtension 10.0, *) { |
实现下面方法。
1 | 10.0, *) (iOSApplicationExtension |
代码共享
虽然widget附属于主应用,但其实是独立的。在widget中无法调用主应用中的代码,这样一来就蛋疼了。有些公共方法或者控件,在主应用中写完了,在widget却无法使用。当然把主应用中的代码拷贝一份到widget中也是可以的,这种做法太low。
可以使用framework做代码共享。创建一个framework
File
-> New
-> Target
在framework
的Build Phases
-> Compile Sources
里面添加要共享的代码文件。
在TARGETS
里面,分别在主项目和widget下面的Linked Frameworks and Libraries
里面添加新建的framework
并在widget中用到共享代码的地方引入framework
1 | import ShareToday |
问题1.引入framework报错和报警告
引入的时候会如下错误:
1 | TodayViewController.swift:11:8: Module file's minimum deployment target is ios10.0 v10.0: |
是因为framework的Deployment Target
的版本号和widget的版本号不相符,改为一样的即可。
报如下警告:
1 | ld: warning: linking against a dylib which is not safe for use in application extensions: |
是因为application extensions限制了一些API的使用,而在新建的framework里面,可能包含了这些API,所以才会出现这个警告。
解决办法:勾选framework里面的Allow app extension API only
问题2.方法调用不到
swift中,加入到framework的一些方法,在引入头文件后的widget调用不到。
解决办法:需要把方法设置为公用的,用public
修饰方法,例如
1 | public func getString(a: Int) -> String { |
如果有共用的oc代码,需要将.m文件引入到Compile Sources
,将.h文件拖入Headers
的Public
里面,然后在framework的.h头文件中#import
共用oc代码的.h头文件
数据共享
配置证书:
- 在
Certificates, Identifiers & Profiles
里的Identifiers
下面添加App IDs
时,要勾选App Groups
。 - 在
App Group
添加一个App Group
,在写Identifier
,会在前面自动添加group.
- 添加
App Group
之后,在App IDs
点开第1步创建的id,点击edit,把App Group
添加上,App Group
的黄点会变成绿点。
添加证书
在Xcode的TARGEST
下面,主程序和widget的Capabilities
里面,都要打开App Groups
。下面的Steps不能有红色叹号的错误。
在证书配置正确的前提下,还出现了红色叹号的错误警告,有可能是因为主项目或者widget的General
里面没有选择好正确的签名Team。
用NSUserDefaults共享数据,
存储数据
1 | let shareDefaults = UserDefaults(suiteName: "group.xxx.xxx.xx")//App Groups ID |
读取数据
1 | let shareDefaults = UserDefaults(suiteName: "group.xxx.xxx.xx")//App Groups ID |
点击widget开启app
在widget中,点击图标可以开启主应用,不用添加任何方法。如果想点击其他地方开启app,需要在app的TARGEST
里的info
下URL Types
添加URL Schemes
添加点击事件,调用方法,开启app
1 | func openApp() { |