当你在开发一个应用扩展,让其执行某个任务时,你可能会遇到一些常见的,或者说在多种扩展中都会出现的通用的问题,并且必须要处理这些问题。在这一章节中,我们将帮助你如何应对和处理这些常见的问题。

使用内嵌框架共享代码

你可以创建一个内嵌框架,用于在应用扩展和它的载体应用之间共享代码。比如,你开发了一个图片滤镜的照片编辑扩展,同时该扩展的载体应用也有这个功能,那么你就可以将实现该功能的代码封装成一个框架,让扩展target和载体应用target都使用这个框架。

你要确保你创建的内嵌框架不包含应用扩展不能使用的API。这类API一般使用unavailability宏来标记,比如像NS_EXTENSION_UNAVAILABLE

如果你创建的内嵌框架中包含应用扩展不能使用的API,那么你将其Link到载体应用没关系,载体应用可以正常使用框架中的API,但是不能与应用扩展共享代码,也就是应用扩展不能使用该框架提供的所有API,继而无法做到代码共享。如果你上传App Store的应用扩展中有这种框架,或者应用扩展中使用了不允许使用的API,那么审核时会被拒绝。

如果我们要想应用扩展使用内嵌框架,那么首先要配置一下。将targetRequire Only App-Extension-Safe API选项设置为Yes。如果你不这样设置,那么Xcode会向你提示警告:linking against dylib not safe for use in application extensions

重要提示:如果载体应用要使用内嵌框架,那么必须要支持arm64架构,否则在上传App Store时会被拒绝。(如“创建应用扩展”章节中介绍的,所有应用扩展都要支持arm64架构。)

在配置你的Xcode项目时,在Build Phases选项卡的Copy Files项中一定要将Destination设置为Frameworks

重要提示:我们要始终将Destination设置为Frameworks,如果你将其设置为SharedFramework,那么上传App Store时会被拒绝的。

你可以让载体应用支持iOS7或更早的版本,但当在iOS8或更新的版本中运行时,要特别注意内嵌框架的安全性。详细内容可以参阅Deploying a Containing App to Older Versions of iOS

有关创建和使用内嵌框架的更多内容,请观看WWDC 2014的视频“Building Modern Frameworks”。

与载体应用共享数据

虽然应用扩展的包内嵌在载体应用中,但是应用扩展和载体应用的安全域还是不一样的。默认情况下,应用扩展和载体应用是不能直接访问对方内容的。

不过你可以通过数据共享来实现这个愿望。比如,你希望应用扩展和它的载体应用共享一个单一的大数据集。比如prerendered assets

要实现数据共享,我们要使用Xcode或者开发者门户网站允许应用扩展和它的载体应用成为一个应用组,然后在开发者门户网站中注册应用组,并指明在载体应用中使用该应用组。关于应用组的知识请查阅Entitlement Key Reference文档的Adding an App to an App Group章节。

当你设置好应用组后,应用扩展和它的载体应用就可以通过NSUserDefaultsAPI共享访问用户的信息。我们可以使用initWithSuiteName:方法实例化一个NSUserDefaults对象,然后传入共享组的标示符。比如一个共享扩展,它或许会更新用户最近经常使用的共享账号,那么我们可以这样来写:


// Create and share access to an NSUserDefaults object.
NSUserDefaults *mySharedDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"com.example.domain.MyShareExtension"];


// Use the shared user defaults object to update the user's account.
[mySharedDefaults setObject:theAccountName forKey:@"lastAccountName"];

下图向我们展示了应用扩展和它的载体应用是如何通过共享容器实现数据共享的:

结构

重要提示:如果你的应用扩展使用NSURLSession类执行后台的上传下载任务时,你必须要设置一个共享容器。这样扩展和载体应用就可以访问到转换传输的数据。后台上传下载的更多知识请参阅 Performing Uploads and Downloads

如果你设置了共享容器,那么载体应用和它包含的扩展就可以对共享容器里的内容进行读写操作了。同时你还必须要对数据的操作进行同步,以避免数据损坏或出错。使用UIDocument类、Core Data或者SQLite可以帮助定位、协调操作共享容器中的内容。

访问网页

在分享扩展(iOS与OS X平台)和Action扩展(iOS平台)中,一般都允许用户使用Safari浏览器执访问网页并通过执行JavaScript脚本将结果返回到扩展中。你也可以在你的扩展运行之前或执行完任务之后通过JavaScript文件修改网页内容。比如分享扩展,它可以帮助用户将网页上的内容分享到用户的社交网站上,或者iOS上的Action扩展可能会显示当前网页的指定翻译内容。

如果要通过应用扩展访问网页并操作网页内容,那么需要遵循下面几个步骤:

  • 创建一个JavaScript文件,并申明一个全局对象,名为ExtensionPreprocessingJS并将其实例化。
  • 在应用扩展的属性列表文件中添加关键字NSExtensionJavaScriptPreprocessingFile,给Safari浏览器指明使用哪个JavaScript文件。
  • NSExtensionActivationRule字典中,将NSExtensionActivationSupportsWebURLWithMaxCount赋值一个非零的值。(更多关于NSExtensionActivationRule字典的知识请参阅Declaring Supported Data Types for a Share or Action Extension。)
  • 当你的应用扩展开始运行时,使用NSItemProvider类从JavaScript文件的返回值中获取结果。
  • 在iOS系统的应用扩展中,如果你希望当应用扩展执行完任务后实时更新网页内容,那么你要向JavaScript文件中传入内容。(在这一步中也使用NSItemProvider类。)

