在Swift中,类型分为两类:第一种是值类型,该类型的每个实例持有数据的副本,并且该副本对于每个实例来说是独一无二的一份,比如结构体(struct)、枚举(enum)、元组(tuple)都是值类型。第二种是参照类型,该类型的实例共享数据唯一的一份副本(在native层面说的话,就是该类型的每个实例都指向内存中的同一个地址),比如类(class)就是参照类型。在这篇文章中,我们将深入探讨值类型和参照类型的使用价值,以及如何在某种场景下选择正确的类型。

它们有什么不同?

值类型最基本的特点就是复制,这影响到它的赋值、初始化、传参等操作。来看看下面的代码示例:


// 值类型示例
struct S { var data: Int = -1 }
var a = S()
var b = a // a复制一份,并将副本赋值给了b
a.data = 42 // a的数据改变了,但是b的并没有改变
println("\(a.data), \(b.data)") // prints "42, -1"

参照类型的复制行为其实是隐式的创建了一个共享的实例,作为原始类型的参照。下面的例子中,两个变量都会参照唯一的那个共享实例的数据,所当改变这两个变量中任何一个的数据,都会同样作用于原始类型的数据:


// 参照类型示例
class C { var data: Int = -1 }
var x = C()
var y = x // 将x赋值给y
x.data = 42 // 修改了x的数据,其实是修改了参照数据,那么y的数据也会改变
println("\(x.data), \(y.data)") // prints "42, 42"

Mutation在安全性中的角色

选择值类型的一个很重要的原因是可以让你比较容易的理解和掌控你的代码。如果你使用值类型,那么都是唯一的数据值、类型的副本在和你打交道,你对数据的修改只作用于数据所属的类型实例,所以你可以不用担心因为你在某个地方对数据的修改而影响到其他地方的数据。这在多线程环境中非常有用,因为在多线程下,不同的线程有可能会在你不知情的情况下改变数据。发生这种Bug后,调试起来就非常困难。

因为值类型和参照类型的表象区别就在于当你修改类型实例的数据时,它们对原始类型数据的处理方式不同。但是有一种情况,值类型和参照类型的处理方式却又相似,那就是当类型实例的数据为只读的时候。在不存在修改的情况下,值类型和参照类型就没什么区别了。

你可能会觉得这一点很有用,假如说一个class是完全不能被重定义的,那么就比较符合使用Cocoa的NSObject对象的一些习惯,并能很好的保持原本的语义。今天,在Swift你可以通过定义不可改变的存储属性来创建一个不可重定义的类,这样可以避免暴露出的API被修改。事实上,许多普通的Cocoa框架里的类,比如NSURL,都被定义成了不可重定义的类。尽管如此,Swift目前还不提供任何机制像结构体(struct)和枚举(enum)一样去强制使一个class成为不可重定义的类(比如像子类)。

如何选择正确的类型?

如果你想创建一个新类型,那么你应该选择值类型还是参照类型呢?当你使用Cocoa框架时,很多API都是NSObject的子类,那么你就必须要使用参照类型,也就是class。在其他情况下,这里有一些指导建议你可以参考:

使用值类型的情形:

  • 使用==运算符比较实例数据的时候。
  • 你想单独复制一份实例数据的时候。
  • 当在多线程环境下操作数据的时候。

使用参照类型(比如class)的情形:

  • 当使用===运算符判断两个对象是否引用同一个对象实例的时候。
  • 当上下文需要创建一个共享的、可变的对象时。

在Swift中,ArrayStringDictionary都是值类型。它们的使用方式类似C语言中得int,每一个实例都有一份数据。你不需要进行显示的复制操作去防止数据在你不知情的情况下被修改。更重要的是,你可以跨线程进行传参而不需要考虑同步的问题,因为传递值类型很安全。秉着高安全性的精神,这种类型划分模式能帮助你在Swift中写出更加有可预测性的代码。

