extension ios8
2016-08-15 17:19:00 weixin_34060299 阅读数 15

Share Extension简单说就是系统自带的社会化。我们可以打开safari浏览器 随便选中一个网站,当我们想分享数据的时候,点击中间的那条icon 就会出现一个分享界面就是系统自带的Share Extension(如图所示)。下面详细说如何把自己的app添加系统分享中。

1823313-1ad7537f92e90234.png

2.APP里添加创建ShareExtension

1.Share Extension需要依赖于主程序,我们需要先创建一个新的工程ShareExtensionDemo。然后开始创建Share Extension,如下图:

1823313-6d80767dda71ac77.png
1823313-dab09a06abf48790.png

2.然后我们切换到safari里面选中一个网站点击中间的那条icon 就会出现分享界面里面就会出现我们的刚创建的app,如下图:

1823313-5c4fbc42216d7358.png


1823313-c1c96c67eaa9119e.png

3. 配置Share Extension

接下来我们需要给他一些设置。我们展开XCode左侧栏的Share目录,找到Info.plist文件。如:


1823313-7b4561f62ffcaa02.png

4.将分享数据传递给容器程序

在默认情况下,iOS的应用是存在一个沙盒里面的,不允许应用与应用直接进行数据的交互。为此,苹果提供了一项叫App Groups的服务,该服务允许开发者可以在自己的应用之间通过NSUserDefaults、NSFileManager或者CoreData来进行相互的数据传输。如下图:

1823313-17f2b7c959ab9ab9.png
1823313-0d060a895a3f5b38.png

终于启动啦应用和扩展的App Groups服务,现在就要进行分享内容的传输操作。下面分别介绍一下NSUserDefaults实现App Groups下的数据操作:

//初始化一个供App Groups使用的NSUserDefaults对象

NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.weaventmind.mind.ShareExtensionDemo"];

//写入数据

[userDefaults setValue:@"value" forKey:@"key"];

//读取数据

NSLog(@"%@", [userDefaults valueForKey:@"key"]);

为了方便演示,这里会使用NSUserDefault来直接把取到的url地址保存起来。代码如下所示:

- (void)didSelectPost {        //加载动画初始化    UIActivityIndicatorView *activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];    activityIndicatorView.frame = CGRectMake((self.view.frame.size.width - activityIndicatorView.frame.size.width) / 2,                                            (self.view.frame.size.height - activityIndicatorView.frame.size.height) / 2,                                            activityIndicatorView.frame.size.width,                                            activityIndicatorView.frame.size.height);    activityIndicatorView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin;    [self.view addSubview:activityIndicatorView];        //激活加载动画    [activityIndicatorView startAnimating];        __weak ShareViewController *theController = self;    __block BOOL hasExistsUrl = NO;    [self.extensionContext.inputItems enumerateObjectsUsingBlock:^(NSExtensionItem * _Nonnull extItem, NSUInteger idx, BOOL * _Nonnull stop) {                [extItem.attachments enumerateObjectsUsingBlock:^(NSItemProvider * _Nonnull itemProvider, NSUInteger idx, BOOL * _Nonnull stop) {                        if ([itemProvider hasItemConformingToTypeIdentifier:@"public.url"])            {                [itemProvider loadItemForTypeIdentifier:@"public.url"                                                options:nil                                      completionHandler:^(id_Nullable item, NSError * _Null_unspecified error) {

if ([(NSObject *)item isKindOfClass:[NSURL class]])

{

NSLog(@"分享的URL = %@", item);

NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.weaventMind.weaventMind"];

[userDefaults setValue:((NSURL *)item).absoluteString forKey:@"share-url"];

//用于标记是新的分享

[userDefaults setBool:YES forKey:@"has-new-share"];

[activityIndicatorView stopAnimating];

[theController.extensionContext completeRequestReturningItems:@[extItem] completionHandler:nil];

}

}];

hasExistsUrl = YES;

*stop = YES;

}

}];

if (hasExistsUrl)

{

*stop = YES;

}

}];

if (!hasExistsUrl)

{

//直接退出

[self.extensionContext completeRequestReturningItems:@[] completionHandler:nil];

}

// This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.

// Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.

//    [self.extensionContext completeRequestReturningItems:@[] completionHandler:nil];

}

5.容器程序获取分享数据

在AppDelegate.m文件找到

- (void)applicationDidBecomeActive:(UIApplication *)application方法里写:

//获取共享的UserDefaults

NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.weaventMind.weaventMind"];

if ([userDefaults boolForKey:@"has-new-share"])

{

NSLog(@"新的分享 : %@", [userDefaults valueForKey:@"share-url"]);

//重置分享标识

[userDefaults setBool:NO forKey:@"has-new-share"];

}

到此,基本完成!我项目功能需要的比较简单所以,本文我只针对我的项目操作。大家可以看下面参考文章,作者写的很详细,很不错!

6.注意事项:

1.做好分享插件的提示操作(由于获取附件是一个异步过程,那么,就需要做好界面上的提示。否则,分享界面消失后由于没有操作提示,会使用户误以为界面进行卡死的状态,其实是分享内容还没有处理完成。接下来就是优化UI上的提示操作)。

2.扩展中的处理不能太长时间阻塞主线程(建议放入线程中处处理),否则可能导致苹果拒绝你的应用。

3.扩展不能单独提审,必须要跟容器程序一起提交AppStore进行审核。

4.提审的扩展和容器程序的Build Version要保持一致,否则在上传审核包的时候会提示警告,导致程序无法正常提审。

7.参考文章

iOS扩展开发攻略(一) - Share Extension

2014-07-26 13:25:23 sunyazhou13 阅读数 791

iOS 8新特性之扩展

 

扩展概述

扩展(Extension)是iOS 8中引入的一个非常重要的新特性。扩展让app之间的数据交互成为可能。用户可以在app中使用其他应用提供的功能,而无需离开当前的应用。

在iOS 8系统之前,每一个app在物理上都是彼此独立的,app之间不能互访彼此的私有数据。

 

而在引入扩展之后,其他app可以与扩展进行数据交换。基于安全和性能的考虑,每一个扩展运行在一个单独的进程中,它拥有自己的bundle, bundle后缀名是.appex。扩展bundle必须包含在一个普通应用的bundle的内部。

iOS 8系统有6个支持扩展的系统区域,分别是Today、Share、Action、Photo Editing、Storage Provider、Custom keyboard。支持扩展的系统区域也被称为扩展点。

Today Widget

对于赛事比分,股票、天气、快递这类需要实时获取的信息,可以在通知中心的Today视图中创建一个Today扩展实现。Today扩展又称为Widget。

 

Share

在iOS 8之前,用户只有Facebook,Twitter等有限的几个分享选项可以选择。如果希望将内容分享到Pinterest,开发者则需要一些额外的努力。在iOS 8中,开发者可以创建自定义的分享选项。

Action

action在所有支持的扩展点中扩展性最强的一个。它可以实现转换另一个app上下文中的内容。苹果在WWDC大会上演示了一个Bing翻译动作扩展,它可以将在Safari中选中的文本翻译成不同的语言。

 

Photo Editing

在iOS 8之前,如果你想为你的照片添加一个特殊的滤镜,你需要进入第三方app中,这个过程是相当繁琐的。在iOS 8中,你可以直接在Photos中使用第三方app,如Instagram,VSCO cam、Aviary提供的Photo Editing扩展完成对图片的编辑,而无需离开当前的app。

Storage Provider