为了告知Safari你的应用扩展中包含一个JavaScript文件,你需要在应用扩展的Info.plist文件中,向NSExtensionAttributes字典添加NSExtensionJavaScriptPreprocessingFile关键字来指明你的JavaScript文件。这个关键字的值就是你希望当你的应用扩展运行前,Safari要加载的JavaScript文件的名称。比如:

<key>NSExtensionAttributes</key>
    <dict>
        <key>NSExtensionJavaScriptPreprocessingFile</key>
        <string>MyJavaScriptFile</string>
    </dict>

在iOS和OS X平台中,在你自定义的JavaScript类中可以定义一个run()函数,该函数就是Safari加载JavaScript文件的入口。在run()函数中,Safari提供了一个名为completionFunction的参数,你可以使用键值对象的形式将结果传给应用扩展。

在iOS平台中,你还可以定义一个finalize()函数,当应用扩展在任务结束阶段调用completeRequestReturningItems:expirationHandler:completion:方法时Safari会调用finalize()函数。在该函数中,可以通过向completeRequestReturningItems:expirationHandler:completion:方法传值,来改变网页内容。

比如,你的iOS应用扩展需要基于一个网页URI启动,并且当它结束运行时改变网页的背景色,那么你需要这样写JavaScript代码:


var MyExtensionJavaScriptClass = function() {};

MyExtensionJavaScriptClass.prototype = {
run: function(arguments) {
// Pass the baseURI of the webpage to the extension.
arguments.completionFunction({"baseURI": document.baseURI});
},

// Note that the finalize function is only available in iOS.
finalize: function(arguments) {
// arguments contains the value the extension provides in [NSExtensionContext completeRequestReturningItems:expirationHandler:completion:].
// In this example, the extension provides a color as a returning item.
document.body.style.backgroundColor = arguments["bgColor"];
}
};

// The JavaScript file must contain a global object named "ExtensionPreprocessingJS".
var ExtensionPreprocessingJS = new MyExtensionJavaScriptClass;

在iOS和OS X平台中,你需要编写代码来处理fun()函数返回的值,为获取到字典中的值,我们需要指定kUTTypePropertyList类型作为标示符传入NSItemProvider类的 loadItemForTypeIdentifier:options:completionHandler:方法。在该字典中使用NSExtensionJavaScriptPreprocessingResultsKey作为key来取值。比如下面例子中我们想要获取将URI传入run()的返回值:


[imageProvider loadItemForTypeIdentifier:kUTTypePropertyList options:nil completionHandler:^(NSDictionary *item, NSError *error) {
NSDictionary *results = (NSDictionary *)item;
NSString *baseURI = [[results objectForKey:NSExtensionJavaScriptPreprocessingResultsKey] objectForKey:@"baseURI"];
}];

finalize()函数是在当应用扩展执行完任务后传参并调用的,创建一个含有我们需要处理的值的字典,然后用NSItemProviderinitWithItem:typeIdentifier:方法来封装该字典。比如当扩展执行完任务后我们想让网页变为红色,我们可以这样写:


NSExtensionItem *extensionItem = [[NSExtensionItem alloc] init];
extensionItem.attachments = @[[[NSItemProvider alloc] initWithItem: @{NSExtensionJavaScriptFinalizeArgumentKey: @{@"bgColor":@"red"}} typeIdentifier:(NSString *)kUTTypePropertyList]];
[[self extensionContext] completeRequestReturningItems:@[extensionItem] expirationHandler:nil completion:nil];

执行上传下载任务

用户一般的操作习惯都倾向于当使用你的应用扩展完成某个任务后,可以将结果立即反馈在使用扩展的应用中。如果一个扩展要处理的任务包含较长时间的上传下载操作时,你要确保当你的应用扩展关闭后能继续完成该任务。为实现这个功能,我们需要使用NSURLSession类创建一个URL会话并创建后台的上传下载任务。

提示:你可以回想一下其他类型的后台任务,比如后台支持VoIP、后台播放音乐,这些是不能用应用扩展去实现的。更多信息请参阅Respond to the Host App’s Request

当你的应用扩展准备好上传下载任务后,扩展会完成调用它的应用发出的请求,并在不影响上传下载任务的前提下终止扩展。更多关于扩展处理主叫应用请求的知识请参阅Respond to the Host App’s Request。在iOS系统中,如果你的应用扩展在执行完后台任务时并没有在运行,那么系统会自动在后台运行扩展的载体应用,并调用application:handleEventsForBackgroundURLSession:completionHandler:代理方法。