原文地址:Value and Reference Types

开场白

在今年的WWDC大会上绝对不容忽视的一点就是苹果除了宣布iOS8的发布,还介绍了一门新的编程语言Swift。是与Objective-C大不相同的一门语言,它不但是强类型的语言,而且吸收了当前多种优秀编程语言的特点。

为了能够涵盖到Swift语言所有的新特性,该博客系列将只使用Swift语言来进行讲解。我认为,学习如何使用Swift语言、如何与Cocoa框架交互最有价值的资料应该官方提供的两个文档。我相信如果你一开始就跟着这两个文档学习,你能避免不少弯路。

你还应该经常去看Swift官方博客以及Apple提供的关于该语言的其他资源

既然已经有很多资料和文档能帮助你学习如何使用Swift语言,所以这个博客系列不会将已有的知识点重复讲解。相反,我会根据初学者在使用Swift语言中可能会遇到的一些陷阱、难以理解的知识点进行讲解,尤其当和系统框架一起使用的时候。

今天这篇文章主要使用Xcode 6提供的Playground演示每个章节的代码示例。你可以在ShinobiControls的Github主页上下载这些示例代码。地址:github.com/ShinobiControls/iOS8-day-by-day

如果你对这篇文章里的知识点有什么问题或者其他建议,你可以联系我。我会在最新一期的博客中进行说明。你可以在底部进行评论或者给我发Twitter-@iwantmyrealname

构造函数

Swift围绕着对象构造过程形成了一个概念,包括命名、方便的构造器以及在对象的构造阶段指定了严格的调用顺序等。在未来几周内,该博客系列将会有一篇文章深入详细地介绍Swift中对象的构造过程,以及对你写的Objective-C有何影响。

Swift与Objective-C另外一个最大的不同之处就是构造函数的返回值和构造失败的处理。在Objective-C中的构造器大多数都是这样:


- (instancetype)init {
self = [super init];
if (self) {
// Do some stuff
}
return self;
}

然而在Swift中是这样:


init {
variableA = 10
...
super.init()
}

这里要注意的是,Objective-C中的构造器负责创建并返回self,但是Swift中的构造函数却没有返回值。这意味着在Swift的构造函数中不可能也没有机会返回一个nil的返回值,而这在Objective-C中恰恰是表明构造失败的通用模式。

这显然是Swift语言新的Beta版中需要修改的问题。那么现在我们唯一的解决办法是通过类方法返回一个Optional类型用于表明是否构造成功:


class MyClass {
class func myFactoryMethod() -> MyClass? {
...
}
}

有趣的是Objective-C中的工厂方法被转换成了Swift中的构造方法,这一点显然是不可取的。但是,到目前为止,我们只能选择Swift提供的有潜在隐患的构造方法。

可变性

在Cocoa开发者中,可变性这个概念已然不是什么新鲜的东西了,比如在合适的情景下,我们会使用NSArray的可变类型NSMutableArray。Swift将这个概念进行了升华,并且它将不可变性升级为一个基本概念。

Swift中用let关键字定义一个不可变的变量,也就是常量。比如说:


let a = MyClass()
a = MySecondClass() // 不允许

这意味着,我们不能改变被let关键字定义的常量的值,以及该常量引用对象的类型。这里需要注意的是,如果引用的类型是一个值类型(比如结构体)那么不论是引用类型还是引用类型自身都是不可变的。如果引用的是一个类,那么它的引用类型也是是不可变的,但是引用类型自身是可以改变的。

我们来看看下面这个结构体:


struct MyStruct {
let t = 12
var u: String
}

如果你用var关键字定义一个变量名叫struct1,那么你可以对其做以下操作:


var struct1 = MyStruct(t: 15, u: "Hello")
struct1.t = 13 // 编译错误,因为t是一个常量,也就是不可变属性
struct1.u = "GoodBye"
struct1 = MyStruct(t: 10, u: "You")