Storage Provider让跨多个文件存储服务之间的管理变得更简单。类似Dropbox、Google Drive等存储提供商通过在iOS 8中提供一个Storage Provider扩展,app直接可以使用这些扩展检索和存储文件而不再需要创建不必要的拷贝。

Custom Keyboard

苹果公司在2007年率先推出了触摸屏键盘,但一直没多大改进。在这一方面,Android则将键盘权限开放给了第三方开发者,所以出现了许多像Swype,SwiftKey等优秀的键盘输入法。在iOS 8中,苹果终于将键盘权限开发给了第三方开发者,自定义键盘输入法可以让用户在整个系统范围内使用。

 

二、创建扩展与发布扩展

在创建扩展之前,你需要创建一个用来包含扩展的常规的app项目。这个包含扩展的app被称为containing app。在创建好containg app之后,选择File->New->Target菜单,从弹出的对话框中选择一个适当的扩展目标模板。每一个扩展目标模板都包含了与扩展点相关的文件和设置。一个containing app可以包含多个不同类型的扩展。

每一个扩展目标模板包含一个头文件和实现文件,一个Info.plist文件,以及一个storyboard文件。Info.plist文件包含了对扩展的配置信息,其中最重要的键是NSExtension。下面列出了一个NSExtension可能包含的常用键值对。

<key>NSExtension</key>

 <dict>

    <key>NSExtensionAttributes</key>

    <dict>

            <key>NSExtensionActivationRule</key> <!--1-->

            <dict>

                <key>NSExtensionActivationSupportsImageWithMaxCount</key>

                <integer>10</integer>

                <key>NSExtensionActivationSupportsMovieWithMaxCount</key>

                <integer>1</integer>

                 </dict>

            <key>NSExtensionJavaScriptPreprocessingFile</key> <!--2-->

            <string>MyJavaScriptFile</string>

           <key>NSExtensionPointVersion</key>

           <string>1.0</string>

       </dict>

     <key>NSExtensionMainStoryboard</key>  <!--3-->

     <string>MainInterface</string>

       <key>NSExtensionPointIdentifier</key>  <!--4-->

       <string>com.apple.ui-services</string>

     <key>NSExtensionPrincipalClass</key>  <!--5-->

       <string>ActionViewController</string>

</dict>

1)   NSExtensionActivationRule定义了当前的扩展支持的数据类型及数据项个数,例如当前的设置只支持图片格式和视频格式的数据,并且最多不超过10张图片和1个视频。

2)   NSExtensionJavaScriptPreprocessingFile用于配置与脚本交互的JS脚本文件的名字。

3)   NSExtensionMainStoryboard配置扩展的Storyboard文件名。

4)   NSExtensionPointIdentifier用于表示扩展点,每一个扩展点拥有一个唯一的名字。

5)   NSExtensionPrincipalClass配置当扩展启动时,扩展点首先要实例化的类

为了将扩展提交苹果商店,你需要提交你的containg app。并且需要注意,除了扩展必须包含功能以外,同时containg app还需要提供一些功能,而针对OS X平台的扩展则无此限制。当用户安装了你的containg app,containg app中包含的扩展也会一同被安装。

三、理解扩展如何运作

在安装扩展之后,扩展并不会自动运行,用户必须执行特定的操作来启用扩展。如果是Today扩展,用户可以在通知中心的Today视图中编辑启用扩展。如果是自定义键盘扩展,用户需要在系统设置的通用选项下的键盘选项中启用自定义键盘扩展。而如果是Share扩展,用户只需点击系统提供的分享按钮,即可在分享列表中找到分享扩展。

一个扩展并不是一个app,它的生命周期和运行环境不同于普通app。在生命周期方面,扩展的生命周期从用户在另一个app中选择了扩展开始,一直到扩展完成了用户的请求生命周期结束。在运行环境方面,扩展的限制要比普通app更严格,扩展的可用内存上限以及可用的API都比普通app要少。严格限制扩展的内存是因为在同一时间可能会有多个扩展同时运行,如Widget扩展。如果API声明包含NS_EXTENSION_UNAVAILABLE宏,则此API在扩展中将不可用,常见的API如:

+ (UIApplication *)sharedApplication NS_EXTENSION_UNAVAILABLE_IOS("Use view controller based solutions where appropriate instead.");

调用扩展的应用称为host app,对于Widget扩展,host app就是Today。host app会在扩展的有效生命周期内定义一个扩展上下文。通过扩展上下文,host app可以和扩展互传数据。注意,扩展只和host app直接通信,扩展与containg app以及containing app与host app之间不存在通信关系,如果扩展需要打开containg app,则通过自定义URL scheme方式实现,而不是直接向containg app发送消息。三者的关系见下图:

扩展是一个单独的个体。扩展拥有独立的target,独立的bundle文件,独立的运行进程,独立的地址空间。这意味着即使你的containing app不在运行,系统也可以启动扩展。或者你的containing app处于挂起状态,同样不会影响扩展的运行。所以系统可以单独对扩展执行优化。扩展与containg app的关系:

四、设计扩展过程中常见的几个问题

1. containg app与扩展如何通过扩展上下文互传数据

在iOS 8中,UIViewController新增了一个扩展上下文属性extensionContext。来处理containing app与扩展之间的通信,上下文的类型是NSExtensionContext。假设你现在需要在host app中将一张图片传递给扩展做滤镜处理,host app中的代码如下:

    UIActivityViewController *activityViewController = [[UIActivityViewController alloc] initWithActivityItems:@[[self.imageView image]] applicationActivities:nil];

    [self presentViewController:activityViewController animated:YES completion:nil];

当用户在弹出的Action列表中选择了扩展,扩展将被启动,然后在扩展的viewDidLoad方法中,通过extensionContext检索host app传回的数据项。扩展中的代码如下:

- (void)viewDidLoad {

    [super viewDidLoad];

    NSExtensionItem *imageItem = [self.extensionContext.inputItems firstObject];

    if(!imageItem){

        return;

    }

    NSItemProvider *imageItemProvider = [[imageItem attachments] firstObject];

    if(!imageItemProvider){

        return;

    }

   // 检查是否包含文本

    if([imageItemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]){

        [imageItemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeImage options:nil completionHandler:^(UIImage *image, NSError *error) {

            if(image){

                dispatch_async(dispatch_get_main_queue(), ^{

                    self.imageView.image = image;

                });

            }

        }];

       

    }

}

上述代码中,extensionContext表示一个扩展到host app的连接。通过extionContent,你可以访问一个NSExtensionItem的数组,每一个NSExtensionItem项表示从host app传回的一个逻辑数据单元。你可以从NSExtensionItem项的attachments属性中获得附件数据,如音频,视频,图片等。每一个附件用NSItemProvider实例表示。上述代码中NSItemProvider的loadItemForTypeIdentifier实例方法的第一个参数是(NSString *)kUTTypeImage,如果你需要处理的是文本类型,参数则为(NSString *)kUTTypeText,相应的处理代码则变成:

if([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeText]){

    [imageItemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeText options:nil completionHandler:^(NSAttributedString *string, NSError *error) {

        if (string) {

            // 在这里处理文本

        }

    }];

}

当扩展处理完host app传回的图片数据后,它需要将处理好的数据再传给host app。在扩展中的代码如下:

-(IBAction)done:(id)sender{

    NSExtensionItem* extensionItem = [[NSExtensionItem alloc] init];

    [extensionItem setAttachments:@[[[NSItemProvider alloc] initWithItem:[self.imageView image] typeIdentifier:(NSString*)kUTTypeImage]]];

   

    [self.extensionContext completeRequestReturningItems:@[extensionItem] completionHandler:nil];

}