重要提示:如果你的应用扩展在后台创建了NSURLSession任务,那么你必须要设置一个共享容器,以确保扩展和载体应用实现数据共享。我们可以在NSURLSessionConfiguration类中使用sharedContainerIdentifier属性来指定一个共享容器的标示符,然后我们就可以通过该标示符获取到共享容器。
请参阅Sharing Data with Your Containing App文档来设置共享容器。

下面的例子展示了如何配置一个URL会话,并创建一个下载任务:


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”];
// To access the shared container you set up, use the sharedContainerIdentifier property on your configuration object.
config.sharedContainerIdentifier = @“com.mycompany.myappgroupidentifier”;
mySession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
}
return mySession;
}

因为在单位时间内只能由一个进程使用后台会话,所以你需要为载体应用中的所有扩展创建不同的后台会话(每个后台会话都要有一个唯一的标示符)。在这里我们建议当载体应用在后台处理扩展的任务时,只使用一个该扩展创建的后台会话。如果你要执行其他的网络相关的任务,那么就要创建相应的URL会话。

如果你需要在后台创建URL会话之前完成主叫应用的请求,那么要确保创建和使用会话的代码是有效可执行的。当你的扩展调用completeRequestReturningItems:completionHandler:方法告知主叫应用已经完成相关请求后,系统就可以随时终止你的应用扩展。

为分享和Action扩展申明支持的数据类型

在你的分享或Action扩展中,在它们的工作中可能会使用到一些数据,并且这些数据的类型各不相同。为了确保只有当用户在主叫应用中选择了你的扩展支持的数据类型时,才会展示你的扩展功能。你需要在扩展的属性列表文件中添加NSExtensionActivationRule关键字。你也可以使用该关键字指定扩展处理每种类型的最大数目。当你的应用扩展运行时,系统会用改关键字的值与扩展数据项的attachments属性值作比较。关于NSExtensionActivationRule关键字的详细信息可以参阅Action Extension Keys文档中的Information Property List Key Reference章节。

比如,你可以申明你的分享扩展支持最大处理10张图片,一部影片和一个网站URL。你可以参考下面的写法:

<key>NSExtensionAttributes</key>
    <dict>
        <key>NSExtensionActivationRule</key>
        <dict>
            <key>NSExtensionActivationSupportsImageWithMaxCount</key>
            <integer>10</integer>
            <key>NSExtensionActivationSupportsMovieWithMaxCount</key>
            <integer>1</integer>
            <key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
            <integer>1</integer>
        </dict>
    </dict>

如果你想指定不支持的数据类型,那么你可以将该类型的值设置为0,或者在NSExtensionActivationRule中不添加该类型即可。

提示:如果你的分享扩展或iOS中的Action扩展需要访问网页,那你必须要确保NSExtensionActivationSupportsWebURLWithMaxCount关键字的值不为0(更多关于在应用扩展中通过JavaScript访问网页的内容请参阅Accessing a Webpage)。

你也可以使用NSExtensionItem定义的UTI子类型以便数据检测器检测文本信息,比如电话号码或通讯地址。

NSExtensionActivationRule字典中的关键字足以满足大多数应用的过滤需求。如果你需要做更复杂的过滤,比如像public.urlpublic.image之间的区别,那么你就得在文本中创建断言语句。如果你要创建一个断言,那么就将NSExtensionActivationRule关键字的值设置为你指定的断言字符串。(在运行时,系统会自动将该字符串编译为NSPredicate对象)

比如,一个应用扩展的附件属性可以指定为PDF文件,可以这样写:

{extensionItems = ({
    attachments =     (
                {
            registeredTypeIdentifiers =             (
             "com.adobe.pdf",
            "public.file-url"
            );
        }
    );
})}

为了指定你的应用扩展可以处理PDF文件,你可以像这样创建断言字符串:

SUBQUERY(extensionItems, $extensionItem, SUBQUERY($extensionItem.attachments, $attachment, ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "com.adobe.pdf").@count == 1).@count == 1

开发过程中,在你创建断言语句之前你可以使用TRUEPREDICATE常量(结果为true)测试你的代码路径。更多断言语句的语法知识请参阅Predicate Format String Syntax

重要提示:在将你的载体应用上传App Store之前,要确保所有的TRUEPREDICATE常量已经替换为指定的断言语句或NSExtensionActivationRule关键字,不然载体应用会被App Store拒绝。

配置载体应用以适用于老版本的iOS系统

如果你在载体应用中使用了内嵌框架,那么它就可以在iOS8.0之后的版本中使用,即便内嵌框架不支持老版本的系统也没关系。

使载体应用能做到上述这一点的是dlopen命令,它可以使你使用条件链接和加载框架包的机制。你可以使用这个命令来代替编译时链接,你可以在Xcode的General 选项或Build Phases选项中对该命令进行编辑。其原理就是只有当载体应用在iOS8.0或更高的版本中运行时,才会链接使用内嵌框架。

重要提示:如果你的载体应用使用了内嵌框架,那么就必须要支持arm64架构,否则会被App Store拒绝。