你可以对u属性重新赋值,也可以对struct1重新赋值,因为他们都是用var定义的变量。但是你不能对t属性重新赋值,因为它是用let定义的常量属性。下面我们再来看看如果用let关键字定义一个MyStruct的实例常量,会发生什么:


let struct2 = MyStruct(t: 12, u: "World")
struct2.u = "Planet" // 编译错误,struct2是一个常量,也就是不可变的
struct2 = MyStruct(t: 10, u: "Defeat") // 编译错误,struct2的类型是不可变的

在上述代码片段的情况下,你不能改变struct2的类型,同样也不能改变这个类型自身(比如该类型的属性u),这是因为结构体是一个值类型

但如果引用的是一个类,那么上述代码示例的结果就会有所不同了,我们先看看下面这个类的定义:


class MyClass {
let t = 12
var u: String

init(t: Int, u: String) {
self.t = t
self.u = u
}
}

我们使用var定义一个变量class1


var class1 = MyClass(t: 15, u: "Hello")
class1.t = 13 // 编译错误,因为t是一个常量,也就是不可变属性
class1.u = "GoodBye"
class1 = MyClass(t: 10, u: "You")

从上述代码示例可以看出,你可以改变类型以及该类型中使用var定义的属性,但是不能改变使用let定义的属性。当我们用let定义一个常量class2,看看会发生什么:


let class2 = MyClass(t: 12, u: "World")
class2.u = "Planet" // 编译通过
class2 = MyClass(t: 11, u: "Geoid") // 编译错误,class2的类型是不可变的

从上述代码示例中可以看出,你可以改变类型自身,也就是可以改变类型中使用var定义的属性。这是因为类是一个引用类型

这种行为很好理解,在文档中也有较全面的解释。但是当我们查看Swift中的集合类型时,就会产生一些疑惑。

NSArray是一个引用类型。也就是说,当你实例化一个NSArray时,你创建的变量实际是指向了该数组在内存中的位置,所以在Objective-C中,用*号定义该变量。如果你回顾一下我们刚才说的使用letvar关键字定义引用类型和值类型的知识点,那么你应该基本清楚letvar关键字的用法和区别。但实际上,在Objective-C中,要想使用一个可变的NSArray,你需要使用另外一个类NSMutableArray

Swift中的数组与Objective-C中的就有很大区别了,数组不再是引用类型,而是值类型。这意味着在使用中,它们像上述代码示例中的结构体一样使用,而不像类那样使用。因此,letvar关键字不仅仅是指定变量是否可以被重新定义,同时也会指定创建的数组是否是一个可变数组。

使用var定义的数组不仅可以对其重新赋值,也可以改变数组:


var array1 = [1,2,3,4]
array1.append(5) // [1,2,3,4,5]
array1[0] = 27 // [27,2,3,4,5]
array1 = [3,2] // [3,2]

但是用let定义的数组都不能进行上述的改变:


let array2 = [4,3,2,1]
array2.append(0) // 编译错误,array2是不可变数组
array2[2] = 36 // 编译错误,array2是不可变数组
array2 = [5,6] // 编译错误,不能对array2重新赋值

在这一点,Swift与Objective-C有巨大的差异,所以会给我们在实际开发中造成一些概念上混淆。也许在Swift之后的版本中对此会有所改变,所以要时刻关注Swift的文档。

通过上面的示例代码我们可以得出一个结论,因为在Swift中数组是一个值类型,所以当你创建一个数组时,实际上是将数组的副本赋值给了变量或常量。但在Objective-C中,创建一个数组时,是将一个指针赋值给变量,也就是引用了该数组在内存中的地址。所以在Objective-C中将它作为方法的参数传递时,始终传递的是数组在内存中的那块相同的地址。而在Swift中,传递的是数组的副本。同时根据数组中存储的对象的类型你可以对数组进行完全复制或者部分复制。在开发过程中要时刻注意这点。