最后一步是host app接收来自扩展传回的数据,在host app中的代码如下:

[activityViewController setCompletionWithItemsHandler:^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError * error){

        if([returnedItems count] > 0){

            NSExtensionItem* extensionItem = [returnedItems firstObject];

            NSItemProvider* imageItemProvider = [[extensionItem attachments] firstObject];

            if([imageItemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]){

                [imageItemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeImage options:nil completionHandler:^(UIImage *item, NSError *error) {

                    if(item && !error){

                        dispatch_async(dispatch_get_main_queue(), ^{

                            [self.imageView setImage:item];

                        });

                    }

                }];

               

            }

        }

    }];

上述代码主要是通过设置一个completionBlock处理数据回调。

注意,所有的扩展都是一个UIViewController。所以UIViewController的所有生命周期方法,如viewWillAppear:、viewWillDisappear:等在扩展中都是可以使用的。

2. 如何在扩展中打开containing app

在一般情况下,扩展和containing app不存在通信关系。但是有时候需要在扩展中打开containing app,如iOS 7中预置的日历Widget。在常规的app中,可以使用如下代码在A app中打开B app:

    if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:customURL]]) {

        [[UIApplication sharedApplication] openURL:[NSURL URLWithString:customURL]];

    }

但是之前有讲到,sharedApplication API在扩展中被禁止使用,所以为了实现同样的功能,NSExtensionContext定义了一个新的方法用来打开containing app:

- (void)openURL:(NSURL *)URL completionHandler:(void (^)(BOOL success))completionHandler;

在调用此方法之前,需要在containg app中定义一个自定义URL Scheme。定义方法可参见链接,最终的结果如下图:

在扩展中打开containing app的代码如下:

- (IBAction)openContainingApp:(id)sender {

    NSURL *url = [NSURL URLWithString:@"ExtensionDemo://"];

    [self.extensionContext openURL:url completionHandler:^(BOOL success) {

    }];

}

3. 如何实现containing app与扩展共享数据

扩展和containing app各自拥有自己的数据容器,虽然扩展内嵌在containing app的内部,但是它们并不可以互访彼此的数据。为了实现containing app与扩展的数据共享,苹果在iOS 8中引入了一个新的概念——App Group。为了开启App Group,找到你的containing app目标,在右侧找到Capabilities标签,定位到App Groups分组,如下图所示。

 

然后选择你需要共享数据的扩展目标,重复执行一次操作,注意两次的App Group名要相同,不要添加新的条目。当开启App Group后,你可以使用NSUserDefaults方法访问共享区域,如下述代码,注意不是[NSUserDefaults standardUserDefaults]:

_sharedUserDefault= [[NSUserDefaults alloc] initWithSuiteName:@"group.com.aegeaon.ExtensionDemo"];

你也可以使用NSFileManager的containerURLForSecurityApplicationGroupIdentifier方法访问共享数据区:

- (BOOL)saveTextByNSFileManager {

    NSError *err = nil;

    NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.wangzz"];

    containerURL = [containerURL URLByAppendingPathComponent:@"Library/Caches/good"];

 

    NSString *value = _textField.text;

    BOOL result = [value writeToURL:containerURL atomically:YES encoding:NSUTF8StringEncoding error:&err];

    if (!result) {

        NSLog(@"%@",err);

    } else {

        NSLog(@"save value:%@ success.",value);

    }

 

    return result;

}

App Group区域在containing app与扩展之间所处的关系图:

 

你可能注意到了,在Xcode 6中iPhone模拟器的位置已经发生了变化。与此同时,在iOS 8 release Note中有提到,app的沙盒结构已经发生了改变,现在它被划分成了三个容器,Bundle容器、Data容器、iCloud容器。iOS 8 app沙盒目录结构如下图:

为了具体了解沙盒目录的布局,使用如下代码分别在containing app和扩展中打印出App Group目录,app bundle目录,以及Document目录:

- (void)logAppPath

{

    //app group路径

    NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.com.aegeaon.ExtensionDemo"];

    NSLog(@"app group:\n%@",containerURL.path);

   

    //打印可执行文件路径

    NSLog(@"bundle:\n%@",[[NSBundle mainBundle] bundlePath]);

   

    //打印documents

    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);

    NSString *path = [paths objectAtIndex:0];

    NSLog(@"documents:\n%@",path);

}

在containing app中执行logAppPath方法的结果如下:

app group:

/Users/aegaeon/Library/Developer/CoreSimulator/Devices/72CA5D31-9509-4076-BC94-BF4D29DC0151/data/Containers/Shared/AppGroup/5B4CFBD8-D95D-4F01-9268-D9F79792147D

bundle:

/Users/aegaeon/Library/Developer/CoreSimulator/Devices/72CA5D31-9509-4076-BC94-BF4D29DC0151/data/Containers/Bundle/Application/EED1F771-A8AD-4A97-97F3-2B0A57936C17/ExtensionDemo.app

documents:

/Users/aegaeon/Library/Developer/CoreSimulator/Devices/72CA5D31-9509-4076-BC94-BF4D29DC0151/data/Containers/Data/Application/95DBF43A-8B4B-426C-9A3A-C1745FCB3FA2/Documents

在扩展中执行logAppPath方法的结果如下:

app group:

/Users/aegaeon/Library/Developer/CoreSimulator/Devices/72CA5D31-9509-4076-BC94-BF4D29DC0151/data/Containers/Shared/AppGroup/5B4CFBD8-D95D-4F01-9268-D9F79792147D

bundle:

/Users/aegaeon/Library/Developer/CoreSimulator/Devices/72CA5D31-9509-4076-BC94-BF4D29DC0151/data/Containers/Bundle/Application/EED1F771-A8AD-4A97-97F3-2B0A57936C17/ExtensionDemo.app/PlugIns/ExpressExt.appex

documents:

~/Documents

其中标注为红色的意思是每次运行目录名都会发生变化。标注为绿色的表示文件名不会变化的,标准为橘色也验证了iOS 8中沙盒目录被划分的说法。其中也可以看出扩展扩展名为appex,它包含在containing app的PlugIns目录内。下图展示了扩展目录在Finder中的结构:

4. 如何让扩展访问到网页内容

在WWDC上,苹果演示了在Safari for iOS中使用Bing Action扩展将当前页面翻译为其他语言。考虑一下,为了完成这个功能,扩展和浏览器之间一定要建立一个连接,浏览器负责将选中的文本发给扩展,扩展将翻译的结果发回浏览器。为了实现这个机制,这里需要借助一个Javascript脚本,使用JS脚本可以访问网页的DOM。脚本的内容很简单,只包含两个方法,脚本文件名为MyJavaScriptFile.js。代码如下:

var MyExtensionJavaScriptClass = function() {};

 

MyExtensionJavaScriptClass.prototype = {

    run: function(arguments) {

        arguments.completionFunction({"baseURI": document.baseURI});

    },

   

    finalize: function(arguments) {

        var newContent = arguments["content"];

        document.write(newContent);

    }

};

var ExtensionPreprocessingJS = new MyExtensionJavaScriptClass;

其中包含一个run()和finalize()方法。当Safari一加载好你的JS文件,就会立即调用run方法,当你在扩展中调用了completeRequestReturningItems:expirationHandler:completion:方法,Safari会调用finalize()方法。在run()方法中,Safari提供了一个arguments参数,使用它可以利用键值对的形式将数据传给扩展。在上述代码中,传给扩展的键值对是:@{@"baseURI" : document.baseURI}。在finalize()方法中,当你调用了completeRequestReturningItems:expirationHandler:completion:方法,方法第一个参数的值会传给finalize()方法的arguments形参。在上述代码中,finalize()接收到参数后,将内容写入了当前的文档。