设置Xcode项目中应用扩展的条件链接

  1. 将每一个应用扩展的运行系统版本设置为iOS8.0或更高,通常选中Xcode中的target,在General选项中设置Deployment info
  2. 将你载体应用的运行系统版本设置为你想支持的最低iOS版本。
  3. 在你的载体应用中,通过systemVersion方法,在运行时检查判断iOS的版本,并判断是否执行dlopen命令。只有你的载体应用在iOS8.0或更高的版本中运行时才会指定dlopen命令。

特定的iOS API通过dlopen命令使用内嵌框架。你必须选择性的使用这些API,就像使用dlopen命令时那样。这些API都是CFBundleRef的封装类型:

还有来自NSBundle类的方法:

因为你一般会将载体应用的运行系统版本配置为较低的版本,所以这些API通常都是在运行时检查,只有确保载体应用在iOS8.0或更高版本中运行时才会使用这些API。

原文地址:Handling Common Scenarios

碰撞背后的故事

在UIKit的动态引擎中,每个行为都有一个action属性,它的类型是一个函数(() -> Void),我们可以使用一个闭包来打印一下每一步行为的信息。我们在viewDidLoad方法加入下面这行代码:


collision.action = {
println("\(NSStringFromCGAffineTransform(square.transform)) \(NSStringFromCGPoint(square.center))");
};

上面的代码记录了蓝绿色方块坠落、碰撞时的centertransform属性。当编译运行时,在方块下坠的前一秒我们可以在控制台看到如下信息:

[1, 0, 0, 1, 0, 0] {150, 150}
[1, 0, 0, 1, 0, 0] {150, 150}
[1, 0, 0, 1, 0, 0] {150, 151}
[1, 0, 0, 1, 0, 0] {150, 151}
[1, 0, 0, 1, 0, 0] {150, 152}
[1, 0, 0, 1, 0, 0] {150, 154}

我们可以看到,随着方块的坠落,它的center属性在不断变化,也就是中点的x坐标没变,y坐标在一直变化,这时方块还没有与barrier发生碰撞。当方块与红色障碍物barrier碰撞时,我们可以看到方块的transform属性也有了变化:

[0.999995500003375, 0.0029999955000020251, -0.0029999955000020251, 0.999995500003375, 0, 0] {150, 250}
[0.99970233476860393, 0.02439757894140453, -0.02439757894140453, 0.99970233476860393, 0, 0] {151, 249}
[0.99894218654750844, 0.045983779049604996, -0.045983779049604996, 0.99894218654750844, 0, 0] {152, 249}

通过这些信息我们得知,动态引擎通过不断改变ransformcenter两个数据模型的数据来驱动View的行为。

虽然这些数据的精确度可能不是很高,但关键在于这些数据让我们知道了动力引擎是如何进行驱动的,通过这些数据也让我们知道了在行为动作背后也是有据可循的。因此,我们能不能通过程序改变这些数据呢,也就是从另一个层面来控制物体的行为动作。如果是这样的话,那么就需要我们自己计算出一套运行轨迹和行为的数据,而不是通过动作引擎去控制了。

这里有一个协议,它描述了动作行为的数据模型,那就是UIDynamicItem,它遵循NSObjectProtocol协议。UIDynamicItem协议提供了两个可读写的属性centertransform,物体的运动轨迹靠这两个属性来计算。同时还提供了一个只读的属性bounds,该属性运动物体的边界,它用于描述碰撞物体的边界周长,这样就可以计算碰撞时该物体的受力大小,并做出相应的动作。

因为UIDynamicItem是一个协议,所以这就说明了它与UIView是松耦合的关系。在UIKit中还有一个类遵循这个协议,就是UICollectionViewLayoutAttributes,这意味着动作引擎不但可以作用于一个View,还可以作用于一个集合中的View。

碰撞通知

到目前为止,我们的程序中已经添加了两个View和两个行为,并让他们产生碰撞行为,那么下面我们将看看当他们互相发生碰撞时我们如何捕获这个行为呢。

我们回到ViewController.swift文件,让ViewController类遵循UICollisionBehaviorDelegate协议:


class ViewController: UIViewController, UICollisionBehaviorDelegate {

viewDidLoad方法中,设置碰撞行为collision的碰撞代理:


collision.collisionDelegate = self;

然后我们实现UICollisionBehaviorDelegate协议的一个方法:


func collisionBehavior(behavior: UICollisionBehavior!, beganContactForItem item: UIDynamicItem!, withBoundaryIdentifier identifier: NSCopying!, atPoint p: CGPoint) {
println("Boundary contact occurred - \(identifier)");
}

这个方法在View每次发生碰撞时调用。我们让它在控制台打印一些信息。为了能更好的查看该方法中打印的信息,我们将之前设置的collision.action打印信息先去掉。

我们编译运行一下,可以看到当View之间发生交互,也就是碰撞时,我们在控制台看到了打印信息:

Boundary contact occurred - barrier
Boundary contact occurred - barrier
Boundary contact occurred - nil
Boundary contact occurred - nil
Boundary contact occurred - nil
Boundary contact occurred - nil

从上面的打印信息中可以看出,坠落的方块与标示符为barrier的View发生了两次碰撞,这就是我们之前添加的那个隐性的红色障碍物。标示符为nil的几次碰撞就是方块与引用的屏幕边界发生的碰撞。

我们接着在该方法中添加代码:


let collidingView = item as UIView;
collidingView.backgroundColor = UIColor.yellowColor();
UIView.animateWithDuration(0.3) {
collidingView.backgroundColor = UIColor.grayColor();
}

通过上面的代码可以看出,我们定义了一个collidingView常量,用item参数将其赋值,实际上这就是发生碰撞的View,就是那个小方块。然后我们将它的背景色改为黄色,然后经过0.3秒将其背景色从黄色改为了灰色。我们编译运行一下看看:

luping

我们可以看到当方块每次发生碰撞行为时,它都会闪着黄色。

到目前为止,我们所看到的一切都是由动作引擎帮我们实现的,比如说碰撞质量、弹力等,那么接下来,你会看到如何通过UIDynamicItemBehavior类由我们自己控制这些行为属性。

配置行为属性

viewDidLoad方法中添加如下代码:


let itemBehaviour = UIDynamicItemBehavior(items: [square]);
itemBehaviour.elasticity = 0.6;
animator.addBehavior(itemBehaviour);

上面的代码中,我们创建了一个行为项,它与方块View相关联,修改它的弹力为0.6,然后将其添加到行为中。elasticity的最大值为1,这意味着当方块与障碍物发生碰撞时,所以不会损失碰撞能量和速度,所以方块会一直弹。

我们编译运行一下,看看现在当方块发生碰撞时会有什么反应:

luping

我们也可以通过collision.action来记录方块的运行轨迹:


var updateCount = 0;
collision.action = {
if (updateCount % 3 == 0) {
let outline = UIView(frame: square.bounds);
outline.transform = square.transform;
outline.center = square.center;

outline.alpha = 0.5;
outline.backgroundColor = UIColor.clearColor();
outline.layer.borderColor = square.layer.presentationLayer().backgroundColor;
outline.layer.borderWidth = 1.0;
self.view.addSubview(outline);
}

++updateCount;
}

上面代码的意思是,每当collision.action执行三次时,我们添加一个View,大小与square相同,transformcenter也相同,透明度设为5,背景色设为空,边框色设为当前square的颜色。这样就可以记录下square运行轨迹了。

luping

在上面的代码中,我们只修改了弹力属性elasticity,其实还有其他的属性,我们来看一下:

  • 弹力(elasticity):设置物体发生碰撞时的弹力,比如当物体碰撞时弹开的高度、角度的大小,物体的韧性等。
  • 摩擦力(friction):设置物体滑动时的摩擦力。
  • 密度(density):设置物体密度,密度越大加速度越大。
  • 阻力(resistance):设置物体滑动时的阻力,与friction不同的是,它只作用于线性滑动时。
  • 角度阻力(angularResistance):物体进行旋转运动时的阻力设置。
  • 允许旋转(allowsRotation):该属性并不是模拟现实中的一些行为属性,它是物体是否可以旋转的开关属性。

动态添加行为

在目前的程序中,我们都是预先设置好了View的各种行为和行为属性,那么这一节我们会展现如何动态的添加或移除行为。

我们打开ViewController.swift文件,在viewDidLoad方法中添加一个属性:


var firstContact = false;

然后在碰撞代理的方法collisionBehavior(behavior:beganContactForItem:withBoundaryIdentifier:atPoint:)中添加如下代码:


if (!firstContact) {
firstContact = true;

let square = UIView(frame: CGRect(x: 30, y: 0, width: 100, height: 100));
square.backgroundColor = UIColor.grayColor();
view.addSubview(square);

collision.addItem(square);
gravity.addItem(square);

let attach = UIAttachmentBehavior(item: collidingView, attachedToItem:square);
animator.addBehavior(attach);
}

我们又创建了一个正方形View,背景色设置为灰色,将它添加至碰撞和重力行为中,然后我们创建了一个关联行为,将这个正方形和我们之前创建的正方形关联起来。

编译运行一下,我们可以看到当之前创建的方块与障碍物发生碰撞后,会出现另一个方块:

luping

虽然你看到了两个方块似乎被连接起来了,但是之间没有连线,因为我们只是动态的添加了一个行为,然后由该行为达到了这种效果。

用户交互

现在你看到了,我们可以在系统运行时动态的添加或删除行为。在最后呢,我们再介绍一个行为,那就是UISnapBehavior。当你点击屏幕时,UISnapBehavior行为会让对象像弹簧一样跳到你点击的那个位置。

为了能更好的看清楚效果,我们将刚才添加的那段用于显示方块运行轨迹的代码注释掉,也就是下面这段代码:


if (!firstContact) {
firstContact = true;

let square = UIView(frame: CGRect(x: 30, y: 0, width: 100, height: 100));
square.backgroundColor = UIColor.grayColor();
view.addSubview(square);

collision.addItem(square);
gravity.addItem(square);

let attach = UIAttachmentBehavior(item: collidingView, attachedToItem:square);
animator.addBehavior(attach);
}

然后我们在ViewController.swift文件中添加两个属性:


var square: UIView!;
var snap: UISnapBehavior!;

viewDidLoad方法中,我们去掉申明squarelet关键字:


square = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100));