强类型和AnyObject

强类型是Swift语言最大的一个特点。它可以提高我们代码的安全性,因此,以前在Objective-C中会在运行时才能发现的异常,现在在编译时就可以发现了。

这一点非常棒。但是当你在Swift中使用Objective-C框架进行开发时,你要特别注意AnyObject这个类型。它相当于Objective-C中的id。在有些时候,AnyObject给我们的感觉好像又不太符合Swift的特性。因为你可以将AnyObject作为任何的类型去使用,但是这会导致程序在运行时发生异常。实际上,AnyObject的使用方式大多时候都和Objective-C中的id一样。不同的一点是,当你用AnyObject代表一个类型,但是你使用了该类型中并不存在的方法或属性时,它会返回nil


let myString: AnyObject = "hello"
myString.cornerRadius // 返回 nil

为了更符合Swift语言的特性,在使用Cocoa API时,你也许该使用如下的模式:


func someFunc(parameter: AnyObject!) -> AnyObject! {
if let castedParameter = parameter as? NSString {
// Now I know I have a string :)
...
}
}

如果你确定你传入的参数就是String类型的,你就没必要在强制转换时用?来保护了:


let castedParameter = parameter as NSString

根据上述内容我们得知,转换数组其实也是一件很容易的事情。你从Cocoa框架接收的所有数组的类型都是[AnyObject],因为NSArray不支持泛型。然而在大多数情况下,不是所有的元素都是同一类型,但是他们都有已知类型。所以你可以使用有条件判断的转换,也可以不用条件判断进行转换,像下面代码中的语法:


func someArrayFunc(parameter: [AnyObject]!) {
let newArray = parameter as [String]
// Do something with your strings :)
}

协议一致性

协议在Swift很好理解,定义方式如下:


protocol MyProtocol {
func myProtocolMethod() -> Bool
}

有件事一定是你经常想做的,那就是判断一个对象是否遵循了指定的协议,我们可以这样写:


if let class1AsMyProtocol = class1 as? MyProtocol {
// We're in
}

但是这里存在一个错误,因为判断指定的这个协议必须是一个Objective-C协议,所以在定义协议时要加上@objc标签;


@objc protocol MyNewProtocol {
func myProtocolMethod() -> Bool
}

if let class1AsMyNewProtocol = class1 as? MyNewProtocol {
// We're in
}

虽然看起来只是加了@objc标签,但实际上不仅如此,因为在协议前加上了@objc标签,所以协议里的所有属性和方法返回值类型都被解析成Objective-C中的类型了。

枚举

枚举在Swift中变得更加实用。现在不仅可以在枚举中关联值(它们可以是不同类型的),也可以包含函数。


enum MyEnum {
case FirstType
case IntType (Int)
case StringType (String)
case TupleType (Int, String)

func prettyFormat() -> String {
switch self {
case .FirstType:
return "No params"
case .IntType(let value):
return "One param: \(value)"
case .StringType(let value):
return "One param: \(value)"
case .TupleType(let v1, let v2):
return "Some params: \(v1), \(v2)"
default:
return "Nothing to see here"
}
}
}

这非常有用:


var enum1 = MyEnum.FirstType
enum1.prettyFormat() // "No params"
enum1 = .TupleType(12, "Hello")
enum1.prettyFormat() // "Some params: 12, Hello"

只需要做一些小练习,你就能体会出它的强大之处。

总结

Swift的确很强大,它还需要我们通过实践去摸索新的开发模式,而不必局限在Objective-C的模式中。这篇文章列举了一些从Objective-C转到Swift中可能遇到的潜在的问题和疑惑,但希望不要让你对Swift却步。所有与本博客系列相关的项目和代码示例都是用Swift写的,并且都很通俗易懂。

本文中的代码示例都是在Playground中完成的,你可以在github.com/ShinobiControl/iOS8-day-by-day下载这些示例。