为了Safari能够调用正确调用到JS文件,需要在扩展的Info.plist文件中添加如下配置:

<key>NSExtensionAttributes</key>

 <dict>

      <key>NSExtensionJavaScriptPreprocessingFile</key>

      <string>MyJavaScriptFile</string>

  </dict>

在你的扩展中,为了取得从JS脚本传回的键值对,你需要为NSItemProvider的方法loadItemForTypeIdentifier:options:completionHandler:指定kUTTypePropertyList数据类型,在取得返回的键值字典后,使用NSExtensionJavaScriptPreprocessingResultsKey键取值,代码如下:

    NSExtensionContext *context = self.extensionContext;

    NSExtensionItem *item = context.inputItems.firstObject;

    NSItemProvider *provider = item.attachments.firstObject;

   

    [provider loadItemForTypeIdentifier:(NSString *)kUTTypePropertyList

                                options:nil

                      completionHandler:^(id<NSSecureCoding> item, NSError *error) {

                          NSDictionary *results = (NSDictionary *)item;

                          NSString *baseURI = [[results objectForKey:NSExtensionJavaScriptPreprocessingResultsKey] objectForKey:@"baseURI"];

                          NSLog(@"%@", baseURI);                         

                      }];

为了在扩展中将处理后的结果传给脚本,你需要使用NSItemProvider的initWithItem:typeIdentifier:包装键值对。代码如下:

NSExtensionItem *extensionItem = [[NSExtensionItem alloc] init];

extensionItem.attachments = @[[[NSItemProvider alloc] initWithItem: @{NSExtensionJavaScriptFinalizeArgumentKey: @{@"content":@"Hello World"}} typeIdentifier:(NSString *)kUTTypePropertyList]];

 [[self extensionContext] completeRequestReturningItems:@[extensionItem] expirationHandler:nil completion:nil];

5. 如何在containing app与扩展之间共享代码

iOS 8中,你可以内嵌一个framework文件在扩展和containing app之间共享代码。假设你希望在你的containing app与扩展之间共享图片处理的代码,此时你可以将代码打包成framework文件,内嵌到两个目标中。对于内嵌框架中的代码,确保不包含扩展不允许使用的API。

如何将代码打包成framework文件这里就不敖述了,感兴趣的同学可以参见:http://blog.sina.com.cn/s/blog_407fb5bc01013v6s.html。当你创建好.framework文件后,你可以直接将.framework文件同时拖入containing app和扩展中,如下图所示:

 

这里使用公司ILSLib目录下的的MagicalRecord21.framework文件作为素材,讲解如何在containing app和自定义键盘扩展之间实现共享Core Data数据库。在你的扩展和containing app中中配置好引用头文件。分别在containing app的AppDelegate文件的application: didFinishLaunchingWithOptions: launchOptions与自定义键盘扩展的UIInputViewController子类文件中viewDidLoad方法中添加如下代码:

[MagicalRecord setupCoreDataStackWithStoreNamed:@"demo.sqlite"];

上述代码分别对containing app和扩展执行Core Data栈初始化,其中包括数据模型、sqlite存储文件等配置。运行containing app,此时AppDelegate中的数据库配置代码会被执行,接着打开系统设置中的通用选项下的键盘选项,在这里启用自定义键盘。然后回到containing app,切换到自定义键盘扩展,此时自定义键盘扩展中viewDidLoad方法中的数据库配置代码执行,但是控制台出现错误提示:

CoreData: error: -addPersistentStoreWithType:SQLite configuration:(null) URL:~/Library/Application%20Support/CustomKeyboardExt/demo.sqlite -- file:/// options:(null) ... returned error Error Domain=NSCocoaErrorDomain Code=512 "The operation couldn’t be completed. (Cocoa error 512.)" UserInfo=0x7b48a720 {reason=Failed to create file; code = 2} with userInfo dictionary {

    reason = "Failed to create file; code = 2";

}

上述错误表示在扩展的~/Library/Application%20Support/CustomKeyboardExt/demo.sqlite目录创建.sqlite文件失败。翻阅MagicalRecord源代码(需要从github重新下载源代码,.framework看不到源代码),其中在创建.sqlite存储文件路径的代码中会发现:

+ (NSURL *) MR_urlForStoreName:(NSString *)storeFileName {

NSArray *paths = [NSArray arrayWithObjects:[self MR_applicationDocumentsDirectory], [self MR_applicationStorageDirectory], nil];

    NSFileManager *fm = [[NSFileManager alloc] init];

    for (NSString *path in paths) {

        NSString *filepath = [path stringByAppendingPathComponent:storeFileName];

        if ([fm fileExistsAtPath:filepath]) {

            return [NSURL fileURLWithPath:filepath];

        }

    }

 

    //set default url

    return [NSURL fileURLWithPath:[[self MR_applicationStorageDirectory] stringByAppendingPathComponent:storeFileName]];

}

其中MR_applicationStorageDirectory方法返回的是Application Support目录,而这个目录是处在Library目录内的。上文中已经讲过,扩展没有Documents目录,同样也是没有Library目录。所以文件创建会发生失败。为了实现扩展与containing app之间共享.sqlite文件,这里需要将.sqlite文件创建在App Group区域。问题是MagicalRecord21.framework文件只暴露了头文件,无法对其源文件中的MR_urlForStoreName:方法做修改。这里使用Objective-C的动态运行时技术——Method Swizzling,在运行时将MR_urlForStoreName:方法的实现使用新的实现进行替换。 (注:这里可以直接给setupCoreDataStackWithStoreNamed方法传递一个包含文件路径的URL类型参数实现修改.sqlite文件的存放位置,methodSwizzling只是另一种通用处理方法)

首先需要为自定义键盘扩展创建一个Category文件NSPersistentStore+Tracking.h/m,.m文件中的完整的代码如下:

#import "NSPersistentStore+Tracking.h"

#import <objc/runtime.h>

#import <MagicalRecord21/CoreData+MagicalRecord.h>

 

static NSString * const kGroupName = @"group.com.aegeaon.ExtensionDemo";

static NSString * const kContainingDirectory = @"CoreDataStore/";

 

@implementation NSPersistentStore (Tracking)

 

+ (void)load {

    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{

        Class class = [self class];

       

        SEL originalSelector = @selector(MR_urlForStoreName:);

        SEL swizzledSelector = @selector(ILS_urlForStoreName:);

       

        SwizzleClassMethod(class, originalSelector, swizzledSelector);

    });

}

 

void SwizzleClassMethod(Class c, SEL orig, SEL new) {

   

    Method origMethod = class_getClassMethod(c, orig);

    Method newMethod = class_getClassMethod(c, new);

   

    c = object_getClass((id)c);

   

    if(class_addMethod(c, orig, method_getImplementation(newMethod), method_getTypeEncoding(newMethod)))

        class_replaceMethod(c, new, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));

    else

        method_exchangeImplementations(origMethod, newMethod);

}

 

 

#pragma mark - Method Swizzling

+ (NSURL *) ILS_urlForStoreName:(NSString *)storeFileName {

    NSURL *storeURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:kGroupName];

    storeURL = [storeURL URLByAppendingPathComponent:[kContainingDirectory stringByAppendingString:storeFileName]];

   

    return storeURL;

}

 

@end