square申明为类的属性,而不是方法的常量,是因为我们需要追踪square的轨迹,这样我们就可以在ViewController类的任何方法和地方访问到square

然后我们重写touchesEnded方法,在该方法中创建Snap行为,也就是当用户点击屏幕时,会触发Snap行为:


override func touchesEnded(touches: NSSet!, withEvent event: UIEvent!) {
if (snap != nil) {
animator.removeBehavior(snap);
}

let touch = touches.anyObject() as UITouch;
snap = UISnapBehavior(item: square, snapToPoint: touch.locationInView(view));
animator.addBehavior(snap);
}

上述代码的作用非常清晰,首先判断Snap行为是否存在,如果存在就从引擎中移除。然后实例化Snap行为,作用项是square,弹跳的位置是我们点击屏幕的位置。编译运行看看,会发生什么呢?

luping

参考原文:UIKit Dynamics Tutorial in Swift

动力学这个名词不论在哪个行业领域听起来似乎都很高大上。那么在Swift中的重力学是什么呢?那就是将我们移动端屏幕上毫无生命力的东西也置于万有引力中,使它们能够展现出好像真的由于引力而向下坠落以及碰到物体后自然的弹开的效果。

要想做到这一点,我们得需要两个利器:UIKit DynamicsMotion Effects

  • UIKit DynamicsUIKit中一套完整的物理引擎。它可以让我们在程序中对界面元素添加一些行为从而达到诸如重力、弹簧等现实中的动作行为。你只需在引擎中注册界面元素,并指定好物理行为,其他的事就交给物理引擎去完成了。
  • Motion Effects可以创建很酷的视差效果,就像你iPhone上横竖屏切换时那样。它基于Apple提供的重力加速器提供的数据计算分析,使我们的界面元素根据移动设备的倾斜方向做出相应的反应。

当这两者一起使用的时候,我们就可以让程序活起来,富有生命力。

让我们开始屌丝的逆袭

我们挑一些小例子来学习UIKit Dynamics

注:由于个人编码习惯,在Swift代码中我还是加了;,其实可加可不加,根据大家喜好。

打开Xcode6新建项目,选择iOS Application/Single View Application,名称随便取,我这里命名为UIKitDynamicsDemo,我们可以看到Single View Application的目录结构:
目录结构

打开ViewController.swift文件,在viewDidLoad方法中添加如下代码:


// 创建一个正方形View,颜色设置为蓝绿色,加入当前的View中
let square = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100));
square.backgroundColor = UIColor.cyanColor();
self.view.addSubview(square);

上面的代码在我们的屏幕上添加了一个蓝绿色的正方形UIView。运行一下,我们能看到一个木讷的蓝绿色正方形静静的待在屏幕中:
屏幕

如果你用真机运行,你可以摇晃一下手机、倾斜手机、拿着手机手舞足蹈一下,看看那个木讷的方块有什么反应和变化么?答案是要有反应你就见鬼了。因为我们写的这几行代码只能让它杵在那一动不动,那怎么能让它动起来呢?接下来让我们见证奇迹的时刻!

添加重力行为

ViewController.swift文件中添加两个属性:


// UIKit物理引擎
var animator: UIDynamicAnimator!;

// 重力行为
var gravity: UIGravityBehavior!;

注:代码中感叹号的作用这里不做过多介绍,请自行查阅官方文档。

然后在viewDidLoad方法中再加入以下代码:


// 实例化UIKit物理引擎类,作用于ViewController的View
animator = UIDynamicAnimator(referenceView: self.view);

// 实例化重力行为类,目前只作用于刚才创建的正方形View
gravity = UIGravityBehavior(items: [square]);

// 将重力行为添加到UIKit物理引擎类中
animator.addBehavior(gravity);

现在再编译运行一下,这时我们可以看到这个蓝绿色正方形开始做自由落体运动了,一直跌落出屏幕下边缘然后消失。

屏幕

我们来看看刚才我们添加的两个属性的类型:

  • UIDynamicAnimator属于UIKit物理引擎中的类。它的作用是跟踪你添加到物理引擎中的各种行为动作,比如这里的重力行为,并且提供整个上下文。实例化UIDynamicAnimator时,它的构造函数需要传入一个referenceView参数,用于告知它要跟踪并制定坐标的View
  • UIGravityBehavior是一个模拟重力的模型,可作用于一个或多个元素。它的构造函数需要传入一个数组,该数组的内容就是我们希望有重力表现的一个元素或多个元素。

大多数的行为都有一些配置属性,比如重力行为就有可以改变角度和速率的属性:


// 实例化UIKit物理引擎类,目前只作用于刚才创建的正方形View
animator = UIDynamicAnimator(referenceView: self.view);

// 实例化重力行为类,目前只作用于刚才创建的正方形View
gravity = UIGravityBehavior(items: [square]);