原文地址:iOS8 Day-by-Day :: Day 1 :: Blagger’s Guide to Swift

在Swift官方《The Swift Programming Language》文档的Basic Operators一节中添加了Nil Coalescing Operator小结,介绍了一个新的运算符Nil Coalescing,符号是??,它的表现形成如下:

let c = a ?? b

这个运算符有两个条件:

  • a必须是Optional类型的。
  • b的类型必须要和a解包后的值类型一致。

符合这两个条件后,我们来解释一下上述这行代码,意思就是c的值是ab中一个的值,但有前提条件,就是当a解包后值不为nil时,那么就将a解包后的值赋值给c,如果a解包后值为nil,那么就将b的值赋值给c

我们还可以用三目运算来更形象的解释这个运算符:

let c = a != nil ? a! : b

从上面的代码我们很容易理解,当a的值不等于nil时,将a解包后的值赋值给c,否则将b的值赋值给c

我们来看看官方给的代码示例:

let defaultColorName = "red"
var userDefinedColorName: String?   // 默认值为nil

var colorNameToUse = userDefinedColorName ?? defaultColorName
// 因为userDefinedColorName的值是nil,所以colorNameToUse的值为"red"


userDefinedColorName = "green"
colorNameToUse = userDefinedColorName ?? defaultColorName
// 因为userDefinedColorName的值不为nil,所以colorNameToUse的值为"green"

大家看到这应该对Nil Coalescing这个运算符有比较清晰的理解了,但在实际运用中,我们还需要注意以下几点。

编译器中的类型匹配原则

原谅我在末尾加的;,编码习惯而已 = =||

我们先来看一段代码:

let a: Int? = nil;
let b: Int? = 7;

let c = a != nil ? a! : b; // 因为a的默认值为nil,所以c的值为{Some 7}
let d = a ?? b; // 这里d的值为nil,这是怎么回事?

首先我们需要注意的是,在官方文档中有这么一句话:“The expression b must match the type that is stored inside a”。但是上面的代码示例中,我们的bInt?类型,那么此时我们的编译器会怎么处理呢?我们通过自己实现一个Nil Coalescing运算符来说明,代码片段如下:

infix operator ??? {
    associativity right;
    precedence 110;
}

func ???<T>(a: T?, b: @autoclosure () -> T) -> T {
    return a != nil ? a! : b();
}

let d = a ??? b; // 这里d的值仍然是nil

从上面的代码片段中我们可以看出,a的类型是根据传入的b的类型决定的,所以当我们传入的bInt?类型时,编译器其实将a的类型自动转换为Int??类型了,也就是Optional(a),那么我们就能解释let d = a ?? b;这行代码。因为a == nil但是Optional(a) != nil,所以d = Optional(a)!d的值为nil

Nil Coalescing运算符返回值的类型

我们先看示例代码片段:

let a: Int? = nil;
let b: Int? = 5;
let c: Int? = 6;

// 因为a的默认值为nil,所以将b的值赋值给x,x的值为{Some 5},类型为Int?
let x = a != nil ? a! : b;
// 因为c的值不为nil,所以将c解包后的值赋值给y,但是y的值却是{Some 6},而不是6,这是怎么回事?
let y = c != nil ? c! : b;

根据上面我们提到的编译器中类型判断的原则就可以理解了,因为b的类型是Int?,所以在编译时c的类型已经成了Int??,所以为Int??解包的类型就是Int?了。

如果我们声明变量y的类型,编译器就要提出抗议了:

// 如何a为Int?类型,那么编译器就不会通过,因为会导致赋值运算符两边类型不等
let x: Int = a != nil ? a! : b;

// 这种写法和上面一样,都不会编译通过
let y: Int = a ?? b;

当你准备好开发一个应用扩展时,要选择一个合适的扩展点来呈现你的应用扩展的功能。然后使用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能挑起你的好奇心,鼓励你不断的来测试你的代码。

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