在当前的代码一载入内存,load方法将被执行,它比AppDelegate的application: didFinishLaunchingWithOptions: launchOptions方法要先被执行,上述代码会将MR_urlForStoreName:的实现替换成ILS_urlForStoreName:,在ILS_urlForStoreName:方法中,使用NSFileManager的containerURLForSecurityApplicationGroupIdentifier方法设定App Group,最终的.sqlite文件将保存在App Group目录内的CoreDataStore目录下。同样需要为containing app中使用此方法,可以直接将NSPersistentStore+Tracking.h/m拖入containing app目标内。再次运行自定义键盘扩展,数据库文件已成功保存到App Group中。如下图:

 

同时被共享的代码框架MagicalRecord21.framework被containg app和扩展共享,双方共用一个框架文件,如下图:

6. 如何在扩展中处理长时间任务

用户希望在扩展完成他们的任务之后能够立即返回到host app中。但是如果扩展执行的任务是一个长时间任务,比如下载。在这种情况下,需要使用NSURLSession来创建一个下载session,并初始化一个后台下载任务。当扩展初始化了上传下载任务后,就算是完成了host app的请求,扩展就可以被终止。这不会影响到任务的结果。如果当后台任务完成后,你的扩展不在运行,系统将在后台启动你的contaiing app并调用appdelegate的aplication:handleEventsForBackgroundURLSession:completionHandler:方法。为了在扩展中初始化一个后台的NSURLSession任务,你必须设置一个containing app和扩展都可以访问的共享容器。

相关代码如下:

NSURLSession *mySession = [self configureMySession];

NSURL *url = [NSURL URLWithString:@"http://www.example.com/LargeFile.zip"];

NSURLSessionTask *myTask = [mySession downloadTaskWithURL:url];

[myTask resume];

 

- (NSURLSession *) configureMySession {

    if (!mySession) {

        NSURLSessionConfiguration* config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@“com.mycompany.myapp.backgroundsession”];

        config.sharedContainerIdentifier = @"com.mycompany.myappgroupidentifier";

        mySession = [NSURLSession sessionWithConfiguration:config delegate:selfdelegateQueue:nil];

    }

    return mySession;

}

参考文档


本文章转自:http://www.cnblogs.com/xdream86/p/3855932.html

2016-08-07 16:44:16 weixin_33851177 阅读数 11

目录


作者感言

这次的Today Extension预研,让我觉得自己还有很多的不足,因为还有很多东西都没有去仔细的去研究,以后接下来会继续再接再厉。

最后: 如果你有更好的建议或者对这篇文章有不满的地方, 请联系我, 我会参考你们的意见再进行修改, 联系我时, 请备注Today Extension, 祝大家学习愉快~谢谢~

Cain(罗家辉)

zhebushimengfei@qq.com: 联系方式

350116542: 腾讯QQ


简介

Today Extension是在iOS 8之后所推出来的重大更新之一,在此之前, 或许有人看过部分App就已经实现过这些功能,但那种实现方式是并不是系统所提供的,所以在性能方面需要打个问号。


创建Today Extension

开始创建Today Extension

选择Today Extension

激活Today Extension


使用Storyboard实现Today Extension

在创建好Today Extension时,Xcode会自动创建一个对应的MainInterface.storyboard文件,并且与Today ExtensionController关联,打开MainInterface.storyboard, 我们会看到有一个内容为Hello WorldUILabel,废话少说现在我们来看看运行效果。

选择你需要关联启动的App

不要怀疑,就是这么简单的,Today Extension就这么出来了。


打开数据共享服务

不过,骚年郎们别着急,只是展示个Hello World而已,别高兴得太早,接下来我们讲重头戏,也就是应用AppToday Extension的数据交互,在此之前, 我们需要打开两个服务。

首先是主程序里的

再者呢,就是Today Extension里的

做完这两个操作之后,我们会看到多出来的两个证书

PS:这个证书是收费的, 如果没有去申请,一个账号可以免费测试10个证书,主应用1个,Today Extension插件1个,也就是说一个应用需要两个。


删掉Storyboard

接下来之前,我们要把MainInterface.storyboard给干掉,毕竟代码才是王道(个人观点,不喜勿喷)如果喜欢用Storyboard的朋友,也有一个Storyboard版本的,后面再补上,废话就不多说了,上教程。

找到TodayI Extension中的Info.plist文件,看到这小样就在这,先留着先

手动添加NSExtensionPrincipalClass字段 并设为TodayViewController(这个Controller你可以自己指定,我这里为了方便,就直接拿Xcode生成的)

现在我们可以把storyboard这小样给删掉了

再运行,你就会看到整个Today Extension是空的了,只有一个空图标和一个标题。


代码实现

主应用中,我们需要设置一下NSUserDefault

- (void)viewDidLoad {
    [super viewDidLoad];

    NSUserDefaults *userDefault = [[NSUserDefaults alloc] initWithSuiteName:@"group.todayExtensionCodeExample"];
    [userDefault setObject:@"tips" forKey:@"group.todayExtensionCodeExample.tips"];
}复制代码

现在我们进入TodayViewController开始写代码了

interface TodayViewController () 

@property (nonatomic, strong) UIView *contentView;
@property (nonatomic, strong) UILabel *tipsLabel;

@end

@implementation TodayViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    /**
     *  设置Today Extension的Size
     *
     *  @param 0   Today Extension的宽度是不可变的,所以这里随便给个0就好了
     *  @param 150 高度是可控制的,这里我给了一个固定值150
     *
     *  @return CGSize
     */
    self.preferredContentSize = CGSizeMake(0, 150);

    /**
     *  初始化一个UIView,且设置它的属性
     */
    self.contentView = [[UIView alloc] initWithFrame:CGRectMake(0,
                                                                0,
                                                                self.view.frame.size.width,
                                                                self.view.frame.size.height)];
    self.contentView.backgroundColor = [UIColor whiteColor];

    [self.view addSubview:self.contentView];

    /**
     *  初始化一个Label,并且设置它的属性
     *
     */
    self.tipsLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 50, self.view.frame.size.width, 30)];

    self.tipsLabel.text            = @"这是一个测试代码";
    self.tipsLabel.numberOfLines   = 0;
    self.tipsLabel.textColor       = [UIColor blackColor];
    self.tipsLabel.backgroundColor = [UIColor redColor];

    [self.view addSubview:self.tipsLabel];

    /**
     *  获取主应用传过来的数据
     */
    NSUserDefaults *userDefault = [[NSUserDefaults alloc] initWithSuiteName:@"group.todayExtensionCodeExample"];

    NSString *nickName = [userDefault objectForKey:@"group.todayExtensionCodeExample.tips"];

    if (nickName) {

        NSString *message = @"今天XX又给你准备了很多惊喜哦,快去看看吧!";

        self.tipsLabel.text = [NSString stringWithFormat:@"%@,%@", nickName, message];
    }
}

/**
 *  该方法是用来设置Today Extension的偏移,默认会像左偏移
 *
 *  @param defaultMarginInsets UIEdgeInsets
 *
 *  @return UIEdgeInsets
 */
- (UIEdgeInsets)widgetMarginInsetsForProposedMarginInsets:(UIEdgeInsets)defaultMarginInsets {

    return UIEdgeInsetsZero;
}

/**
 *  该方法是用来刷新Today Extension数据的
 *
 *  @param completionHandler
 */
- (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);
}

@end复制代码

从Today Extension跳转至App

首先,我们需要添加Identifier,以及URL Schemes

PS:这里的IdentifierURL Schemes是你自己定义的,不能与其他ApplicationIdentifierURL Schemes相同,否则会造成冲突。

然后呢,我们去到主应用的AppDelegate.m文件中添加方法