// 角度
gravigy.angle = 1.6;

// 速率
gravigy.magnitude = 0.1;

// 将重力行为添加到UIKit物理引擎类中
animator.addBehavior(gravity);

上述代码中的angle是重力行为的角度属性,angle的值为0时,方块会水平向右移动,随着值的增大,方块会顺时针改变角度。不过我们要模拟现实中的重力,所以该属性一般不设置,不设置时默认是垂直向下移动。magnitude是重力行为的速率属性,值越大下降的速度越快,当magnitude属性的值为0时,方块就不会下降了,所以最小的速率是0.1。

注意:在现实世界中,重力加速度大约是g = 9.80665m/s^2,就是9.8米每平方秒。根据牛顿第二定律,我们可以使用0.5 * g * Time^2公式来计算下坠距离。
在UIKit的重力世界中,计算重力加速度的公式是一样的,但是单位有所不同。不是米而是像素,即g = 1000pixels/s^2,我们同样可以使用牛顿第二定律来计算我们的方块在单位时间内下降的距离。我们只需要知道重力加速度g越大,坠落速度越快,所以上述代码中的magnitude属性就差不多是这意思。

不能让我们的方块一坠千里

从目前代码的运行状况知道,我们的方块下降到屏幕底部时丝毫没有停止的意思,直接坠崖而下,看都看不见。我们希望我们的屏幕类似一个盒子一样,方块在盒子中,当下降到底部时就停止,那么我们就需要设置一个边界。

ViewController.swift文件中再添加一个属性:

 
// 碰撞行为
var collision: UICollisionBehavior!;

然后在viewDidLoad方法中加入以下几行代码:


// 实例化碰撞行为类,目前只作用于刚才创建的正方形View
collision = UICollisionBehavior(items: [square]);

// 将参考视图的边界作为碰撞边界
collision.translatesReferenceBoundsIntoBoundary = true;

// 将碰撞行为添加到UIKit物理引擎类中
animator.addBehavior(collision);

上面的代码创建了边界行为,它会将一个或多个边界与指定的View联系起来,并使两者有具有交互行为。

从上面代码我可以注意到collision.translatesReferenceBoundsIntoBoundary = true;这行代码,它的意思是将UIDynamicAnimator引用的View的边界作为碰撞行为的触发边界,这样就不用我们再去设置边界的坐标了,非常好用。

接着我们编译运行看看,此时小方块坠落到屏幕底部时会产生碰撞效果,并且还会反弹几下,是不是很逼真呢。

屏幕

碰撞行为进阶应用

接下来我们在屏幕中再添加一个View,长方形并横在屏幕中间,在viewDidLoad方法中添加如下代码:


// 创建一个长方形View,颜色设置为红色,加入当前的View中
let barrier = UIView(frame:CGRect(x: 0, y: 300, width: 140, height: 20));
barrier.backgroundColor = UIColor.redColor();
self.view.addSubview(barrier);

屏幕

但是我们发现这个红色的障碍物和方块并没有任何交互行为,这里有一点很重要:只有被行为关联起来的Views才会具有交互行为。我们来看看下面这张关系图:
屏幕

UIDynamicAnimator引擎引用了当前屏幕的view,该view给引擎提供了整个坐标系统。每个行为可以关联多个元素,每个元素都可以被多个行为关联,上面的关系图可以很清晰的表现出当前app中各个行为与元素的关联关系。由于所有行为都没有与长方形view关联,所以barrier基本可以被忽略。

唤醒Barrier

如果我们想让barrier活起来,我们得让碰撞行为将它关联起来:


// 实例化碰撞行为类,目前作用于刚才创建的正方形View和长方形View
collision = UICollisionBehavior(items: [square,barrier]);

barrier添加到碰撞行为关联元素的数组中。这样的话,squarebarrier这两个view都会有碰撞行为,所以当他们相撞时就会产生碰撞效果。

编译并运行,让我们来看看睡醒的barrier会产生什么效果:

屏幕

我们再来看看现在app中行为与元素的关系图:
屏幕

来看看我给这个红色长方形view起的名字:barrier,没错,障碍物。但是它现在是一个称职的障碍物吗?显然不是,当它与蓝绿色正方形碰撞时,它会于正方形一起旋转坠落。

等等,为什么barrier坠落到屏幕底部后不像square会有蹦跶一下然后停止,反而在缓慢旋转,感觉向失重一样。嗯哼,这是因为我们并没有将重力行为与barrier关联起来,所以它会向失重一样开始旋转。

这显然不是我想要的结果,我希望square坠落碰到barrier后,barrier纹丝不动,然后square被撞的七荤八素。我们要拦住那个square

隐形的边界与碰撞效果

我们先去掉barrier的碰撞效果,将:


collision = UICollisionBehavior(items: [square,barrier]);

改为:


collision = UICollisionBehavior(items: [square]);

然后在这行下面添加另外一行代码:


collision = UICollisionBehavior(items: [square]);
collision.addBoundaryWithIdentifier("barrier", forPath: UIBezierPath(rect: barrier.frame));

