当你准备好开发一个应用扩展时,要选择一个合适的扩展点来呈现你的应用扩展的功能。然后使用Xcode提供的各个扩展点的模板来创建Target,以便使默认文件与你自定义的代码和界面更好的结合。最后,在你调试并优化你的应用扩展之后,就可以打包进你的应用程序给用户使用了。

开发之前要选择正确的扩展点

因为每一个扩展点,它的功能定义都非常明确,对用户的使用习惯针对性很强,所以你首先应该考虑清楚的是,我要开发的这个应用扩展给用户提供什么样的功能,并且用户最习惯在哪个扩展点使用它。这是非常关键的,因为每个扩展点都有不同的API,为你提供不同的功能开发,所以如果你选错了扩展点,那么很有可能你就无法实现你想要的功能。表1-1列出了iOS和OS X中支持的所有扩展点。

当你选定了一个扩展点并创建后,在你准备包含扩展的应用程序工程中就会有一个新的Target出现。创建一个应用扩展Target最简单的方式就是使用Xcode提供的模板,因为它已经帮你将基本的配置信息都配置好了,你只需要进行功能的开发即可。

你可以通过菜单choose File > New > Target在你的Xcode工程中创建一个新Target。在打开的窗口中,选择iOS或OS X的Application Extension选项,然后选择你想要创建的应用扩展模板,如下图所示:
图片

当你选择好模板,并在工程中创建Target后,你就可以试着编译并运行一下默认代码。如果你是基于Xcode提供的模板创建的扩展Target,那么当编译成功后,就会生成一个扩展名为.appex的应用扩展包。

注意64位的架构:
一个应用程序扩展在设置其编译架构时必须要包含arm64的架构,否则在上传App Store时会被拒绝。你创建完一个应用扩展后,可以在Xcode的Standard architectures选项中设置arm64架构。
如果你的包含扩展的应用程序使用了一些框架,那么你的应用程序也必须要包含arm64架构,否则上传App Store时也会被拒绝。
关于64位架构开发环境的更多信息,请参阅64-Bit Transition Guide for Cocoa Touch

在大多数情况下,你可以在OS X的System Preferences或iOS的Settings中开启应用扩展,或授予权限,这样就可以在其他应用中测试只含有初始默认代码的扩展了。比如说,你可以在OS X系统中通过Safari中打开一个页面来测试分享扩展,点击分享按钮,然后选择你要测试的扩展即可。

检查默认的扩展模板

一般情况下,每个扩展模板都包含一个属性列表文件(就是Info.plist文件),一个View Controller类和一个默认的界面文件,这些都是基于扩展点定义的。默认的View Controller类(或主体类)都含有扩展点对应功能的方法,需要我们去实现。

应用扩展TargetInfo.plist文件中除了标示扩展点的信息外还挪列了应用扩展的详细信息。但至少,该文件中包含一个Key NSExtension,它的值是一个用于说明扩展点的Dictionary。比如Key NSExtensionPointIdentifier的值就代表一个扩展点的名称的引用值,比如com.apple.widget-extension。在应用扩展的NSExtension字典中还有其他的Key代表了不同的信息。

  • NSExtensionAttributes:这是一个描述扩展点具体属性的字典,就像照片编辑扩展中的PHSupportedMediaTypes一样。
  • NSExtensionPrincipalClass:这是扩展模板创建的主体视图控制器类,就像SharingViewController。当主应用程序调用扩展时,扩展点会实例化这个类。
  • NSExtensionMainStoryboard(只适用于iOS):扩展的默认Storyboard文件,一般名为MainInterface

除了在属性列表中设置以外,扩展模板还可以设置一些默认的功能。每个扩展点都可以给它们提供任务类型的场景定义功能,比如说一个iOS的Document Provider扩展就包含com.apple.security.application-groups的功能。

所有的OS X扩展模板,都默认包含应用程序沙箱和com.apple.security.files.user-selected.read-only功能。如果你开发的扩展需要适用网络,或者访问用户的相册,再或者需要访问用户的通讯录,那么你就需要额外定义这些功能。

注意:
通常情况下,如果用户允许主应用程序访问他们的私有数据,那么主程序里的扩展也同样拥有该权限。

响应主程序的请求

正如你在Understand How an Extension Works这篇文档中了解的,当用户在主程序选择一个扩展,并使主程序向扩展发出请求时,扩展才会被打开。说的再详细一点,用户做出了一个动作或者说是任务请求,然后你的扩展收到了一个请求,这个请求是帮助用户完成他希望完成的任务,最后扩展完成并关闭这个请求。比如说,一个分享扩展收到了它主程序的请求,然后该扩展响应请求并打开它相应的界面。然后用户在该界面中编辑要分享的内容,用户可以选择发送或者不发送,最后扩展根据用户的行为响应完成还是关闭请求。

当主程序向扩展发出请求时,一般都会指明扩展运行的上下文。对于很多扩展来说,最重要的一部分就是要设置一个工作项,这个工作项就是用户在使用这个扩展时要处理的工作项。比如说,一个分享扩展的上下文可能就包含用户选择的想要分享的一段文字。