最后,我们去到TodayViewController里补上对应的方法就好了


最终效果


注意点

PS:在保证代码正确的前提下,如果遇到Today Extension无法加载数据,或者其他异常,可以把Application删掉,插件也删掉,Clear一下Project,在运行即可。


补上几篇文章

iOS8Extension之Today插件

WWDC 2014 Session笔记 - iOS 通知中心扩展制作入门

如何用纯代码构建一个Widget(today extension)

2014-10-09 10:38:25 aini9080 阅读数 1989
总览
扩展 (Extension) 是 iOS 8 和 OSX 10.10 加入的一个非常大的功能点,开发者可以通过系统提供给我们的扩展接入点 (Extension point) 来为系统特定的服务提供某些附加的功能。对于 iOS 来说,可以使用的扩展接入点有以下几个:
1. Today 扩展 - 在下拉的通知中心的 "今天" 的面板中添加一个 widget
2. 分享扩展 - 点击分享按钮后将网站或者照片通过应用分享
3. 动作扩展 - 点击 Action 按钮后通过判断上下文来将内容发送到应用
4. 照片编辑扩展 - 在系统的照片应用中提供照片编辑的能力
5. 文档提供扩展 - 提供和管理文件内容
6. 自定义键盘 - 提供一个可以用在所有应用的替代系统键盘的自定义键盘或输入法



Timer Demo
Demo 做的应用是一个简单的计时器,即点击开始按钮后开始倒数计时,每秒根据剩余的时间来更新界面上的一个表示时间的 Label,然后在计时到 0 秒时弹出一个 alert,来告诉用户时间到,当然用户也可以使用 Stop 按钮来提前打断计时。其实这个 Demo 就是我的很早之前做的一个番茄工作法的 app 的原型。
初始工程运行起来的界面大概是这样的:

初始工程
 
简单说整个项目只有一个 ViewController,点击开始按钮时我们通过设定希望的计时时间来创建一个 Timer 实例,然后调用它的 start 方法。这个方法接收两个参数,分别是每次剩余时间更新,以及计时结束(不论是计时时间到的完成还是计时被用户打断)时的回调方法。另外这个方法返回一个 tuple,用来表示是否开始成功以及可能的错误。
 
剩余时间更新的回调中刷新界面 UI,计时结束的回调里回收了 Timer 实例,并且显示了一个 UIAlertController。用户通过点击 Stop 按钮可以直接调用 stop 方法来打断计时。直接简单,没什么其他的 trick。
 
我们现在计划为这个 app 做一个 Today 扩展,来在通知中心中显示并更新当前的剩余时间,并且在计时完成后显示一个按钮,点击后可以回到 app 本体,并弹出一个完成的提示。

添加扩展 Target
第一步当然是为我们的 app 添加扩展。正如在总览中所提到的,扩展是项目中的一个单独的 target。在 Xcode 6 中, Apple 为我们准备了对应各类不同扩展点的 target 模板,这使得向 app 中添加扩展非常容易。对于我们现在想做的 Today 扩展,只需点选菜单的 File > New > Target...,然后选择 iOS 中的 Application Extension 的 Today Extension 就行了。


在弹出的菜单中将新的 target 命名为 SimpleTimerTodayExtenstion,并且让 Xcode 自动生成新的 Scheme,以方便测试使用。我们的工程中现在会多出一个和新建的 target 同名的文件夹,里面主要包含了一个 .swift 的 ViewController 程序文件,一个叫做 MainInterface 的 storyboard 文件和 Info.plist。其中在 plist 里 的 NSExtension 中定义了这个 扩展的类型和入口,而配套的 ViewController 和 StoryBoard 就是我们的扩展的具体内容和实现了。

我们的主题程序在编译链接后会生成一个后缀为 .app 的包,里面包含主程序的二进制文件和各种资源。而扩展 target 将单独生成一个后缀名为 .appex 的文件包。这个文件包将随着主体程序被安装,并由用户选择激活或者添加(对于 Today widget 的话在通知中心 Today 视图中的编辑删增,对于其他的扩展的话,使用系统的设置进行管理)。我们可以看到,现在项目的 Product 中已经新增了一个扩展了。



如果你有心已经打开了 MainInterface 文件的话,可以注意到 Apple 已经为我们准备了一个默认的 Hello World 的 label 了。我们这时候只要运行主程序,扩展就会一并安装了。
将 Scheme 设为 Simple Timer 的主程序,Cmd + R,然后点击 Home 键将 app 切到后台,拉下通知中心。这时候你应该能在 Toady 视图中找到叫做 SimpleTimerTodayExtenstion 的项目,显示了一个 Hello World 的标签。如果没有的话,可以点击下面的编辑按钮看看是不是没有启用,如果在编辑菜单中也没有的话,恭喜你遇到了和 Session 视频里的演讲者同样的 bug,你可能需要删除应用,清理工程,然后再安装试试看。一般来说卸载再安装可以解决现在的 beta 版大部分的无法加载的问题,如果还是遇到问题的话,你还可以尝试重启设备(按照以往几年的 SDK 的情况来看,beta 版里这很正常,正式版中应该就没什么问题了)。

如果一切正常的话,你能看到的通知中心应该类似这样:



这种方式运行的扩展我们无法对其进行调试,因为我们的调试器并没有 attach 到这个扩展的 target 上。
有两种方法让我们调试扩展
一种是将 Scheme 设为之前 Xcode 为我们生成的 SimpleTimerTodayExtenstion,然后运行时选择从 Today 视图进行运行,如图;

另一种是在扩展运行时使用菜单中的 Debug > Attach to Process > By Process Identifier (PID) or name,然后输入你的扩展的名字(在我们的 demo 中是 com.onevcat.SimpleTimer.SimpleTimerTodayExtension)来把调试器挂载到进程上去。




在应用和扩展间共享数据 - App Groups
扩展既然是个 ViewController,那各种连接 IBOutlet,使用 viewDidLoad 之类的生命周期方法来设置 UI 什么的自然不在话下。我们现在的第一个难点就是,如何获取应用主体在退出时计时器的剩余时间。只要知道了还剩多久以及何时退出,我们就能在通知中心中显示出计时器正确的剩余时间了。
 
对 iOS 开发者来说,沙盒限制了我们在设备上随意读取和写入。但是对于应用和其对应的扩展来说,
Apple 在 iOS 8 中为我们提供了一种可能性,那就是 App Groups。App Groups 为同一个 vender 的应用或者扩展定义了一组域,在这个域中同一个 group 可以共享一些资源。对于我们的例子来说,我们只需要使用同一个 group 下的 NSUserDefaults 就能在主体应用不活跃时向其中存储数据,然后在扩展初始化时从同一处进行读取就行了。
 
首先我们需要开启 App Groups。得益于 Xcode 5 开始引入的 Capabilities,这变得非常简单(至少不再需要去 developer portal 了)。
选择主 target SimpleTimer,打开它的 Capabilities 选项卡,找到 App Groups 并打开开关,然后添加一个你能记得的 group 名字,比如 group.simpleTimerSharedDefaults。接下来你还需要为 SimpleTimerTodayExtension 这个 target 进行同样的配置,只不过不再需要新建 group,而是勾选刚才创建的 group 就行。



然后让我们开始写代码吧!首先是在主体程序的 ViewController.swift 中添加一个程序失去前台的监听(这里的主体程序是swift写的oc的另外处理),在 viewDidLoad 中加入:

  1. NSNotificationCenter.defaultCenter()   
  1.     .addObserver(self, selector: "applicationWillResignActive",name: UIApplicationWillResignActiveNotification, object: nil) 