这行代码的意思是在碰撞效果中添加一个隐形的边界,它的位置和形状和barrier的一样。这样就会让人产生一种错觉,认为红色的barrier成为了边界,其实真正的边界隐藏在它之后。编译运行一下,我们来看看会发生什么:

屏幕

我们看到了当蓝绿色方块下降碰到红色障碍物时产生了碰撞行为,蓝绿色方块弹开并翻转落下。

从上面内容我们了解到,Swift的UIKit动力引擎非常强大,通过简短的几行代码就可以帮助我们实现惊艳的实现行为。下篇文章会向大家详细的介绍碰撞行为背后的故事。

参考原文:UIKit Dynamics Tutorial in Swift

当Swift提供了访问控制特性后大家对该特性的响应非常积极和强烈。同时也有很多开发者朋友会问,“为什么Swift中没有提供protected访问级别呢?” 然而在很多其他语言中有这种访问控制级别,就是只能由子类可以访问某些方法或属性的访问级别。

当定义Swift的访问级别时,我们主要考虑了以下两个使用情况:

  • 在App中隐藏class的私人信息。
  • 在App客户端中隐藏框架中的实现细节。

这两种情况就分别对应了privateinternal访问级别。

相比之下,protected级别保护了继承者的访问权限,这个新的访问控制级别给我们带来全新的思考方式和设计模式。但实际上,该级别并没有提供真正意义上的保护,因为子类总是能通过public级别的方法和属性去暴露protected级别的API。它也没有提供额外的优化解决机制,因为一个父类的方法或属性可以被很多个子类重写,很难控制。所以也没有必要在这方面进行限制,也就是说在现有机制下,子类可以访问父类的一些成员,但是在子类中使用的,或帮助子类实现某些功能的类并不一定能访问到该子类父类的成员。

正如一些开发者指出的,Apple的框架里有些API被分离出来,专门供子类使用。那么为什么不使用protected级别控制一下呢?我们查看了相关代码后,发现这些API大体符合这么两点情况。第一,一些方法在子类外没有用,所以没有必要将其保护为只能由子类使用。第二,一些方法可以被重写,但不能被调用。比如drawRect(_:)方法,在UIKit框架代码库中肯定可以使用,但是在UIKit框架之外就没法使用了。

在扩展类的时候,如何与protected级别的成员交互也思考的不是很明确。比如一个类的扩展是否可以访问该类的protected成员呢?或者一个子类的扩展是否能访问其父类的protected成员呢?在相同模块中申明类的扩展会有什么影响呢?

还有一个影响我们设计的原因是:现在已有的应用大多数都使用了Apple提供的或第三方提供的框架。Objective-C的方法和属性通常都是在.h公共头文件中申明的,但同样可以加在类扩展的.m实现文件中。当公共类的某些部分在框架的其他地方使用,但没有超出框架外时,开发者就要创建第二个头文件用于内部使用。这些种情况就对应着Swift中的publicprivateinternal三种访问级别。

Swift提供的访问控制特性本着单一、易于理解、与继承无关的原则。我们认为现在这种模式很简单也很容易理解,并且大多数的应用场景是想隐藏类或框架中一些功能实现细节。这种访问控制机制可能和你以前使用的其他语言不太一样,但是我们鼓励你们大胆的去尝试。

原文地址:Access Control and protected

一个函数A的参数是返回值为Optional类型的函数,如果你传入了一个返回值不是Optional的函数,那么函数A会自动将传入函数的返回值转换为Optional类型,来看代码示例:


// 函数B的返回值为Int类型
func B()->Int {
return 1;
}

// 函数A的参数是返回值为Optional(Int)类型
func A(fun: ()->Int?) {
println(fun());
}

// 把函数B作为参数传入函数A,输出结果为Optional(1)
A(B);

更棒的是你还可以使用经过转换后的函数,比如:


// 让函数A返回作为参数的函数
func A(fun: ()->Int?)->()->Int? {
return fun;
}

var i = 0
let j = { ++i }

j() // returns 1
j() // returns 2

let h = A(j)

h() // returns {Some 3}
h() // returns {Some 4}
j() // returns 5

这种特性只能作用于函数的返回值,如果是下面这种情况,就不好使了:


func B(i: Int)->Int {
return i;
}

func A(fun: (Int?)->Int?) {
println(fun(1));
}

// 编译失败,不能将B函数参数的Int类型隐式转换为Int?类型
A(B);

这是因为函数B希望它的参数永远不能为nil,当接收到值为nil的参数后它就不知道该怎么办了。但如果函数B的参数为Optional类型,而函数A希望传入的函数参数为非Optional呢?我们来看看:


func B(i: Int?)->Int {
return i ?? 0
}

func A(fun: (Int?)->Int?) {
println(fun(1));
}

// 编译成功,并做了隐式转换,
A(B);

上面的特新其实可以理解为,编译器使用了一个闭包,调用了你传入的函数并进行了封包操作:


func B()->Int {
return 1;
}

func A(fun: ()->Int?) {
println(fun());
}

A( { Optional(B()) } );

这是Swift很有意思也很有用的一个特性。