当主程序发出一个请求(通常就是调用beginRequestWithExtensionContext:方法),你的扩展就可以用主试图控制器中的extensionContext属性来接受上下文,然后使用NSExtensionContext类解析上下文并获得工作项。通常,在视图控制器的loadView方法中解析上下文并获得工作项,这样在加载完视图后就可以将信息显示在视图界面中了。获取扩展上下文可以使用如下代码:

NSExtensionContext *myExtensionContext = [self extensionContext];

有意思的inputItems属性,它包含了用户在使用扩展时要处理的工作项。inputItems属性包含一个NSExtensionItem类型的数组,数组的每一个成员都包含一个可执行的工作项。从上下文中获取工作项可以使用如下代码:

NSArray *inputItems = [myExtensionContext inputItems];

每个NSExtensionItem对象都包含若干个描述工作项的属性,比如标题、文本内容、附件信息、用户信息。

注意attachments属性,它包含一个与工作项相关联的媒体数据数组。比如说一个分享请求的工作项,那么attachments属性可能就包含用户想要分享网页中的信息。

当用户希望的工作项处理完后,应用扩展通常会给用户两个选择,完成任务或取消任务。根据用户的选择,扩展会调用completeRequestReturningItems:expirationHandler:completion:方法,选择返回给主体程序的工作项,或者会调用cancelRequestWithError:方法,返回一个错误编码。

在iOS中,你的应用程序扩展可能需要更多的时间去处理潜在的需要长时间处理的任务,比如说往网上上传数据。这种情况下,你就要使用NSURLSession类将该任务转为后台处理的任务。因为转换到后台处理任务需要用一个单独的线程,所以在扩展完成主应用请求并关闭后仍然可以处理。想了解更多关于扩展中NSURLSession类的使用,请参阅:Performing Uploads and Downloads

