学习前准备工作:
一创建项目
声明:本篇博客写一半才发现widget单词不小心写错写成了wedged。嘘,大家知道就行了,懒得改项目名字再重新截图了。
这里我就不多说了,我这里创建一个wedgedDemo项目供学习使用。
我这里做的很简单,一个主控制器包含一个一级界面和两个二级界面。
二创建appid和开发配置文件。
一般公司项目这些都已经早就创建好了,但这里为了给你们提供一个完整的演示,我就单独创建为这个demo创建了。
准备工作做完了,下面正式进入今天的主题:
一:添加Today Extension工程
在项目中创建一个target,具体为Xcode-->File-->New-->Target-->Today Extention
添加完成之后我们看到工程:
target工程命名一般我用:项目名+WidgetToday,这样在项目中看文件区分很容易,但是当我们创建之后要去改一下Display Name,因为这个是我们widget里面要显示的名字,这个根据项目需要去改,大家可以看看像美团啊滴滴啊 这些应用的widged就知道了。
如果不小心创建target了,可以在这里选择删除重新创建
二:定制UI
凭个人习惯,默认target会帮你创建一个storyboard。一般我习惯用纯代码绘制ui界面,所以我会删除默认创建的MainInterface.storyboard,然后在info.plist中删除NSExtensionMainStoryboard,添加NSExtensionPrincipalClass为TodayViewController
三:真机调试和打包配置
其实到这里我们已经可以进入开发工作了,因为模拟器已经可以跑起来了。但是在真机上却不能调试。我们把Today Extension当作一个单独的app,有自己的App ID和Provisioning profile. 所以,我们还需要注册一个Today Extension的App ID和对应的配置文件。
Today Extension的bundle id一般命名方式为你的contain app的bundle id加上你创建的Today Extension工程名"com.jsc.demo.wedged"
将Today Extension的bundleID修改为刚刚为Today Extension创建的APP ID
打包或者真机调试的时候contain app与Today Extension选择各自的profile文件就可以了。
注意点:Today Extension版本号与contain app配置一致,否则审核上传的时候会有警告。
四:开发工作
打开我们的TodayViewController.m文件,我们可以进入开发工作了
iOS 8
iOS8下没有折叠和展开功能,默认的Widget高度为self.preferredContentSize设置的高度。
self.preferredContentSize = CGSizeMake(kScreenW, 100);
iOS8下所有组件默认右移30单位,可以通过下面的方法修改上下左右的距离
- (UIEdgeInsets)widgetMarginInsetsForProposedMarginInsets:(UIEdgeInsets)defaultMarginInsets {
return UIEdgeInsetsMake(0, 0, 0, 0);
}
iOS 10
iOS 10以后,Widget有了两种显示模式
//高度固定,最低高度为110
NCWidgetDisplayModeCompact;
//折叠,高度可变
NCWidgetDisplayModeExpanded;
设定显示模式,需要在设定Size前设定这个属性!!!
//设定显示模式,需要在设定Size前设定这个属性
if ([[UIDevice currentDevice] systemVersion].intValue >= 10) {
//高度固定,最低高度为110
// self.extensionContext.widgetLargestAvailableDisplayMode = NCWidgetDisplayModeCompact;
//折叠,高度可变
self.extensionContext.widgetLargestAvailableDisplayMode = NCWidgetDisplayModeExpanded;
}
//貌似这个实在ios8下才有用,如果在ios10后,这个设置高度没有用,系统固定高度,待测
self.preferredContentSize = CGSizeMake([UIScreen mainScreen].bounds.size.width, 200);
当显示模式设置为NCWidgetDisplayModeExpanded时,点击折叠和打开时,会触发下面这个方法,在这个方法中可以修改对应状态的高度
- (void)widgetActiveDisplayModeDidChange:(NCWidgetDisplayMode)activeDisplayMode withMaximumSize:(CGSize)maxSize {
if (activeDisplayMode == NCWidgetDisplayModeCompact) {
self.preferredContentSize = CGSizeMake(maxSize.width, 110);
}
else {
self.preferredContentSize = CGSizeMake(maxSize.width, 200);
}
}
在下面的方法中更新视图
-(void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult))completionHandler {
// NCUpdateResultNewData 新的内容需要重新绘制视图
// NCUpdateResultNoData 部件不需要更新
// NCUpdateResultFailed 更新过程中发生错误
completionHandler(NCUpdateResultNoData);
}
现在给大家看一下我的TodayViewController.h页面,由于不想在一个个敲代码,就直接上图了
为了测试,我写了一个button事件。
关于打开widged调用:
1.经测试,每次手机左滑进入widget,都会调用widget页面中的viewDidLoad和widgetPerformUpdateWithCompletionHandler方法,其实左滑一次就是重新加载一次,但是存在间隔时间。(大概2秒左右间隔),不能一直左右滑动,这样并不会调用方法。
2.我们的主程序app关闭之后,依然可以进入button方法,也就是说只要左滑出现widget,那么这个target工程就一直在运行了。侧面说,左滑向当于打开了我们的widget APP。
五:代码共享
在开发widged时,我们也需要用到主app中的对象,类以及一些三方库,那么我们如何进行代码共享呢?
我们先测一下,在widget中是否能直接#import主程序App某个类,然后使用呢。
我们在主项目中创建一个person类,简单设置下属性。然后在widget中使用试一下。
看样子觉得可以用,xcode并没有报错,而且还帮你做了识别。但是一旦编译,就出错了。
找不到这个类哟,怎么办,老实消停的往下看吧。
1:最简单最不推荐的方法,复制!我们可以将这些库在Widget中复制一份。这样,我们的widged就可以用这些库了,但是安装包会变大,程序运行变慢,所以我不推荐。
2:通过Pods导入,不太建议通过pod分别向两个Target中导入第三方库,因为很容易发生一些不好处理的问题
3.将需要共享的文件按图中进行勾选配置,我也不建议,为啥呢?一般项目中用的随便一个三方库,比如AFNetWorking就有几十个文件,而且我们用到的不止这些,你要勾到何年何月,当然你是在愿意,那也可以,毕竟比前两个方法好一些。
4.将公用代码打包放在一个新的Framework中,然后link到主App和Widget中,那就可以了。极力推荐这个。
怎么做呢?
第一步:和创建APP Extension一样,New->Target,选择Cocoa Touch Framework来创建framework
第二步:我们在Today Extension工程里面link这个新的PublicFramework
而在主app工程里面不用添加,因为xcode已经帮我们自动添加好了
第三步:将公用的类库啊,都拖到PublicFramework中
然后我们就可以使用啦
六:数据共享
1.首先需要去[苹果开发者中心]的APP Groups中创建一个APP Group,命名方式"group.com.companyName.项目名",注意要以group开头
2.编辑你的主 app的APP ID,Service中选中App Groups,并且点击右边的Edit按钮选中刚刚创建的group,返回后,点击Done完成APP ID的编辑,再用同样的方法编辑widget App ID。
3.此时我们的主 app和widget App中的Provisioning Profiles文件会显示为无法使用,我们需要重新编辑更新一下,并且下载下来覆盖安装。
4.Today Extension工程与app工程中的Capabilities配置都如下图所示
通过App Groups提供的同一group内app共同读写区域,可以用NSUserDefaults和NSFileManager两种方式实现Today Extension和containing app之间的数据共享。
通过NSUserDefaults共享数据
- (void)saveDataByNSUserDefaults
{
NSUserDefaults *shared = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.xxx.xxx"];
[shared setObject:@"test" forKey:@"widget"];
[shared synchronize];
}
- (NSString *)readDataFromNSUserDefaults
{
NSUserDefaults *shared = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.xxx.xxx"];
NSString *value = [shared valueForKey:@"widget"];
return value;
}
通过NSFileManager共享数据
- (BOOL)saveDataByNSFileManager
{
//存在和Library同级目录
NSError *error = nil;
NSFileManager *fm = [NSFileManager defaultManager];
//这里Identifier必须和你的group id保持一致,不然获取的URL会为null
NSURL *groupURL = [fm containerURLForSecurityApplicationGroupIdentifier:@"group.come.jsc.jscdemo"];
NSURL *fileURL = [groupURL URLByAppendingPathComponent:@"appGroup.txt"];
NSLog(@"fileURL====%@",fileURL);
NSString *value = @"test";
//写入文件
BOOL result = [value writeToURL:fileURL atomically:YES encoding:NSUTF8StringEncoding error:&error];
if (!result) {
NSLog(@"%@",error);
} else {
NSLog(@"save value:%@ success.",value);
}
// //存在和Library/Caches文件下
// NSError *error = nil;
// NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.come.jsc.jscdemo"];
// containerURL = [containerURL URLByAppendingPathComponent:@"Library/Caches/test"];
//
// NSLog(@"containerURL====%@",containerURL);
// NSString *value = @"test222";
// BOOL result = [value writeToURL:containerURL atomically:YES encoding:NSUTF8StringEncoding error:&error];
// if (!result) {
// NSLog(@"%@",error);
// } else {
// NSLog(@"save value:%@ success.",value);
// }
return result;
}
- (NSString *)readDataByNSFileManager
{
NSError *error = nil;
NSURL *groupURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.come.jsc.jscdemo"];
NSURL *fileURL = [groupURL URLByAppendingPathComponent:@"appGroup.txt"];
NSString *value = [NSString stringWithContentsOfURL:fileURL encoding:NSUTF8StringEncoding error:&error];
return value;
// NSError *error = nil;
// NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.come.jsc.jscdemo"];
// containerURL = [containerURL URLByAppendingPathComponent:@"Library/Caches/test"];
// NSString *value = [NSString stringWithContentsOfURL:containerURL encoding:NSUTF8StringEncoding error:&error];
return value;
}
这样就实现了Today Extension与app的数据共享
七:调起App
1.设置App的URLSchemes,打开APP主要通过URLScheme打开和传递参数值。
设置URLSchemes时,要独特一些,避免与其他App重复
2.在Widget中添加点击事件,用于触发打开App的操作和传递参数。这里我在之前写的button按钮事件中添加打开App操作
//scheme为app的scheme
[self.extensionContext openURL:[NSURL URLWithString:@"widgedDemo://friends"]
completionHandler:^(BOOL success) {
NSLog(@"open url result:%d",success);
}];
// [self.extensionContext openURL:[NSURL URLWithString:@"widgedDemo://token=123456"]
// completionHandler:^(BOOL success) {
// NSLog(@"open url result:%d",success);
// }];
3.app的appdelegate的代理方法中,截取URL,做响应处理:
// 所有版本的都可以使用
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
[self appCallbackWithOpenUrl:url];
return YES;
}
/// iOS 8 以后
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options {
[self appCallbackWithOpenUrl:url];
return YES;
}
/// iOS 7
- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url {
[self appCallbackWithOpenUrl:url];
return YES;
}
- (void)appCallbackWithOpenUrl:(NSURL *)url{
// 针对url进行不同的操作
NSLog(@"url====%@",url);
NSLog(@"url.host===%@ url.scheme===%@",url.host,url.scheme);
if([url.absoluteString hasPrefix:@"widgedDemo"]) {
if([url.host isEqualToString:@"friends"]) {
//判断是否是直接跳入到具体页面
UINavigationController *nav = (UINavigationController *)self.window.rootViewController;
FriendsViewController *friendVC = [[FriendsViewController alloc] init];
[nav pushViewController:friendVC animated:YES];
}
}
}
有时候,我们发现运行程序时,在app里面打断点,却无法截取到url,不会走那几个方法。为什么呢?这是因为你运行的是 Today Extension 而不是我们的主app,当你创建了Today Extension时,xcode自动帮你默认了运行这个。当然无法在appdelegate里面调试到断点了。
将它改成我们的主app就可以了。记住,调试哪个,就切换到哪个。
以下借用别人已经写好的注意点:
在APP Extension中不能使用的API
10.1Access a sharedApplication object, and so cannot use any of the methods on that object
不能获取sharedApplication对象
10.2Use any API marked in header files with theNS_EXTENSION_UNAVAILABLEmacro, or similar unavailability macro, or any API in an unavailable framework,For example, in iOS 8.0, the HealthKit framework and EventKit UI framework are unavailable to app extensions.
不能使用API的标志性头文件 和 theNS_EXTENSION_UNAVAILABLE 宏定义,还有一些不能用的框架API,例如HealthKit framework and EventKit UI framework。
10.3Access the camera or microphone on an iOS device (an iMessage app, unlike other app extensions, does have access to these resources, as long as it correctly configures theNSCameraUsageDescriptionandNSMicrophoneUsageDescriptionInfo.plistkeys)
不能获取麦克风和照相机的权限(iMessage 不同于其它的应用扩展,只要配置相关的应用权限就可以直接使用了)
10.4Perform long-running background tasks
不能长期的在后台运行
10.5Receive data using AirDrop
不能使用AirDrop接收相关的数据
以上个人翻译,建议查看官方文档
TodayExtension的进入设置的快捷方式
//打开Wi-Fi
[self.extensionContextopenURL:[NSURLURLWithString:@"Prefs:root=WIFI"]completionHandler:^(BOOLsuccess) {
}];
//打开蜂窝网络
self.extensionContextopenURL:[NSURLURLWithString:@"Prefs:root=MOBILE_DATA_SETTINGS_ID"]completionHandler:^(BOOLsuccess) {
}];
更多方式请点击这里
请尽量按照以上方式避免被拒绝。
widget开发注意点:
widget开发注意点:
widget开发注意点:
重要的事情说三遍!!!
1.取资源图片取不到?当前taget---build phases--copy bundle resources看看是不是没有?有可能你两个taget来回移动或者别的操作,导致当前taget已经失去了这个图片资源。添加资源一定要看看是给那个target用还是两个都要用
还有资源文件,如果我们用mainBundle取资源文件,每个target的mainbundle应该也是不同的,这个我还没有测,有机会大家可以取测试一下。
2.运行项目不走调试?一定要搞清楚我们在调试那个target,如果是widget,就run widget target,如果是demo项目,就run demo target
3.一定要记得共享代码,共享代码,别把类放在其中一个target下,用另一个target去调用
好了,附上我的代码Demo地址
4.widget有一个自己的沙盒,app有一个自己的沙盒
5.关于PublicFramework。关系系统的.framework属于动态库,而我们创建的.framework属于动态库。如果直接创建,然后link使用真机模拟器都是没有问题,可是一旦archive时就找不到PublicFramework中的文件了。为什么?因为我们还少了一些配置。鬼知道我经历了什么后才写下来第5点注意点!!!直接看别人的博客连接吧,关于动态库制作的。https://blog.csdn.net/u014361280/article/details/80938740
还有,如果我们在静态库中添加了libsqlite3.tbd这类的库,运行时会报错:libsqlite3.tbd is not an object file (not allowed in a library).
关于这个解释继续看别人的博客吧,心态崩了,不想写了:
https://blog.csdn.net/sandy_shell/article/details/49247045
总之,记住制作静态库后一定要配置,看博客https://blog.csdn.net/u014361280/article/details/80938740的第4点、更改Xcode项目配置,如果我们的静态库中添加了libsqlite3.tbd这类库,直接删了,在项目中添加就行了,静态库直接删了。
你以为到这里,这个静态库就不给你找事了???
WTF!打包时又出问题!懒得说问题了,上博客:https://www.jianshu.com/p/8e28914f013e
到这里可以打包了,但是!项目出问题了,上博客:https://www.jianshu.com/p/d9fd65b5f727
上图片:
总之,只要在app中和widget开发中,我们用到了第三方的静态库(.a文件),那么在各自的build setting中找到other link flags,检查-licucore、-ObjC 和-all_load是否添加。具体为什么,可以看这个博客:https://blog.csdn.net/cjh965063777/article/details/51880966
至此完成!楼主心态已崩!再见!!!
NSUserDefaults *dft = [NSUserDefaults standardUserDefaults];
[dft setObject:@"widget" forKey:@"widget"];
[dft synchronize];
如果用standardUserDefaults,则widget里面用存在widget沙盒目录里,app用则存放在沙盒目录里。如果放在PublicFramework中,做成公共方法的话,则谁调用此方法,会存在谁的沙盒里面。[NSBundle mainBundle]也是如此,说明本身PublicFramework虽然是一个target,但它的类型导致它并没有自己的沙盒。
5.公共库PublicFramework里面的文件找不到:
问题:如果我们直接在PublicFramework里面创建类,或者从外部拖入,是没有问题的。app和App Extensions都能使用,可是如果我们从项目app中把一些三方库或者自己封装的类库拖进去,会发现App Extensions找不到这个文件,无法调用里面的方法。
解决方案:
首先看我们自己创建的类库:
这里没问题,是正常link到PublicFramework中,而app和App Extensions又link了PublicFramework,所以没问题。
在看我们从app中已有的类库拖入到PublicFramework中:
注意到,我们的nssring分类,xcode并没有帮你更改。还是link到app中,因此app extensions并不能使用这个分类。这可能和xcode版本有关,也可能和项目有关,有时候xcode也会帮你自动改好,也有时候不会改。总之,当我们拖入类库到PublicFramework中,一定要检查这个link。
另外,当我们创建类库时,大多数忽略一个选项:
这里又target让我们选,只不过xcode帮我们自动做了勾选,你在哪个target下创建的,默认帮你勾选了这个。