然后是所调用的 applicationWillResignActive 方法:
  1. @objc private func applicationWillResignActive() { 
  1.     if timer == nil { 
  1.         clearDefaults() 
  1.     } else { 
  1.         if timer.running { 
  1.             saveDefaults() 
  1.         } else { 
  1.             clearDefaults() 
  1.         } 
  1.     } 
  1.   
  1. private func saveDefaults() {   
  1.     let userDefault = NSUserDefaults(suiteName: "group.simpleTimerSharedDefaults"
  1.     userDefault.setInteger(Int(timer.leftTime), forKey: "com.onevcat.simpleTimer.lefttime"
  1.     userDefault.setInteger(Int(NSDate().timeIntervalSince1970), forKey: "com.onevcat.simpleTimer.quitdate"
  1.   
  1.     userDefault.synchronize() 
  1.   
  1. private func clearDefaults() {   
  1.     let userDefault = NSUserDefaults(suiteName: "group.simpleTimerSharedDefaults"
  1.     userDefault.removeObjectForKey("com.onevcat.simpleTimer.lefttime"
  1.     userDefault.removeObjectForKey("com.onevcat.simpleTimer.quitdate"
  1.   
  1.     userDefault.synchronize() 

 
这样,在应用切到后台时,如果正在计时,我们就将当前的剩余时间和退出时的日期存到了 NSUserDefaults 中。这里注意,可能一般我们在使用 NSUserDefaults 时更多地是使用 standardUserDefaults,但是这里我们需要这两个数据能够被扩展访问到的话,我们必须使用在 App Groups 中定义的名字来使用 NSUserDefaults。

接下来,我们可以到扩展的 TodayViewController.swift 中去获取这些数据了。在扩展 ViewController 的 viewDidLoad 中,添加以下代码:
  1. let userDefaults = NSUserDefaults(suiteName: "group.simpleTimerSharedDefaults")   
  2. let leftTimeWhenQuit = userDefaults.integerForKey("com.onevcat.simpleTimer.lefttime")   
  3. let quitDate = userDefaults.integerForKey("com.onevcat.simpleTimer.quitdate"
  4.  
  5. let passedTimeFromQuit = NSDate().timeIntervalSinceDate(NSDate(timeIntervalSince1970: NSTimeInterval(quitDate))) 
  6.  
  7. let leftTime = leftTimeWhenQuit - Int(passedTimeFromQuit) 
  8.  
  9. lblTImer.text = "\(leftTime)"   
 
当然别忘了把 StoryBoard 的那个 label 拖出来:
  1. @IBOutlet weak var lblTImer: UILabel! 
再次运行程序,并开始一个计时,然后按 Home 键切到后台,拉出通知中心,perfect,我们的扩展能够和主程序进行数据交互了:

在应用和扩展间共享代码 - Framework
接下来的任务是在 Today 界面中进行计时,来刷新我们的界面。这部分代码其实我们已经写过(当然..确切来说是我写的,你可能只是看过),没错,就是应用中的 Timer.swift 文件。我们只需要在扩展的 ViewController 中用剩余时间创建一个 Timer 的实例,然后在更新的 callback 里设置 label 就好了嘛。但是问题是,这部分代码是在应用中的,我们要如何在扩展中也能使用它呢?

一个最直接也是最简单的想法自然是把 Timer.swift 加入到扩展 target 的编译文件中去,这样在扩展中自然也就可以使用了。但是 iOS 8 开始 Apple 为我们提供了一个更好的选择,那就是做成 Framework。单个文件可能不会觉得有什么差别,但是随着需要共用的文件数量和种类的增加,将单个文件逐一添加到不同 target 这种管理方法很快就会将事情弄成一团乱麻。你需要考虑每一个新加或者删除的文件影响的范围,以及它们分别需要适用何处,这简直就是人间地狱。提供一个统一漂亮的 framework 会是更多人希望的选择(其实也差不多成为事实标准了)。使用 framework 进行模块化的另一个好处是可以得益于良好的访问控制,以保证你不会接触到不应该使用的东西,然后,Swift 的 namespace 是基于模块的,因此你也不再需要担心命名冲突等等一摊子 objc 时代的烦心事儿。

现在让我们把 Timer.swift 放到 framework 里吧。首先我们新建一个 framework 的 target。File > New > Target... 中选择 Framework & Library,选中 Cocoa Touch Framework (配图中的另外几个选项可能在你的 Xcode 中是没有的,请无视它们,这是历史遗留问题),然后确定。按照 Apple 对 framework 的命名规范,也许 SimpleTimerKit 会是一个不错的名字。

接下来,我们将 Timer.swift 从应用中移动到 framework 中。很简单,首先将其从应用的 target 中移除,然后加入到新建的 SimpleTimerKit 的 Compile Sources 中。

确认在应用中 link 了新的 framwork,并且在 ViewController.swift 中加上 import SimpleTimerKit 后试着编译看看...好多错误,基本都是 ViewController 中说找不到 Timer 之类的。这是因为原来的实现是在同一个 module 中的,默认的 internal 的访问层级就可以让 ViewController 访问到关于 Timer 和相应方法的信息。但是现在它们处于不同的 module 中,所以我们需要对 Timer.swift 的访问权限进行一些修改,在需要外部访问的地方加上 public 关键字。关于 Swift 中的访问控制,可以参考 Apple 关于 Swift 的这篇官方博客,简单说就是 private 只允许本文件访问,不写的话默认是 internal,允许统一 module 访问,而要提供给别的 module 使用的话,需要声明为 public。修改后的 Timer.swift 文件大概是这个样子的。

修改合适的访问权限后,接下来我们就可以将这个 framework 链接到扩展的 target 了。链接以后编译什么的可以通过,但是会多一个警告:

这是因为作为插件,需要遵守更严格的沙盒限制,所以有一些 API 是不能使用的。为了避免这个警告,我们需要在 framework 的 target 中声明在我们使用扩展可用的 API。具体在 SimpleTimerKit 的 target 的 General 选项卡中,将 Deployment Info 中的 Allow app extension API only 勾选上就可以了。关于在扩展里不能使用的 API,都已经被 Apple 标上了 NS_EXTENSION_UNAVAILABLE,在这里有一份简单的列表可供参考,基本来说都是 runtime 的东西以及一些会让用户迷惑或很危险的操作(当然这个标记的方法很可能会不断变动,最终一切请以 Apple 的文档和实际代码为准)。

接下来,在扩展的 ViewController 中也链接 SimpleTimerKit 并加入 import SimpleTimerKit,我们就可以在扩展中使用 Timer 了。将刚才的直接设置 label 的代码去掉,换成下面的:
  1. override func viewDidLoad() {   
  2.     //... 
  3.   
  4.     if (leftTime > 0) { 
  5.         timer = Timer(timeInteral: NSTimeInterval(leftTime)) 
  6.         timer.start(updateTick: { 
  7.                 [weak self] leftTick in self!.updateLabel() 
  8.             }, stopHandler: nil) 
  9.     } else { 
  10.         //Do nothing now 
  11.     } 
  12.   
  13. private func updateLabel() {   
  14.     lblTimer.text = timer.leftTimeString 
 
我们在扩展里也像在 app 内一样,创建 Timer,给定回调,坐等界面刷新。运行看看,先进入应用,开始一个计时。然后退出,打开通知中心。通知中心中现在也开始计时了,而且确实是从剩余的时间开始的,一切都很完美:


通过扩展启动主体应用
最后一个任务是,我们想要在通知中心计时完毕后,在扩展上呈现一个 "完成啦" 的按钮,并通过点击这个按钮能回到应用,并在应用内弹出结束的 alert。
 
这其实最关键的在于我们要如何启动主体容器应用,以及向其传递数据。可能很多同学会想到 URL Scheme,没错通过 URL Scheme 我们确实可以启动特定应用并携带数据。但是一个问题是为了通过 URL 启动应用,我们一般需要调用 UIApplication 的 openURL 方法。如果细心的刚才看了 NS_EXTENSION_UNAVAILABLE 的同学可能会发现这个方法是被禁用的(这也是很 make sense 的一件事情,因为说白了扩展通过 sharedApplication 拿到的其实是宿主应用,宿主应用表示凭什么要让你拿到啊!)。为了完成同样的操作,Apple 为扩展提供了一个 NSExtensionContext 类来与宿主应用进行交互。用户在宿主应用中启动扩展后,宿主应用提供一个上下文给扩展,里面最主要的是包含了 inputItems 这样的待处理的数据。当然对我们现在的需求来说,我们只要用到它的 openURL(URL:,completionHandler:) 方法就好了。
 
另外,我们可能还需要调整一下扩展 widget 的尺寸,以让我们有更多的空间显示按钮,这可以通过设定 preferredContentSize 来做到。在 TodayViewController.swift 中加入以下方法:
  1. private func showOpenAppButton() {   
  2.     lblTimer.text = "Finished" 
  3.     preferredContentSize = CGSizeMake(0, 100) 
  4.   
  5.     let button = UIButton(frame: CGRectMake(0, 50, 50, 63)) 
  6.     button.setTitle("Open", forState: UIControlState.Normal) 
  7.     button.addTarget(self, action: "buttonPressed:", forControlEvents: UIControlEvents.TouchUpInside) 
  8.   
  9.     view.addSubview(button)         
 
在设定 preferredContentSize 时,指定的宽度都是无效的,系统会自动将其处理为整屏的宽度,所以扔个 0 进去就好了。在这里添加按钮时我偷了个懒,本来应该使用Auto Layout 和添加约束的,但是这并不是我们这个 demo 的重点。另一方面,为了代码清晰明了,就直接上坐标了。
 
然后添加这个按钮的 action:
  1. @objc private func buttonPressed(sender: AnyObject!) { 
  2.     extensionContext.openURL(NSURL(string: "simpleTimer://finished"), completionHandler: nil) 
 
我们将传递的 URL 的 scheme 是 simpleTimer,以 host 的 finished 作为参数,就可以通知主体应用计时完成了。然后我们需要在计时完成时调用 showOpenAppButton 来显示按钮,更新 viewDidLoad 中的内容:
  1. override func viewDidLoad() {   
  2.     //... 
  3.     if (leftTime > 0) { 
  4.         timer = Timer(timeInteral: NSTimeInterval(leftTime)) 
  5.         timer.start(updateTick: { 
  6.             [weak self] leftTick in self!.updateLabel() 
  7.             }, stopHandler: { 
  8.                 [weak self] finished in self!.showOpenAppButton() 
  9.             }) 
  10.     } else { 
  11.         showOpenAppButton() 
  12.     } 
最后一步是在主体应用的 target 里设置合适的 URL Scheme:

然后在 AppDelegate.swift 中捕获这个打开事件,并检测计时是否完成,然后做出相应:
  1. func application(application: UIApplication!, openURL url: NSURL!, sourceApplication: String!, annotation: AnyObject!) -> Bool {   
  2.     if url.scheme == "simpleTimer" { 
  3.         if url.host == "finished" { 
  4.             NSNotificationCenter.defaultCenter() 
  5.                 .postNotificationName(taskDidFinishedInWidgetNotification, object: nil) 
  6.         } 
  7.         return true 
  8.     } 
  9.   
  10.     return false 
 
在这个例子里,我们发了个通知。而在 ViewController 中我们可以一开始就监听这个通知,然后收到后停止计时并弹出提示就行了。当然我们可能需要一些小的重构,比如添加是手动打断还是计时完成的判断以弹出不一样的对话框等等,这些都很简单再次就不赘述了。

至此,我们就完成了一个很基本的通知中心扩展,完整的项目可以在 GitHub repo 的 master 上找到。这个计时器现在在应用中只在前台或者通知中心显示时工作,如果你退出应用后再打开应用,其实这段时间内是没有计时的。因此这个项目之后可能的改进就是在返回应用的时候添加一下计时的判定,来更新计时器的剩余时间,或者是已经完成了的话就直接结束计时。

其他
其实在 Xcode 为我们生成的模板文件中,还有这么一段代码也很重要:
  1. func widgetPerformUpdateWithCompletionHandler(completionHandler: ((NCUpdateResult) -> Void)!) {   
  2.     // Perform any setup necessary in order to update the view. 
  3.   
  4.     // If an error is encoutered, use NCUpdateResult.Failed 
  5.     // If there's no update required, use NCUpdateResult.NoData 
  6.     // If there's an update, use NCUpdateResult.NewData 
  7.   
  8.     completionHandler(NCUpdateResult.NewData) 
 
对于通知中心扩展,即使你的扩展现在不可见 (也就是用户没有拉开通知中心),系统也会时不时地调用实现了 NCWidgetProviding 的扩展的这个方法,来要求扩展刷新界面。这个机制和 iOS 7 引入的后台机制是很相似的。在这个方法中我们一般可以做一些像 API 请求之类的事情,在获取到了数据并更新了界面,或者是失败后都使用提供的 completionHandler 来向系统进行报告。
 
值得注意的一点是 Xcode (至少现在的 beta 4) 所提供的模板文件的 ViewController 里虽然有这个方法,但是它默认并没有 conform 这个接口,所以要用的话,我们还需要在类声明时加上 NCWidgetProviding。
 
总结
这个 Demo 主要涉及了通知中心的 Toady widget 的添加和一般交互。其实扩展是一个相当大块的内容,对于其他像是分享或者是 Action 的扩展,其使用方式又会有所不同。但是核心的概念,生命周期以及与本体应用交互的方法都是相似的。Xcode 在我们创建扩展时就为我们提供了非常好的模版文件,更多的时候我们要做的只不过是在相应的方法内填上我们的逻辑,而对于配置方面基本不太需要操心,这一点还是非常方便的。
 
就为了扩展这个功能,我已经迫不及待地想用上 iOS 8 了..不论是使用别人开发的扩展还是自己开发方便的扩展,都会让这个世界变得更美好。











2016-01-05 15:27:24 chenyong05314 阅读数 634

转载自:  http://stackoverflow.com/questions/24815957/maximum-height-of-ios-8-today-extension

I am working on a Today Extension with a dynamically sized table. I have been able to get the table to resize for the content using:

    self.preferredContentSize = accountsTable.contentSize

However, I have found that it will not get taller than a certain size (568 px) even though I can tell the table contentSize is larger.

I'm not clear if this is a built-in limit or if there is a way around this to make a larger view. It appears that some previous extensions (Stocks widget) is able to become larger.

Anyone else running into the same behavior. Anyone know if it's possible to make an extension appear larger either immediately or using a "Show All" button like the Stock widget?




14
down voteaccepted

I made some tests and you can calculate the maximum height of your Today Extension with this formular:

for iPhone:

float maxHeight = [[ UIScreen mainScreen ] bounds ].size.height - 126;

for iPad:

float maxHeight = [[ UIScreen mainScreen ] bounds ].size.height - 171;

This should work for alle Screen sizes...




没有更多推荐了,返回首页