注意:
虽然你可以设置一个在后台通过URL上传下载的任务,但是有一些类型的后台任务,比如提供VoIp或者在后台播放音乐的任务,是不能通过扩展来实现的。
如果你应用扩展的Info.plist文件中含有UIBackgroundModes关键字,那么在上传App Store时会被拒绝。(想了解更多关于UIBackgroundModes关键字的内容,请参阅Information Property List Key Reference中的UIBackgroundModes

优化效率和性能

应用扩展在内存使用优先级上要明显低于当前运行的应用程序。不管是iOS还是OS X,系统都会频繁的终止扩展,因为用户想返回到主应用程序。但是也有一些应用扩展的内存使用优先级要高于其他扩展,比如说widgets就要求要高一些,因为它要实时的显示一些信息,并且一般用户都会同时开启多个widgets

你的应用扩展并不拥有主循环线程,你要遵循这一规则,以便让扩展在主循环线程中发挥最好的性能。比如说,如果你的应用扩展阻止了主循环线程,那么在用户使用主应用程序的过程中会造成非常糟糕的用户体验效果。

我们需要明确的一点是,GPU在系统中是一个共享的资源,所以应用扩展不会得到很高的优先级照顾。比如说,如果你正在玩一个对GPU消耗很高的游戏,那么由于内存压力比较大,它就有可能会选择关闭Today widget

设计一个精简的用户界面

大多数的扩展点都允许你向用户提供一个自定义的界面,它在用户打开你的应用扩展时呈现给用户。通常情况下,应用扩展的界面要尽可能的简约、功能的针对性要强。为了提高性能和用户体验效果,你要避免与该扩展功能无关的界面出现。

大多数Xcode提供的扩展点模板都包含一个初始界面文件,你可以在这个文件中设计界面。

在用户的惯性思维中,一般他们都是通过应用扩展的图标来辨识扩展功能的。不过通常情况下,应用扩展的图标和它的主体应用的图标是一致的。使用主体应用的图标作为应用扩展的图标有利于用户去判断这个扩展的来源,也就是说让用户确信这个扩展是来源于他们安装的主体应用。当然也有一些例外。

  • 在iOS中,自定义的Action扩展的图标使用它主体应用的图标。
  • 在OS X中,如果一个扩展的主体程序只是用来安装扩展的安装包,那么该扩展要提供一个单独的图标,否则都会使用主体应用的图标。

应用扩展要使用一个简短,语义明确的名字,这能让用户更乐意使用主体应用和应用扩展,并且能让他们在系统中更好的管理应用扩展。通过应用扩展TargetCFBundleDisplayName属性来设置它的名称,你可以在Info.plist文件中修改它。如果你没有给CFBundleDisplayName设置值,也就是没有给扩展设置名称,那么应用扩展会使用它的主体应用的名称,也就是CFBundleName属性中的值。

同时一些应用扩展也需要一个简短的说明。比如说,OS X中的Widget扩展就会显示一个简单的描述,这能帮助用户更好的选择他们想要显示在今日通知中的Widget扩展。扩展的描述可以在InfoPlist.strings文件的widget.description属性中设置。

调试,配置和测试你的应用扩展

注意:要确保主体应用中的所有扩展都要使用相同签名方式的代码。

在Xcode调试应用扩展和调试其他程序基本是一样的,但唯一一点不同的是:你要选择一个能看到扩展的载体应用。当你编译运行应用扩展后,Xcode会运行载体应用,等待你去使用扩展并触发调试点来调试扩展。你要在scheme中要为扩展指定一个载体应用(一个scheme封装了Target编译的说明)。

当你在主体应用工程中创建一个应用扩展的Target时,Xcode就会为应用扩展默认创建一个scheme。应用扩展的scheme可以让你指定在调试时,由哪个应用程序来调用你的扩展,也就是指定一个调试时的载体应用。默认情况下,当你编译运行扩展时,会询问你使用哪个载体应用来调用该扩展。

在你编译运行应用扩展之前,你要确保你的扩展已经选择了一个scheme。你可以通过Product > Scheme > MyExtensionName或者使用Xcode菜单栏呼出scheme菜单并选择MyExtensionName来设置应用扩展的scheme

注意:如果你使用运行主体应用的scheme代替应用扩展的scheme,那么你在编译工程时Xcode会告诉你它正在等待调试应用扩展。

当你编译运行应用扩展时,Xcode会为你列出允许调用该扩展的载体应用程序。当你选择一个载体应用程序并且运行后,调试器就准备开始工作了,并准备好在你打的断点处进行拦截。当你在载体应用程序中使用扩展时,就可以对应用扩展进行Debug调试了。调试应用扩展的方式和方法与调试其他应用程序是一样的。

注意:如果你选择的载体应用程序不是扩展的scheme中指定的载体应用程序,那么当你在这个载体应用程序中访问扩展时,Debug调试器是不会起作用的。

在OS X中,你在载体应用程序中访问扩展之前,要确保该扩展是允许被使用的。一般情况下,在System Preferences的扩展面板中开启或关闭扩展(你也可以在共享或Action菜单中打开应用扩展面板)。这里要注意一点,在OS X中使用Widget模拟器调试Widget扩展时,是不需要对其进行开启操作的。当你要调试键盘扩展时,必须要开启该扩展(你可以通过Settings > General > Keyboard > Keyboards开启键盘扩展)。

在调试时,Xcode会在OS X中创建一个持续的编译应用扩展的会话。这意味着,如果你要使用OS X系统下的扩展,就要将该扩展编译打包好的程序拷贝到Finder中的Applications文件里。

注意:在Xcode的调试控制台日志中,应用扩展的二进制值可能是和CFBundleIdentifier属性关联,而不是CFBundleDisplayName属性。

因为应用扩展必须要做出一个有效的响应,所以当你运行应用扩展时,在调试导航中查看各种调试监控器就非常的清晰明了。这些调试监控器会告诉你当前运行的应用扩展占用了多少CPU、内存和其他系统资源。当你发现类似占用CPU资源出现异常的性能问题后,你就可以使用Instruments来分析你的应用扩展,并确定需要改进的地方。想学习了解调试监控器,请查阅Debug Your App,想学习了解Instruments,请查阅Instruments User Guide

注意:通过Product > Profile可以直接在Instruments中编译并运行应用扩展。

如果要使用Xcode提供的测试框架(比如XCTest APIs),你需要在主体应用程序中写一些测试用例代码。想了解更多XCTest的知识,请参阅Testing with Xcode

分发扩展主体应用程序

你不能直接将应用扩展上传至App Store,你只能上传应用扩展的主体应用程序,并且你不能将应用扩展从一个应用程序中转到另一个应用程序。

如果想让用户使用你的应用扩展,你就比如将该扩展的主体应用程序上传至App Store中,并且主体应用程序如要有其他的功能,不能只包含应用扩展。

虽然我们推荐将应用扩展主体应用程序上传至App Store作为用户使用应用扩展的途径,但这也不是唯一的途径。比如在OS X中,主体应用程序就可以只包含应用扩展,而不需要提供其他的功能。

注意:如果你不使用通过上传主体应用程序到App Store这种方式交付应用扩展,那么在主体应用程序被用户认可或批准前,Gatekeeper是不会允许应用扩展生效的。同时,如果你不将主体应用程序上传至App Store,那么该主体应用程序也不能签署你的开发者ID名称,所以用户需要在设置用手动将你的主体应用设置为可信任的应用程序,才可以使用你的应用扩展。

原文地址:Creating an App Extension

在WWDC14上我们通过Playground展示了Swift这门语言,很多人会问关于在Playground中展示出的那个气球的示例是如何实现的。从气球这个示例我们可以看到我们写的代码具有很强的交互性,而且非常有趣,并且也从该示例中展示了Playground的许多特性。现在,你可以通过这篇文章,学习如何实现气球示例中的各种有趣的效果,在我们提供的Balloons.playground文件中有详细的文档说明和代码示例。

由于Balloons.playground文件中运用了SpriteKit的新特性,所以要求在最新的Xcode 6 Beta版本环境和OS X Yosemite系统下运行。

Balloons.playground

// Requires Xcode 6 (beta 4) running on Yosemite (developer preview 4) or later.
import SpriteKit
import XCPlayground

Playground文件中包含了探索SpriteKit新特性的示例代码。该示例来自WWDC2014大会上演示Swift语言时使用的示例,该示例描述的是两个大炮,间隔任意一段时间就会向天上的气球开火,当两个气球相撞时,会“砰”的一下消失。在该示例中,你会了解到创建如此炫酷的场景、自定义动作和效果是多么简单。

当你更改示例中的代码时,你会在Playground的右侧Timeline区域即时的看到更改后的效果,所以你可以尝试按照你自己的想法创造出更多有趣的效果。

注意:如果你看不到气球大炮的场景,那么你可以通过View > Assistant Editor > Show Assistant Editor打开Timeline区域,或者也可以使用Option-Command-Return快捷键打开。

来让我们开始吧!

在Timeline区域呈现我们想要的效果

SpriteKit的内容用SKView对象来表示,它负责将要显示的内容提供给模拟器。所有的效果都通过SKScene对象来呈现,它相当于SKNode对象树的根节点。在这个场景中,你将添加一些节点并且创建游戏的内容。

我们在Xcode中通过iOS > Application > SpriteKit Game创建一个SpriteKit Game模板,模板中会提供MyScene文件,这就是我们游戏场景,该文件就是场景文件。所有的资源,包括场景文件、图片资源都全部绑定在Playground包中供我们使用,你也可以在Playground包种添加你自己的图片资源。你可以在Balloons.playground文件上点击鼠标右键,然后选择显示包内容,将资源文件添加进去即可。

let sceneView = SKView(frame: NSRect(x: 0, y: 0, width: 850, height: 638))
let scene = SKScene(fileNamed: "GameScene")
scene.scaleMode = .AspectFill
sceneView.presentScene(scene)

XCPShowView("Balloons", sceneView)

尝试实验:因为重力是在场景中由程序虚拟构建的物理世界(physicsWorld属性)定义的,所以在SpriteKit的物理世界中,我们可以不按照现实世界的定律,并且可以改变定律。
我们可以通过改变scene.physicsWorld.gravity矢量值让重力颠倒。

我们要想在Playground的场景中真正看到希望的效果,那么我们就要调用XCPlayground类的XCPShowView函数。该函数的功能是将场景真实的显示在Timeline区域中,通过该函数,你对代码做的每一个改动都会实时的显示在Timeline区域中。

让大炮开火

当大炮要开火时,让我们在场景中添加一些气球,并让它们在场景中来回穿梭。每一个气球其实就是一个Sprite节点或者叫Sprite元素,为了让气球看起来更真实、更有质感,我们会从气球图片集合中随机的取出气球图片用于显示在场景中。

这里我们先创建一个Swift数组对象,内容是气球图片的名称,然后用数组的map函数创建一个SKTexture对象的数组。这样我们就可以很方便的通过该数组随机的创建Sprite元素。

let images = [
    "blue", "heart-blue", "star-blue",
    "green", "star-green", "heart-pink",
    "heart-red", "orange", "red",
    "star-gold", "star-pink", "star-red",
    "yellow"
]
let textures: [SKTexture] = images.map { SKTexture(imageNamed: "balloon-\($0)") }

var configureBalloonPhysics: ((balloon: SKSpriteNode) -> Void)?
func createRandomBalloon() -> SKSpriteNode {
    let choice = Int(arc4random_uniform(UInt32(textures.count)))
    var balloon = SKSpriteNode(texture: textures[choice])
    configureBalloonPhysics?(balloon: balloon)

    return balloon
}

尝试实验:你可以点击代码编辑区右侧的小圆圈将代码片段的结果显示在Timeline区域,这样可以便于我们检查。
如果将SKTexture添加到Timeline后内容显示不全,你可以拖动Timeline滚动条来显示全部内容。

现在我们就已经创建好了气球,是时候让它们动起来了。我们首先要赋予它们真实的物理身体,因为当模拟物理形态和行为时,只会对有物理身体的元素起作用。

SpriteKit中,每个场景最多可包含32个类别。在众多的元素中,你可以用物理身体的类别来区分它们。这里要注意的是,我们将气球类别赋值为位移掩码,原因是为了当两个气球碰撞时能触发通知。

let BalloonCategory: UInt32 = 1 << 1
configureBalloonPhysics = { balloon in
    balloon.physicsBody = SKPhysicsBody(texture: balloon.texture, size: balloon.size)
    balloon.physicsBody.linearDamping = 0.5
    balloon.physicsBody.mass = 0.1
    balloon.physicsBody.categoryBitMask = BalloonCategory
    balloon.physicsBody.contactTestBitMask = BalloonCategory
}

尝试实验:将把BalloonCategory赋值给contactTestBitMask这行移除掉,看看会发生什么?

更新物理身体的属性是很有意思的实验,因为你的修改会即时的现实在Timeline区域,会给即时给你呈现反馈。并且你还可以在Timeline区域检查并调试当前代码段的结果是否正确。

尝试实验:尝试增加或减少物理身体的masslinearDamping属性的值。气球会有什么变化?再尝试修改其他属性,看看气球会有什么变化?

我们还需要设定气球在场景中的位置。并且我们希望气球被击中时正对着大炮的炮口。

let displayBalloon: (SKSpriteNode, SKNode) -> Void = { balloon, cannon in
    balloon.position = cannon.childNodeWithName("mouth").convertPoint(CGPointZero, toNode: scene)
    scene.addChild(balloon)
}

这里要注意一下,我们通过调用名为mouth子元素的convertPoint:toNode方法给气球设置位置。这样做是没问题的,因为我们在每个大炮中都增加了名为mouth的子元素,用来标示或预定位气球可能出现的位置。这样做的好处是能避免我们去计算气球的位置,并且如果我们想改变气球第一次出现的位置时,根本不用修改代码,因为气球的位置是随机取mouth子元素的位置的。

在气球被击中时,我们应该让气球表现出有一股冲击力作用于它,其实就是瞬间改变了气球的速度。我们可以用SpriteKit提供的推力的行为,可以作用于指定的方向来做出冲击力的效果。当然我们也要根据大炮发出炮弹的旋转方向设置气球被冲击后的方向。

最后,我们将创建、显示已经向气球射击的代码封装成一个单独的函数,用于后续调用。

let fireBalloon: (SKSpriteNode, SKNode) -> Void = { balloon, cannon in
    let impulseMagnitude: CGFloat = 70.0

    let xComponent = cos(cannon.zRotation) * impulseMagnitude
    let yComponent = sin(cannon.zRotation) * impulseMagnitude
    let impulseVector = CGVector(dx: xComponent, dy: yComponent)

    balloon.physicsBody.applyImpulse(impulseVector)
}

func fireCannon(cannon: SKNode) {
    let balloon = createRandomBalloon()

    displayBalloon(balloon, cannon)
    fireBalloon(balloon, cannon)
}

为了能够方便的访问大炮元素,我们在Xcode对他们进行明确的命名。这样我们就可以不需要知道元素树的结构或者说是大炮节点的位置,我们甚至可以不需要修改代码就可以更改大炮的位置。

let leftBalloonCannon = scene.childNodeWithName("//left_cannon")
let rightBalloonCannon = scene.childNodeWithName("//right_cannon")

SpriteKit通过SKAction对象改变节点的位置、旋转、缩放或因我们设置的其他情况而等待(也就是在特定的时间内什么也不做)。你可以在一系列或一组行为中单独指定执行某个行为,并且可以让该行为自动的重复执行任意次数(或者一直执行)。而这并不需要修改节点的属性,它完全可以由一个简单的Block来执行。

let wait = SKAction.waitForDuration(1.0, withRange: 0.05)
let pause = SKAction.waitForDuration(0.55, withRange: 0.05)

let left = SKAction.runBlock { fireCannon(leftBalloonCannon) }
let right = SKAction.runBlock { fireCannon(rightBalloonCannon) }

let leftFire = SKAction.sequence([wait, left, pause, left, pause, left, wait])
let rightFire = SKAction.sequence([pause, right, pause, right, pause, right, wait])

当大炮开火时,我们需要为它创建一系列的行为,让它在开火和停止开火之间交替执行。我们将开火/停止这一系列的行为添加到另外一个行为中,并且让他们一直重复执行。

尝试实验:增加大炮开火的间隔,改变大炮的火力,并让大炮不停的开火。

要想让某个元素执行相应的动作,我们只需要调用runAction函数,并将我们想执行的动作作为参数传入即可。每个节点可以同时执行多个行为动作,这就使得我们可以在SpriteKit中让节点执行自定义的,复杂的行为动作。

leftBalloonCannon.runAction(SKAction.repeatActionForever(leftFire))
rightBalloonCannon.runAction(SKAction.repeatActionForever(rightFire))

尝试实验:SKAction类有一个rotateByAngle函数,该函数可以让节点根据某个角度旋转。
创建一个让大炮在开火时按角度旋转的行为动作,并执行。

气球的撞击

当两个气球碰撞时,我们希望让其中一个气球爆炸。爆炸的效果就是我们创建的一个行为,所以就可以使用该行为创建出一个动画效果,作用在气球相互碰撞时,并将爆炸的气球元素移除场景。当有两个行为合并为一个系列行为时,它们会按顺序一个一个执行。

let balloonPop = (1...4).map {
    SKTexture(imageNamed: "explode_0\($0)")
}

let removeBalloonAction: SKAction = SKAction.sequence([
    SKAction.animateWithTextures(balloonPop, timePerFrame: 1 / 30.0),
    SKAction.removeFromParent()
])

虽然两个元素在场景中碰撞的行为由SpriteKit自动处理,但是我们也必须要为其提供一个符合我们游戏场景的逻辑。这包括定义碰撞时触发的通知。在之前,我们将所有气球元素的物理身体类别都设定为气球类别,但是场景中的地面也是一个元素,并且它的类别是默认类别,这里我们需要知道,默认类别为所有类别。

let GroundCategory: UInt32 = 1 << 2
let ground = scene.childNodeWithName("//ground")
ground.physicsBody.categoryBitMask = GroundCategory

尝试实验:如果不给地面设置类别(也就是将上述三行代码注释掉),那么当气球碰撞或者说落到地面时会发生什么?

接触通知由SpriteKit的物理世界中的接触代理处理,这是一个遵循SKPhysicsContactDelegate协议的类。每当碰撞发生时,物理世界会通知接触代理(即遵循SKPhysicsContactDelegate协议的类),所以我们才能做出碰撞后正确的反应。

class PhysicsContactDelegate: NSObject, SKPhysicsContactDelegate {
    func didBeginContact(contact: SKPhysicsContact) {
        let categoryA = contact.bodyA.categoryBitMask
        let categoryB = contact.bodyB.categoryBitMask

        if (categoryA & BalloonCategory != 0) && (categoryB & BalloonCategory != 0) {
            contact.bodyA.node.runAction(removeBalloonAction)
        }
    }
}

let contactDelegate = PhysicsContactDelegate()
scene.physicsWorld.contactDelegate = contactDelegate

在接触代理的didBeginContact函数中,我们通过物理身体的类别位掩码来确保这次接触或者说碰撞是两个气球发生的(也就是说确保元素的类别是BalloonCategory)。用按位运算来确定两个元素是否是BalloonCategory类别,并执行该类别正确的行为。

尝试实验:允许气球和大炮碰撞。
提示:大炮元素没有赋予物理身体。

总结

Playground为你提供了一种与代码互动的有趣的方式。使用Playground对你也有很大的帮助,因为你的学习过程和调试错误过程都在一个可控的环境中。更重要的一点是,Playground能挑起你的好奇心,鼓励你不断的来测试你的代码。

希望你们在不断的修改代码、实验中找到乐趣,并且永远不要害怕重头开始!

一个应用程序扩展不是一个应用,它在主体应用程序中单独打包并生成单独的二进制文件。应用程序扩展不同于一个应用程序,它的功能相对于应用来说更加明确、单一,并且要严格遵循扩展点的协议。

应用程序扩展的生命周期

因为一个应用程序扩展不是一个应用,所以它的生命周期和运行环境也不同于应用。一般情况来说,应用程序扩展的生命周期开始于用户从某个应用中使用它。这个应用能够让用户选择某一种扩展来实现某个特定功能或任务,这种应用我们称为扩展载体应用。当扩展载体应用给扩展发送一个请求,让扩展代替响应用户的操作时,应用扩展就会被激活并意味着生命周期也随之开始。当应用扩展完成载体应用发送的请求任务后,该扩展的生命周期也随之结束。

比如说,有这样一个场景,用户在扩展载体应用中选中一段文字,并点击分享按钮,从分享列表中选择某一个分享扩展,将选中的内容分享至社区网站。此时,扩展载体应用就会启动该扩展,让它去响应用户的请求,即分享所选文本的内容。应用扩展基本的生命周期归纳在下图2-1中。
生命周期

在图2-1中的第2步中,系统实例化扩展载体应用中定义好的扩展,并建立载体应用于扩展之间的通信通道。然后扩展会根据上下文场景显示相关的界面,并使用载体应用接收到的信息执行用户的操作(在上述的例子中,分享扩展就使用用户选中的文本信息来执行相关操作)。

在图2-1的第3步中,用户在扩展中执行或取消某个任务,或者直接可以忽略该扩展。在对用户的操作做出响应的过程中,扩展会根据载体应用的请求立即开始执行任务,但如果有特殊需要时,扩展也会在系统后台去执行任务,这种情况下,扩展不会显示其相关界面,并且载体应用会显示用户操作扩展前的界面。当应用扩展执行完任务后,不管是返回还是延迟返回,但最后其执行结果都要返回给载体应用。

当扩展开始执行任务之后(或者开始在系统后台执行任务),系统就会终止扩展,比如图2-1中的第4步。

应用扩展是如何进行通信的

当应用扩展在运行的时候,它会直接和载体应用进行通信。但是应用扩展不会和调用它的应用进行直接通信,因为有可能应用扩展在运行,但是调用它的应用程序并没有运行。另外还有一点,扩展载体应用和调用扩展应用之间是绝对不会进行通信的。图2-2中描述了应用扩展、载体应用、调用扩展应用之间的关系。
关系

当一个扩展需要和调用它的应用进行通信时,只能间接的通过扩展载体应用提供的上下文进行。比如说,一个应用扩展有可能会运行调用它的应用程序。在这个例子中,应用扩展不能通过API直接和调用它的应用传递消息。但是,应用扩展和调用它的应用可以共同访问一个定义为私有的共享容器。在图2-3中描述了扩展和调用扩展应用之间通信的模式。
通信

注意:在底层,系统使用进程间通信来确保应用扩展和扩展载体应用做到无缝结合。在你的代码中,你从来不会考虑这个通用的通信机制。那是因为我们使用的扩展API是扩展点和系统已经高度封装后的。

原文地址:Understand How an Extension Works

重要提示:
这个文档只是一个使用API或了解技术的初级文档。Apple提供该文档的目的在于能让开发者尽快熟悉新技术和新的API,用于开发者针对其持有的设备进行开发。但是该文档会不断的更新,因为目前较新的技术和语言还在不断的完善,所以使用该文档提供的技术开发的软件不一定是最终的操作系统所能完全支持的,要时刻关注该文档的更新,以便了解新的API和技术特性。

当iOS8.0和OS X v10.10来临后,一个全新的概念出现在我们眼前,那就是应用扩展。顾名思义,应用扩展允许开发者对应用的功能进行扩展,究其根本的作用就是通过应用扩展能够让用户在使用别的应用时可以快速便捷的了解该应用的相关信息。你可以开发一个应用扩展,用来执行某些特定的任务,当用户使用该扩展后就可以在各种情况下去执行该任务。比如说,你开发了一款分享应用扩展,他可以让用户快速的将当前浏览的内容分享到你的社区网站。再比如说,你开发了一款实时显示比赛当前分数的扩展,那么用户就可以在通知中心的当天通知中看到比赛比分。你甚至可以开发一款自定义的键盘,可以让用户用它来替代系统自带的键盘。

应用扩展的类型

iOS和OS X定义了几种应用扩展的类型,每一种类型的扩展都对应系统中一块展示区域,像分享、通知中心、键盘等。我们把这些区域成为扩展点。当你开发某一个扩展点的应用扩展时,都有相对应的API以及要遵循的策略。还要注意的一点是,当你针某个扩展点开发应用扩展时,该应用扩展的功能必须要符合该扩展点的功能特性。

表1-1列出了iOS和OS X中的扩展点,并列举了对应扩展点的应用示例。


































扩展点 应用扩展示例
今日通知(iOS和OS X) 在通知中心的今日通知中快速查看或处理任务。(这个扩展点中的应用扩展称为Widget)
分享(iOS和OS X) 将网页内容或其他内容分享给他人。
Action(iOS和OS X) 处理或查看其他应用中的信息。
照片编辑(iOS) 在Photos应用中编辑照片或视频。
Finder(OS X) 在Finder中直接显示文件的同步信息等。
文本管理(iOS) 提供用于查看和管理文件的文本库。
自定义键盘(iOS) 用于替换系统自带的键盘。

由于系统定义了扩展特定领域,所以当你在开发应用扩展时,其功能要适用于所有列出的这些扩展点。比如说你要开发一个分享信息的应用扩展,那么你只能选择分享扩展点,而不能使用今日通知或Action扩展点。

重要提示:你开发的应用扩展只可能满足于某一个扩展点,你不能开发一个类似通用的应用扩展去满足所有的扩展点。

Xcode和应用商店帮助你创建并提交应用扩展

应用扩展不同于应用,虽然你需要基于应用去开发应用扩展,但是每个应用扩展都是独立于应用运行的二进制文件。

当你要创建一个应用扩展时,需要在项目中创建一个新的Target。和其他Target一样,扩展Target将设置信息和相关文件打包在Products文件下生成一个扩展名为.appex的包。你可以在应用项目中创建多个扩展Target(一个应用程序可以包含一个或多个应用扩展,该应用程序称为调用扩展应用程序)。

开发一个应用扩展最好的切入点是使用Xcode提供的所有平台上扩展点的应用扩展模板去开发。每个模板包含扩展点的具体实现文件和相关设置,并生成独立的二进制文件添加到你的应用程序中。

注意:在iOS中,包含扩展的应用必须提供一个扩展之外的功能。而在OS X中没有这个硬性要求,一个包含扩展的应用不要求必须提供一个额外功能。

当你将包含扩展的应用提交到应用商店后,用户只要下载并安装了你的应用,那么同时也自动安装了你的应用扩展。

当安装了应用扩展之后,用户首先必须要开启他们。通常,用户可根据当前处理的任务情形使用某个对应的应用扩展。比如,如果你的扩展启用了“Today”通知,那用户可以在通知中心编辑“Today ”view 来添加你的扩展。在其他情况中,用户可以使用 iOS 中的“Settings”或者 OS X 中的“System Preferences”来启用和管理扩展。

用户在不同的情形下体验不同的应用扩展

虽然每种类型的应用扩展的功能都是不同的,但是他们在用户体验及操作方式上还是有共同点的。如果你准备开发一个应用扩展,那么必须明白在不同的扩展点中,用户最喜欢的体验方式是什么,这一点至关重要。应用扩展的最高境界就是快速、流畅、只关注与单一任务。

通常用户是通过系统提供的某个界面开启应用扩展。比如说,如果一个用户想使用分享扩展,那么就需要通过应用程序中系统提供的分享按钮来选择开启。一个应用扩展必须要提供一个图标,以便用户选择和识别,通常情况下,应用扩展的图标与应用程序的图标是相同的。

虽然大多数的应用程序扩展都提供了一些自定义的用户界面,但一般用户不会看到你自定义的用户界面,除非他们进入到应用程序扩展中。当用户进入扩展,你自定义的界面可以将用户的视线、思维带入新的上下文中。由于用户在应用中是可以辨别出你开发的扩展的,所以当你的扩展功能非常独特或者击中用户内心需求的时候,他们会各位青睐你的扩展。当用户意识到扩展其实是独立运行的实体时,他们就可以选择将体验不好或功能不好的扩展移除或停用。

为了让用户平滑过渡到你的应用程序扩展,你要斟酌自定义的界面与扩展点界面的风格,做一个权衡。比如说,最好的方法就是让你的通知扩展看起来像是通知中心中原生的Widget,再比如说照片编辑扩展,你应该完全遵循iOS中的Photos应用界面去设计扩展的界面。

注意:即使你的应用程序扩展没有自定义用户界面(不包括图标),但用户仍然知道该扩展来自与当前应用不同的应用,因为通过激活扩展的方式,或扩展的功能就可以判断出。

原文地址:App Extensions Increase Your Impact

Swift中的Bool类型是许多原始函数的基础。所以基于它可以展示一个有趣的如何构建基本类型的示例。这篇文章的主旨是在Swift中创建一个类似Bool类型的新类型MyBool。我们希望通过这个简单的示例,能让你更清晰的了解Swift语言的工作原理。

让我们从最基本的定义开始。我们用枚举来定义MyBool类型的模型,它有两个不同的case

enum MyBool {
    case myTrue, myFalse
}

为了使大家不会产生混淆,这篇文章中我们将MyBool的两个case命名为myTruemyFalse。我们希望MyBool的构造函数MyBool()将其自身赋值为false,所以我们提供了如下init方法:

extension MyBool {
    init() { self = .myFalse }
}

Swift中的枚举会隐式的在它们自身内申明其枚举检索器的范围,允许我们使用MyBool.myFalse这种语法调用其成员,如果根据上下文可以推断出类型的话,我们甚至可以使用.myFalse调用其成员。但是在真正使用中,我们还是希望使用原始的truefalse关键字。想要做到这一点,我们的新类型MyBool需要遵循BooleanLiteralConvertible协议,像这样:

extension MyBool : BooleanLiteralConvertible {
    static func convertFromBooleanLiteral(value: Bool) -> MyBool {
        return value ? myTrue : myFalse
    }
}

// 我们现在就可以给MyBool类型的变量赋值为true或false.
var a : MyBool = true

通过以上设置,我们有了自己的基本类型,但是目前我们用它还做不了什么。布尔值需要通过if条件语句进行测试。在Swift中,我们通过BooleanType协议来做到这一点,该协议允许任意类型用于进行逻辑判断:

extension MyBool : BooleanType {
    func getBooleanType() -> Bool {
        switch self {
        case .myTrue: return true
        case .myFalse: return false
        }
    }   
}

// 现在我们就可以将MyBool类型的变量a用于'if'和'while'语句中进行测试.
if a {}

我们更希望所有遵循了BooleanType协议的类型都要强制转换为MyBool类型,所以我们可以这样写:

extension MyBool {
    // MyBool类型构造函数的参数设定为BooleanType类型.
    init(_ v : BooleanType) {
        if v.getBooleanType() {
            self = .myTrue
        } else {
            self = .myFalse
        }
    }
}

// 现在我们就可以这样进行转换了.
var basicBool : Bool = true
a = MyBool(basicBool)

注意构造函数里的_,它可以使我们在使用构造函数时省略参数命名。可以使用MyBool(x)这种语法,而不用使用MyBool(v: x)这种啰嗦的语法。

现在我们有了基本的功能,现在让我们来定义它的操作符,先来看看如何定义==操作符。没有关联数据的简单的枚举(像MyBool一样)是由编译器自动认为遵循Equatable协议进行编译的,所以没有必须要实现额外的代码。尽管如此,你也可以让任意类型遵循Equatable协议,并实现==操作符。比如我们的MyBool类型:

extension MyBool : Equatable {
}

func ==(lhs: MyBool, rhs: MyBool) -> Bool {
    switch (lhs, rhs) {
    case (.myTrue,.myTrue), (.myFalse,.myFalse):
        return true
    default:
        return false
    }
}

// 现在我们就可以使用==和!=进行比较了.
if a == a {}
if a != a {}

这里我们在swich语句中使用简单的匹配模式来处理。由于MyBool现在遵循了Equatable协议,所以他已经自动实现了!=操作符。再让我们加一些二进制运算符:

func &(lhs: MyBool, rhs: MyBool) -> MyBool {
    if lhs {
        return rhs
    }
    return false
}

func |(lhs: MyBool, rhs: MyBool) -> MyBool {
    if lhs {
        return true
    }
    return rhs
}

func ^(lhs: MyBool, rhs: MyBool) -> MyBool {
    return MyBool(lhs != rhs)
}

有了基本的运算符后,我们就可以实现各种有用的一元和复合赋值运算符,比如:

prefix func !(a: MyBool) -> MyBool {
    return a ^ true
}

// 复合赋值(按位)
func &=(inout lhs: MyBool, rhs: MyBool) {
    lhs = lhs & rhs
}

&=运算符将左边的运算对象作为inout对象,因为要对它进行读写操作,并且其效果对于操作者是可见的。Swift提供的值类型,使我们可以完全掌控丰富多变的各种操作,比如enumstruct

至此,简单的MyBool类型已经具备了基本的运算符操作。希望这篇文章能给你一些思路,使你能够在自己的代码中构建更高级更复杂的类型。

原文地址:Boolean