as出错 swift

2017-12-24 20:41:13 wangyanchang21 阅读数 2474

Swift关键字总结上篇
Swift关键字总结下篇


Swift中有多少关键字?

在Swift官方文档的词汇结构中, 有非常多的关键字, 它们被用于声明中、语句中、表达式中、类中、模式中, 还有以数字符号开头的关键字, 以及特定上下文环境使用的关键字。 本文中涉及的代码可以在这里下载代码资源

另外, 在特性中还有一些关键字, 是以@开头的关键字。这些所有的关键字将在 Swift关键字总结上篇Swift关键字总结下篇 两篇文章中详细列举。

本篇主要写到不带符号的关键字, 如带#的关键字和带@的特性将在下篇文章中详细说明。

用在声明中的关键字

associatedtypeclassdeinitenumextensionfuncimportinitinoutinternalletoperatorprivateprotocolpublicopenfileprivatestaticstructsubscripttypealiasvar

用在语句中的关键字

breakcasecontinuedefaultdeferdoelsefallthroughforguardifinrepeatreturnswitchwherewhile

用在表达式和类型中的关键字

ascatchdynamicTypefalseisnil , rethrowssuperselfSelfthrowthrowstruetry

特定上下文中被保留的关键字

associativityconveniencedynamicdidSetfinalgetinfixindirectlazyleftmutatingnonenonmutatingoptionaloverridepostfixprecedenceprefixProtocolrequiredrightsetTypeunownedweakwillSet

起始于数字标记(#)的关键字

#available#column#else#elseif#endif#file#function#if#line#selector#sourceLocation

用在模式中的关键字

_

注意事项

以上的关键字被预留,不能被用作标识符,除非它们像上一节标识符中描述的那样使用反引号(`), 才能使用保留字作为标识符。

有个例外是, 特定上下文中被保留的关键字在特定上下文语法之外可以被用于标识符。

以下标记被当作保留符号,不能用于自定义操作符:(){}[].,:;=@#& (作为前缀操作符)、 ->\?! (作为后缀操作符)。

关键字如何使用?

我想在上面的这些关键字中, 大部分的大家应该烂熟于心了。那我就在其中挑选部分特殊的或不常用的来说一说吧。

inout

在函数的入参的类型前添加一个 inout关键字可以定义一个输入输出形式参数。输入输出形式参数有一个能输入给函数的值,函数能对其进行修改,还能输出到函数外边替换原来的值。

你只能把变量作为输入输出形式参数的实际参数。你不能用常量或者字面量作为实际参数,因为常量和字面量不能修改。在将变量作为实际参数传递给输入输出形式参数的时候,直接在它前边添加一个和符合 (&) 来明确可以被函数修改。

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

var abc = 99
var efg = 88
swapTwoInts(&abc, &efg)

print(abc, efg)
// print result is "88 99\n"

typealias、协议组合类型

类型别名可以为已经存在的类型定义了一个新的可选名字。用 typealias 关键字定义类型别名。一旦为类型创建了一个别名,你就可以在任何使用原始名字的地方使用这个别名。

typealias AudioSample = UInt16
var maxAmplitudeFound = AudioSample.min

typealias Point = (Int, Int) 
let origin: Point = (0, 0)

关于typealias还有一种很高效的用法与协议组合类型相关。

协议组合类型

协议组合类型允许你指定一个值,该值的类型遵循多个协议的要求而不必显式定义一个新的命名型的继承自每个你想要该类型遵循的协议的协议。比如,指定一个协议组合类型 ProtocolA & ProtocolB & ProtocolC实际上是和定义一个新的继承自 ProtocolA,ProtocolB, ProtocolC的协议 ProtocolD是完全一样的,但不需要引入一个新名字同理,标明一个协议组合类型 SuperClass & ProtocolA与声明一个新类型 SubClass继承自 SuperClass 并遵循 ProtocolA是一样的,但不需要引入新名字。

协议组合列表中的每项元素必须是类名,协议名或协议组合类型、协议、类的类型别名。列表可以最多包含一个类。

当协议组合类型包含类型别名,就有可能同一个协议在定义中出现不止一次——重复会被忽略。比如说,下面的 PQR 定义等价于 P & Q & R 。

typealias PQ = P & Q
typealias PQR = PQ & Q & R

associatedtype

定义一个协议时,有时在协议定义里声明一个或多个关联类型是很有用的。关联类型给协议中用到的类型一个占位符名称。直到采纳协议时,才指定用于该关联类型的实际类型。关联类型通过 associatedtype 关键字指定。

这里是一个叫做Container 的示例协议,声明了一个叫做 ItemType 的关联类型:

protocol Container {
    associatedtype ItemType
    mutating func append(_ item: ItemType)
    var count: Int { get }
}

Container 协议定义了所有容器必须提供的功能:
1.必须能够通过 append(_: ) 方法向容器中添加新元素
2.必须能够通过一个返回 Int 值的 count 属性获取容器中的元素数量
3.必须能够通过 Int 索引值的下标取出容器中每个元素。

任何遵循 Container协议的类型必须能指定其存储值的类型。尤其是它必须保证只有正确类型的元素才能添加到容器中。为了实现这些要求, Container 协议声明了一个叫做 ItemType 的关联类型,写作 associatedtype ItemType

struct IntStack: Container {
    // original IntStack implementation
    var items = [Int]()
    
    // conformance to the Container protocol
    typealias ItemType = Int
    mutating func append(_ item: Int) {
        // append...
    }
    var count: Int {
        return items.count
    }
}

IntStack为了实现 Container协议,指定了适用于ItemType的类型是 Int类型。typealias ItemType = IntItemType抽象类型转换为了具体的 Int类型。如果你从代码中删除了 typealias ItemType = Int,一切都会正常运行,因为 ItemType 会由Swift的类型推断推断出来。

另外,还可以为协议定义默认的associatedtype,可以用于不能进行类型推断的时候。这样,在默认的实现中,就可以省略类型关联了。

protocol Statistic {
    associatedtype result: Codable = String

    var count: Int { get }
}

subscript

下标的语法, 下标脚本允许你通过在实例名后面的方括号内写一个或多个值对该类的实例进行查询。它的语法类似于实例方法和和计算属性。使用关键字 subscript 来定义下标,并且指定一个或多个输入形式参数和返回类型,与实例方法一样。与实例方法不同的是,下标可以是读写也可以是只读的。这个行为通过与计算属性中相同的 gettersetter 传达:

subscript(index: Int) -> Int {
    get {
        // return an appropriate subscript value here
    }
    set(newValue) {
        // perform a suitable setting action here
    }
}

newValue 的类型和下标的返回值一样。与计算属性一样,你可以选择不去指定 setter 的(newValue)形式参数。 setter 默认提供形式参数 newValue ,如果你自己没有提供的话。

struct TimesTable {
    let multiplier: Int
    subscript(index: Int) -> Int {
        return multiplier * index
    }
}
let threeTimesTable = TimesTable(multiplier: 3)
print("six times three is \(threeTimesTable[6])")
// prints "six times three is 18"

operator、prefix、postfix、infix

除了实现标准运算符,在 Swift 当中还可以声明和实现自定义运算符(custom operators)。可以用来自定义运算符的字符列表请参考运算符。

新的运算符要在全局作用域内,使用 operator 关键字进行声明,同时还要指定 prefixinfix 或者 postfix 限定符, 语法结构如下:

prefix operator `operatorName`
postfix operator `operatorName`
infix operator operatorname: `precedenceGroup`

上面的代码定义了一个新的名为 +++ 的前缀运算符。这个运算符在 Swift 中并没有意义,我们针对下面这个类SomeNumer的实例来赋予它意义。对这个例子来讲, +++ 作为“平方”运算符。

prefix operator +++

class SomeNumber {
    var minNum = 0
    var maxNum = 0

    static prefix func +++(number: SomeNumber) -> SomeNumber {
        number.minNum = number.minNum * number.minNum
        number.maxNum = number.maxNum * number.maxNum
        return number
    }
}

var aaa = SomeNumber()
aaa.minNum = 3
aaa.maxNum = 6
+++aaa
print(aaa.minNum, aaa.maxNum)
// result is "9 36\n"

需要注意的地方是, 当使用自定义运算时, 传入的参数至少要有一个当前对象, 否则编译不会通过。定义前缀或后缀运算符时,不要指定优先级。但是,如果将前缀和后缀运算符应用于相同的操作时,则首先进行后缀运算。

上面这个例子是前缀prefix, 当然后缀 postfix也是同样的用法, 还有一个中缀 infix是比较特殊的, 涉及到结合性associativity 和 优先级precedence的使用。下面继续来进行说明。

precedenceGroup、precedence、associativity

自定义的中缀( infix )运算符也可以指定优先级和结合性。优先级和结合性中详细阐述了这两个特性是如何对中缀运算符的运算产生影响的。

以下示例定义了一个名为+ - 的新自定义中缀运算符,该运算符属于优先级组AdditionPrecedence:

infix operator +-: AdditionPrecedence
extension SomeNumber {
    static func +- (left: SomeNumber, right: SomeNumber) -> Int {
        return  left.minNum * left.maxNum + right.minNum * right.maxNum
    }
}
print(aaa +- aaa)
// result is 648

infix operator +-: AdditionPrecedence这一行对自定义运算符有一个声明的作用。当然也可以不使用系统中的优先级分组,自己自定义。有一点需要注意,从 Swift4.0开始,声明中必须要使用优先级分组而不支持之前直接定义precedence的语句了,已然废弃。否则会有如下错误产生:

infix operator +- { associativity left precedence 140 }
// Eror: Operator should no longer be declared with body; use a precedence group instead

中缀的表达式中的precedenceGroup 是中缀运算符优先级分组。优先级组声明 (A precedence group declaration) 会向程序的中缀运算符引入一个全新的优先级组运算符的优先级指定运算符在没有分组括号的情况下绑定到其操作数的紧密程度。

自定义优先级分组:

precedencegroup 优先级组名称 {
    higherThan: 较低优先级组的名称
    lowerThan: 较高优先级组的名称
    associativity: 结合性
    assignment: 赋值性
}

precedencegroup定义了一个优先级组,表示操作符优先级别。操作符优先级的定义和类型声明有些类似, 一个操作符需要属于某个特定的优先级。Swift定义了许多优先组与标准库提供的运算符一起使用。例如,加(+)和减( - )运算符属于AdditionPrecedence组,乘(*)和除(/)运算符属于MultiplicationPrecedence组。查看提供这些优先级组请看这里Improved operator declarations。如果没有合适你的运算符的优先级组, 你就需要向我们在例子中做的这样, 自己指定结合律方式和优先级顺序了。

higherThanlowerThan定义了较高优先级组和较低优先级组,说明了新建的优先级组是依赖于现存的优先级组的。 lowerThan 优先级组的属性只可以引用当前模块外的优先级组。当两个运算符为同一个操作数竞争时,比如表达式2 + 3 * 5,优先级更高的运算符将优先参与运算。

优先级组的结合性associativity定义了结合律, 即多个同类的操作符顺序出现时的计算顺序。比较常见的加法和减法都是left, 就是说多个加法同时出现时按照从左往右的顺序计算。比如在加法和乘法中,它们满足加法交换律和乘法交换律,无论怎样的顺序都不影响最后的计算结果。而在减法和除法中,就不可以,是需要从左向右进行计算的(即左关联性的),否则结果就会出错。右关联性的运算符是从右往左分组的,指定为none结合性的运算符就没有结合性。还需要注意的是,同样优先级没有结合性的运算符不能相邻出现,例如<运算符是none结合性,那表示1 < 2 < 3就不是一个有效表达式。

优先级组的赋值性assignment表示在包含可选链操作时的运算符优先级。当设为true时,与优先级组对应的运算符在可选链操作中使用和标准库中赋值运算符同样的分组规则,当设为false或者不设置,该优先级组的运算符与不赋值的运算符遵循同样的可选链规则。

自定义优先级分组举例:

precedencegroup MulAddPrecedence {
    associativity: none
    higherThan: MultiplicationPrecedence
}

infix operator +*: MulAddPrecedence

struct Seat {
    var row = 0
    var column = 0

    static func +* (left: Seat, right: Seat) -> Seat {
        let row = left.row * left.row + right.row * right.row
        let column = left.column * left.column + right.column * right.column
        return Seat(row: row, column: column)
    }
}

defer

defer 语句用于在退出当前作用域之前执行代码。

defer {
    statement
}

defer 语句中的语句无论程序控制如何转移都会被执行。在某些情况下,例如,手动管理资源时,比如关闭文件描述符,或者即使抛出了错误也需要执行一些操作时,就可以使用 defer 语句。

如果多个 defer 语句出现在同一作用域内,那么它们执行的顺序与出现的顺序相反。给定作用域中的第一个 defer 语句,会在最后执行,这意味着代码中最靠后的 defer 语句中引用的资源可以被其他 defer 语句清理掉。

func f() {
    print("111")
    defer { print("First") }
    print("222")
    defer { print("Second") }
    print("333")
    defer { print("Third") }
    print("444")
}
f()

// 打印结果
// 111
// 222
// 333
// 444
// Third
// Second
// First

defer 语句中的语句无法将控制权转移到 defer 语句外部。

fallthrough

fallthrough 语句用于在 switch 语句中转移控制权。fallthrough 语句会把控制权从 switch 语句中的一个 case 转移到下一个 case。这种控制权转移是无条件的,即使下一个 case 的模式与 switch 语句的控制表达式的值不匹配。

fallthrough 语句可出现在 switch 语句中的任意 case中,但不能出现在最后一个 case 中。同时,fallthrough 语句也不能把控制权转移到使用了值绑定的 case

switch 1 {
case 1:
    print("111")
    fallthrough
case 2:
    print("222")
case 3:
    print("333")
default:
    print("default")
}
// result is 
// 111 
// 222

dynamicType

注意: 这个关键字在Swift 4.0 开始已经废弃了(depricate from Swift 4.0)!!!
你可以对类型的实例使用 dynamicType 表达式来获取该实例的动态运行时的类型。

class SomeBaseClass {
    class func printClassName() {
        print("SomeBaseClass")
    }
}
class SomeSubClass: SomeBaseClass {
    override class func printClassName() {
        print("SomeSubClass")
    }
}
let someInstance: SomeBaseClass = SomeSubClass()
// The compile-time type of someInstance is SomeBaseClass,
// and the runtime type of someInstance is SomeSubClass
someInstance.dynamicType.printClassName()
// Prints "SomeSubClass"

do 、 try 、 catch 、throw 、 throws、rethrows

表示错误

这些关键字都是关于错误处理的, 错误处理是相应和接收来自你程序中错误条件的过程。Swift 给运行时可恢复错误的抛出、捕获、传递和操纵提供了一类支持。

在 Swift 中,错误表示为遵循 Error 协议类型的值。这个空的协议明确了一个类型可以用于错误处理。

Swift 枚举是典型的为一组相关错误条件建模的完美配适类型,关联值还允许错误错误通讯携带额外的信息。比如说,SomeError 的错误条件:

enum SomeError: Error {
    case SomeError1
    case SomeError2
    case SomeError3(code: Int)
}

抛出一个错误允许你明确某些意外的事情发生了并且正常的执行流不能继续下去。你可以使用 throw 语句来抛出一个错误。

throw SomeError.SomeError2
throw SomeError.SomeError3(code: value)

抛出错误

为了明确一个函数或者方法可以抛出错误,你要在它的声明当中的形式参数后边写上 throws关键字。使用 throws标记的函数叫做抛出函数。如果它明确了一个返回类型,那么 throws关键字要在返回箭头 ( ->)之前。

func makeSomeError(value: Int)
func makeSomeError(value: Int) throws
func makeSomeError(value: Int) throws -> String

但是只有抛出函数可以传递错误。任何在非抛出函数中抛出的错误都必须在该函数内部处理。函数类型如果要抛出错误就必须使用 throws 关键字标记,而且能重抛错误的函数类型必须使用 rethrows 关键字标记。

完善这个可抛异常的函数实现:

func makeSomeError(value: Int) throws {
    switch value {
    case 1:
        throw SomeError.SomeError1
    case 2:
        throw SomeError.SomeError2
    case 3:
        throw SomeError.SomeError3(code: 888)
    case 4:
    	// 默认的这里随便找了一个错误, 来说明catch的范围
        throw MachError(.exceptionProtected)
    default:
        print("excute normal code")
    }
}

处理错误

在 Swift 中有四种方式来处理错误。你可以将来自函数的错误传递给调用函数的代码中,使用 do-catch 语句来处理错误,把错误作为可选项的值,或者错误不会发生的断言。

使用 do-catch语句来通过运行一段代码处理错误。如果do分句中抛出了一个错误,它就会与 catch分句匹配,以确定其中之一可以处理错误。

这是 do-catch语句的通常使用语法:

do {
    try expression
    statements
} catch pattern 1 {
    statements
} catch pattern 2 where condition {
    statements
}

按照上面的例子来写, 如下:

do {
    try makeSomeError(value: 1)
} catch SomeError.SomeError1 {
    print("SomeError1")
} catch SomeError.SomeError2 {
    print("SomeError2")
} catch SomeError.SomeError3(let anyCode) {
    print("SomeError3 code is \(anyCode)")
}

结合三者, 就是一个完整的例子, 当我们执行如上代码时, 将会catchSomeError1 并打印。

如果将 try 语句换为如下代码时, 打印结果如下:

try makeSomeError(value: 3)
// SomeError2
try makeSomeError(value: 3)
// SomeError3 code is 888
try makeSomeError(value: 4)
// nothing print, because can't catch
try makeSomeError(value: 5)
// excute normal code

makeSomeError执行 default分支时, 抛出的异常是不能 处理的, 因为catch中没有涉及相关的异常所以catch不到的.

convenience

便利构造器是类中比较次要的、辅助型的构造器。你可以定义便利构造器来调用同一个类中的指定构造器,并为其参数提供默认值。你也可以定义便利构造器来创建一个特殊用途或特定输入值的实例。

如果你的类不需要便利构造器你可以不提供它。在为通用的初始化模式创建快捷方式以节省时间或者类的初始化更加清晰明了的时候时候便利构造器。

便利构造器可以将构造过程委托给另一个便利构造器或一个指定构造器。但是,类的构造过程必须以一个将类中所有属性完全初始化的指定构造器的调用作为结束。便利构造器不能调用超类的构造器。

便利构造器有着相同的书写方式,但是要用 convenience 修饰符放到 init 关键字前,用空格隔开:

convenience init(parameters) {
    statements
}
class Food {
    var name: String
    init(name: String) {
        self.name = name
    }
    convenience init() {
        self.init(name: "[Unnamed]")
    }
}
var food = Food.init()
print(food.name)
// result is [Unnamed]

willSet、didSet

可以在声明存储型变量或属性时提供 willSetdidSet 观察器。一个包含观察器的存储型变量或属性以如下形式声明:

var 变量名称: 类型 = 表达式 {  
    willSet(setter 名称) {  
        语句
    }  
    didSet(setter 名称) {  
        语句
    }  
}  

可以在全局范围、函数内部,或者类、结构的声明中使用这种形式的声明。当变量以这种形式在全局范围或者函数内部被声明时,观察器表示一个存储型变量观察器。当它在类和结构的声明中被声明时,观察器表示一个属性观察器。

可以为任何存储型属性添加观察器。也可以通过重写父类属性的方式为任何继承的属性(无论是存储型还是计算型的)添加观察器。

当变量或属性的值被改变时,willSetdidSet 观察器提供了一种观察方法。观察器会在变量的值被改变时调用,但不会在初始化时被调用。

willSet 观察器只在变量或属性的值被改变之前调用。新的值作为一个常量传入 willSet 观察器,因此不可以在 willSet 中改变它。didSet 观察器在变量或属性的值被改变后立即调用。和 willSet 观察器相反,为了方便获取旧值,旧值会传入 didSet 观察器。这意味着,如果在变量或属性的 didiset 观察器中设置值,设置的新值会取代刚刚在 willSet 观察器中传入的那个值。

willSetdidSet 中,圆括号以及其中的 setter 名称是可选的。如果提供了一个 setter 名称,它就会作为 willSetdidSet 的参数被使用。如果不提供 setter 名称,willSet 观察器的默认参数名为 newValuedidSet 观察器的默认参数名为 oldValue

提供了 willSet 时,didSet 是可选的。同样的,提供了 didSet 时,willSet 则是可选的。

open、public、internal、fileprivate、private

这些关键字是 Swift 为代码的实体提供个五个不同的访问级别。这些访问级别和定义实体的源文件相关,并且也和源文件所属的模块相关。

open 访问是最高的(限制最少)访问级别,private 是最低的(限制最多)访问级别。

private

private 访问, 将实体的使用限制于封闭声明中。当一些细节仅在单独的声明中使用时,使用 private 访问隐藏特定功能的实现细节。

fileprivate

File-private 访问, 将实体的使用限制于当前定义源文件中。当一些细节在整个文件中使用时,使用 file-private 访问隐藏特定功能的实现细节。

internal

Internal 访问, 为默认访问级别, 允许实体被定义模块中的任意源文件访问,但不能被该模块之外的任何源文件访问。通常在定义应用程序或是框架的内部结构时使用。

public、open

public 访问和Open 访问, 允许实体被定义模块中的任意源文件访问,同样可以被另一模块的源文件通过导入该定义模块来访问。在指定框架的公共接口时,通常使用 openpublic 访问。

public 访问只能在当前模块中被继承和子类重写。
open 访问仅适用于类和类成员,可以在其他模块外被继承和子类重写。

显式地标记类为 open 意味着你考虑过其他模块使用该类作为父类对代码的影响,并且相应地设计了类的代码。

访问控制的注意事项

Swift 中的访问级别遵循一个总体指导准则:实体不可以被更低(限制更多)访问级别的实体定义。

比如: 一个 public 的变量其类型的访问级别不能是 internal, file-private 或是 private,因为在使用 public 变量的地方可能没有这些类型的访问权限。
又比如: 函数类型的访问级别由函数成员类型和返回类型中的最严格访问级别决定。一个函数不能比它的参数类型和返回类型访问级别高,因为函数可以使用的环境而其参数和返回类型却不能使用。

这里简单列举了两个有关访问级别的使用注意事项。想了解有更多详细的注意事项的朋友, 可以查阅我的另外一篇博文: Swift 之访问控制。这里面有代码举例和详细说明, 小编这里不再赘述。

final

该修饰符用于修饰类或类中的属性、方法以及下标。如果用它修饰一个类,那么这个类不能被继承。如果用final 修饰类中的属性、方法或下标,那么它们不能在子类中被重写。

使用final的情况, 是类或方法属性等不希望被继承和重写,具体情况一般是:
1.类或者方法的功能确实已经完备了, 基本不会再继承和重写。
2.避免子类继承和修改造成危险。有些方法如果被子类继承重写会造成破坏性的后果,导致无法正常工作,则需要将其标为final加以保护。
3.保证父类的方法一定被执行, 我们可以把父类的方法定义成final,同时将内部可以继承的部分剥离出来,供子类继承重写。

还有一中说法, 认为final能改成性能,因为编译器能从final中获取额外的信息,所以可以对类或者方法调用进行优化处理。其实这样优化对性能的提升非常有限,所以如果是为了提升性能, 把所有的属性方法都加上final关键字,也没有多大的作用。

required

必要构造器标识符, 修饰符用于修饰类的指定构造器或便利构造器,表示该类所有的子类都必须实现该构造器。在子类实现该构造器时,必须同样使用 required 修饰符修饰该构造器。

为了要求子类去实现超类的构造器,使用 required 声明修饰符标记超类的构造器。子类实现超类构造器时也必须使用 required 声明修饰符。

class SomeClass1 {
    required init() {
        // 构造器的实现代码
    }
}

在子类重写父类的必要构造器时,必须在子类的构造器前也添加required修饰符,表明该构造器要求也应用于继承链后面的子类。在重写父类中必要的指定构造器时,不需要添加override修饰符:

class SomeSubclass: SomeClass1 {
    required init() {
        // 构造器的实现代码
    }
}

如果子类继承的构造器能满足必要构造器的要求,则无须在子类中显式提供必要构造器的实现。就像下面的代码, 因为子类继承的构造器能满足必要构造器的要求, 所以子类的必要构造器可以是隐性的。代码如下:

class SomeClass1 {
    required init() {
        // 构造器的实现代码
    }
}
class SomeSubclass: SomeClass1 {

}

你可以在遵循协议的类中实现构造器,无论是作为指定构造器,还是作为便利构造器。无论哪种情况,你都必须为构造器实现标上 required 修饰符:

class SomeClass: SomeProtocol {
    required init(someParameter: Int) {
        // 这里是构造器的实现部分
    }
}

使用 required 修饰符可以确保所有子类也必须提供此构造器实现,从而也能符合协议。如果类已经被标记为 final,那么不需要在协议构造器的实现中使用 required 修饰符,因为 final 类不能有子类。如果这个类还没有用 final 声明修饰符标记,这个构造器必须用 required 声明修饰符标记。

这就是为什么在日常开发中, 当我们继承系统某各类去指定一个新的构造器时, 系统总是编译报错, 提示添加如下代码:

required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

这种情况一般会出现在继承了遵守NSCoding protocol的类,比如UIView系列的类、UIViewController系列的类。这是NSCoding protocol定义的,遵守了NSCoding protoaol的所有类必须继承。当我们在子类定义了指定初始化器(包括自定义和重写父类指定初始化器),那么必须显示实现required init?(coder aDecoder: NSCoder),而其他情况下则会隐式继承。

如果一个子类重写了父类的指定构造器,并且该构造器满足了某个协议的要求,那么该构造器的实现需要同时标注 requiredoverride 修饰符:

protocol SomeProtocol {
    init()
}
class SomeSuperClass {
    init() {
        // 这里是构造器的实现部分
    }
}

class SomeSubClass: SomeSuperClass, SomeProtocol {
    // 因为遵循协议,需要加上 required
    // 因为继承自父类,需要加上 override
    required override init() {
        // 这里是构造器的实现部分
    }
}

mutating、nonmutating

结构体和枚举是值类型。默认情况下,值类型属性不能被自身的实例方法修改。但是,如果你确实需要在某个特定的方法中修改结构体或者枚举的属性,你可以为这个方法选择可变(mutating)行为,然后就可以从其方法内部改变它的属性;并且这个方法做的任何改变都会在方法执行结束时写回到原始结构中。方法还可以给它隐含的self属性赋予一个全新的实例,这个新实例在方法结束时会替换现存实例。

结构体中用法:

struct Point {
    var x = 0.0, y = 0.0
    mutating func moveBy(x deltaX: Double, y deltaY: Double) {
        self = Point(x: x + deltaX, y: y + deltaY)
    }
}

枚举中用法:

enum TriStateSwitch {
    case off, low, high
    mutating func next() {
        switch self {
        case .off:
            self = .low
        case .low:
            self = .high
        case .high:
            self = .off
        }
    }
}
var ovenLight = TriStateSwitch.low
ovenLight.next()
// ovenLight is now equal to .high
ovenLight.next()
// ovenLight is now equal to .off

在协议中如何使用? 若你定义了一个协议的实例方法需求,想要改变任何采用了该协议的类型实例,只需在协议里方法的定义当中使用 mutating 关键字。这允许结构体和枚举类型能采用相应协议并满足方法要求。

protocol Togglable {
    mutating func toggle()
}

Togglable协议的定义中, toggle() 方法使用 mutating 关键字标记,来表明该方法在调用时会改变遵循该协议的实例的状态:

struct Test: Togglable {
    var time: Int = 0
    
    mutating func toggle() {
        self.time = 33333
    }
}

var test = Test()
test.time = 2
test.toggle()
// result is 2

如果你在协议中标记实例方法需求为 mutating ,在为类实现该方法的时候不需要写 mutating 关键字。 mutating 关键字只在结构体和枚举类型中需要书写。

dynamic

我来告诉你为什么Swift中要使用关键字dynamic。Swift 中的函数可以是静态调用,静态调用会更快。Swift的代码直接被编译优化成静态调用的时候,就不能从Objective-C 中的SEL字符串来查找到对应的IMP了。这样就需要在 Swift 中添加一个关键字 dynamic,告诉编译器这个方法是可能被动态调用的,需要将其添加到查找表中。

纯Swift类没有动态性,但在方法、属性前添加dynamic修饰可以获得动态性。该修饰符用于修饰任何兼容 Objective-C 的类的成员。访问被 dynamic 修饰符标记的类成员将总是由 Objective-C 运行时系统进行动态派发,而不会由编译器进行内联或消虚拟化。

继承自NSObject的Swift类,其继承自父类的方法具有动态性,其他自定义方法、属性需要加dynamic修饰才可以获得动态性。而且因为使用动态修饰符标记的声明是使用Objective-C运行时分派的,所以它们必须用objc属性标记。(从Swift 4.0开始, 加dynamic 修饰符时必须是显式的objc了)

纯Swift类中的dynamic的使用:

class DynamicSwiftClass {
    var zero = 0
    @objc dynamic var fist = 1
    @objc func dynamicFunc() {
    }
//    open this code will be error
//    @objc dynamic var adddd = (0 , 0)
//    @objc dynamic func someMethod(value: Int) -> (Int, Int) {
//        return (1, 1)
//    }
}

若方法的参数、属性类型为Swift特有、无法映射到Objective-C的类型(如CharacterTuple),则此方法、属性无法添加dynamic修饰, 一旦添加就会编译报错。

optional

该修饰符用于修饰协议中的属性、方法以及下标成员,表示符合类型可以不实现这些成员要求。

可选类型

Swift 为命名类型 Optional<Wrapped> 定义后缀 ? 作为语法糖 ,其定义在 Swift 标准库中。换句话说,下列两种声明是等价的:

var optionalInteger: Int? 
var optionalInteger: Optional<Int>

在上述两种情况下,变量 optionalInteger 都声明为可选整数类型。注意在类型和 ? 之间没有空格。

类型 Optional<Wrapped> 是有两种情况的, noneSome(Wrapped) ,它代表可能没有值或可能有值。任何类型都可以被显式的声明(或隐式的转换)为可选类型。如果你在声明可选的变量或属性时没有提供初始值,它的值则会默认为 nil

使用 ! 操作符获解析一个值为 nil 的可选项会导致运行时错误。你也可以使用可选链和可选绑定来有条件地执行对可选表达式的操作。如果值为 nil ,不会执行任何操作并且不会因此产生运行时错误。

隐式展开可选类型

Swift 为命名类型 Optional<Wrapped> 定义后缀 ! 作为语法糖 ,其定义在 Swift 标准库中,作为它被访问时自动解析的附加行为。如果你试图使用一个值为 nil 的隐式解析,你会得到一个运行时错误。除了隐式展开的行为之外,下面两个声明是等价的:

var implicitlyUnwrappedString: String! 
var explicitlyUnwrappedString: Optional<String>

注意类型与 ! 之间没有空格。
有了可选项,如果在声明隐式展开可选变量或属性时你不用提供初始值,它的值会默认为 nil 。使用可选链有条件地对隐式展开可选项的表达式进行操作。如果值为 nil ,就不执行任何操作,因此也不会产生运行错误。

可选的协议

协议可以定义可选要求,遵循协议的类型可以选择是否实现这些要求。在协议中使用 optional 关键字作为前缀来定义可选要求。可选要求用在你需要和 Objective-C 打交道的代码中。协议和可选要求都必须带上objc属性。标记 objc 特性的协议只能被继承自 Objective-C 类的类或者 objc 类遵循,其他类以及结构体和枚举均不能遵循这种协议。只能将 optional 修饰符用于被 objc 特性标记的协议。这样一来,就只有类类型可以采纳并符合拥有可选成员要求的协议。

使用可选要求时(例如,可选的方法或者属性),它们的类型会自动变成可选的。比如,一个类型为 (Int) -> String 的方法会变成((Int) -> String)?。需要注意的是整个函数类型是可选的,而不是函数的返回值。

协议中的可选要求可通过可选链式调用来使用,因为遵循协议的类型可能没有实现这些可选要求。类似 someOptionalMethod?(someArgument) 这样,你可以在可选方法名称后加上 ? 来调用可选方法。

@objc protocol CounterDataSource {
    @objc optional var fixedIncrement: Int { get }
    @objc optional func incrementForCount() -> Int
}

class Counter {
    var count = 0
    var dataSource: CounterDataSource?
    func increment() {
        if let amount = dataSource?.incrementForCount?() {
            count += amount
        } else if let amount = dataSource?.fixedIncrement {
            count += amount
        }
    }
}

class ThreeSource: NSObject, CounterDataSource {
    let fixedIncrement = 3
}

var counter = Counter()
counter.dataSource = ThreeSource()
for _ in 1...4 {
    counter.increment()
    print(counter.count)
}
// 3
// 6
// 9
// 12

当没有代理没有实现可选的协议时, dataSource?.incrementForCount?()nil, 只有当代理实现了此协议方法时, amount才会是返回的那个值。

indirect

递归枚举是一种枚举类型,它有一个或多个枚举成员使用该枚举类型的实例作为关联值。使用递归枚举时,编译器会插入一个间接层。你可以在枚举成员前加上indirect来表示该成员可递归, 而且被 indirect 修饰符标记的枚举用例必须有一个关联值。。

enum ArithmeticExpression {
    case number(Int)
    indirect case addition(ArithmeticExpression, ArithmeticExpression)
    indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}

你也可以在枚举类型开头加上indirect关键字来表明它的所有成员都是可递归的:

indirect enum ArithmeticExpression {
    case number(Int)
    case addition(ArithmeticExpression, ArithmeticExpression)
    case multiplication(ArithmeticExpression, ArithmeticExpression)
}

上面定义的枚举类型可以存储三种算术表达式:纯数字、两个表达式相加、两个表达式相乘。枚举成员additionmultiplication的关联值也是算术表达式。

下面的代码展示了使用ArithmeticExpression这个递归枚举创建表达式(5 + 4) * 2:

let five = ArithmeticExpression.number(5)
let four = ArithmeticExpression.number(4)
let sum = ArithmeticExpression.addition(five, four)
let product = ArithmeticExpression.multiplication(sum, ArithmeticExpression.number(2))

使用这个枚举:

func evaluate(_ expression: ArithmeticExpression) -> Int {
    switch expression {
    case let .number(value):
        return value
    case let .addition(left, right):
        return evaluate(left) + evaluate(right)
    case let .multiplication(left, right):
        return evaluate(left) * evaluate(right)
    }
}
print(evaluate(product))
// 打印 "18"

相关资料:
Swift关键字总结上篇
Swift关键字总结下篇

2016-08-01 10:54:02 kmyhy 阅读数 5304

原文:Magical Error Handling in Swift
作者: Gemma Barlow
译者:kmyhy

Swift 中的错误处理从 O-C 沿袭而来,但 Swift 1.0 之后逐渐发生了巨大改变。重要的改变发生在 Swift 2,它率先使用了“处理非异常的状态和条件”的做法,使你的 app 变得更加简单。

类似于其它编程语言,在 Swift 中,选择使用哪种错误处理技术,需要根据具体的错误类型和 app 整体架构而定。

本教程将演示一个“魔法”,在这个例子中,不但有男巫、女巫和蝙蝠,还有蟾蜍,以此来演示在常见错误处理过程中的最佳实践。你还可以看到,如何将使用 Swift 早期版本编写的错误处理进行升级,最终使用你的水晶球看到未来 Swift 的错误处理将是什么样子。

注:本教程假设你已经熟悉了 Swift 2 语法——尤其是枚举和可空。如果你不知道这些概念,请阅读 Greg Heo 的 What’s New in Swift 2 post

好了,让我们开始领略 Swift2 的错误处理的迷人魅力吧!

开始

本教程有两个开始项目(playground)。一节一个,分别是:Avoiding-Errors-with-nil-Starter.playgroundAvoiding-Errors-with-Custom-Handling-Starter.playground

打开第一个 playground 文件。

阅读代码,你将发现几个类、结构和枚举。

注意如下代码:

protocol MagicalTutorialObject {
  var avatar: String { get }
}

这个协议会被教程中所有类和结构所采用,并用于提供一个能够将每个对象打印到控制台的 String 对象。

enum MagicWords: String {
case Abracadbra = “abracadabra”
case Alakazam = “alakazam”
case HocusPocus = “hocus pocus”
case PrestoChango = “presto chango”
}

这个枚举用于表示“咒语”,它将被“念”(spell)出来。

struct Spell: MagicalTutorialObject {
  var magicWords: MagicWords = .Abracadbra
  var avatar = "*"
}

这个结构用于将咒语“念”出来。默认情况下,其 magicWords 属性的初始值是 Abracadabra。

你已经了解在这个魔法世界的基本知识了,你可以开始练习咒语了。

为什么要进行错误处理?

“错误处理是一门让错误变得优雅的艺术。”
–Swift Apprentice,第 21 章(错误处理)

良好的错误处理能增强用户体验,让软件维护者更容易发现问题,了解出错的原因以及错误的严重性。当代码中的错误的处理无所不在的时候,诊断问题就变得更加容易了。错误处理还会让系统以正确的方式终止执行,避免用户产生不必要的困扰。

当然并不是所有的错误都需要被处理。当不对错误进行处理时,语言特性也会进行某种级别的错误处理。一般,如果你能够避免错误的发生,则尽量避免。如果实在无法避免,则最好的做法就是错误处理。

避免 Swift 引用为空错误

由于 Swift 已经有了优雅的可空处理机制,类似这种错误:在你以为有值的地方却没有值——是可以完全避免的。作为一个聪明的程序员,你可以利用这种特性,在某种错误发生时故意返回一个 nil。如果你不想在错误发生时采取任何动作时,这种方式很好用,例如在事故发生时采取不作为措施。

避免 Swift 引用为空的两个典型例子就是:允许失败的初始化方法,以及 guard 语句。

允许失败的初始化方法

允许失败的初始化方法防止你创建出不完全满足创建条件的对象。在 Swift 2 之前(已经其它语言),这种方法通常在工厂方法设计模式中用到。

在 Swift 中的这种设计模式体现在 createWithMagicWords 中:

static func createWithMagicWords(words: String) -> Spell? {
  if let incantation = MagicWords(rawValue: words) {
    var spell = Spell()
    spell.magicWords = incantation
    return spell
  }
  else {
    return nil
  }
}

上述初始化方法企图用指定的咒语创建一个 Spell 对象,如果提供给它的 words 参数不是一个合法的咒语,则返回一个 nil 对象。

在本教程底部检查 Spell 对象的创建语句,你会看到:

第一个语句用“abracadabra”成功创建了一个 Spell 对象,但第二句使用”ascendio” 就不行了,返回了一个 nil 对象。(哈,巫师不是每次都能成功念出咒语的)

工厂方法是一种古旧的编程风格。其实在 Swift 中我们可以有更好的选择。你可以将 Spell 中的工厂方法修改为“允许失败的初始化方法”。

删除createWithMagicWords(_:) 并替换为:

init?(words: String) {
  if let incantation = MagicWords(rawValue: words) {
    self.magicWords = incantation
  }
  else {
    return nil
  }
}

这里,在这个方法声明中,我们没有显式地创建和返回一个 Spell 对象。

噢,这两句出现编译错误了:

let first = Spell.createWithMagicWords("abracadabra")
let second = Spell.createWithMagicWords("ascendio")

你需要将它们修改成调用新方法。将上面的语句修改为:

let first = Spell(words: "abracadabra")
let second = Spell(words: "ascendio")

这样,错误消失,playground 编译成功。这种改变让你的代码更整洁——但你还有更好的解决办法!

Guard 语句

guard 语句是一种更好的断言某些情况为 true 的方式:例如,判断一个值大于 0,或者判断某个值是否能够被解包的时候。如果这种情况都不满足,你可以执行语句块。

guard 语句在 Swift 2 才开始引入,通常用于在调用堆栈中进行冒泡法错误处理,这种方法中,错误将在最后才被处理。guard 语句能够尽早从方法/函数中退出,比起需要判断某个条件满足剩下的逻辑才会执行来说,显得更加简单。

将 Spell 的允许失败的初始化方法修改为 guard 语句:

init?(words: String) {
  guard let incantation = MagicWords(rawValue: words) else {
    return nil
  }
  self.magicWords = incantation
}

在这里,我们不需要将 else 放在单独的行上,而且对断言失败的处理变得显眼,因为它被更放在了方法的头部。同时,“黄金路径”缩进最少。“黄金路径”是指当每件事都如预期即没有错误发生时的执行路径。而缩进最少,则使它更易于被看到。

注,虽然 first 和 second 最终值不会有任何改变,但代码变得更加合理化。

对错误进行定制化处理

在完成 Spell 的初始化方法并利用 nil 避免某些错误之后,你将学习某些更高级的错误处理。

对于本教程的第二部分内容,请打开 Avoiding Errors with Custom Handling – Starter.playground。

看一下这些代码:

struct Spell: MagicalTutorialObject {

  var magicWords: MagicWords = .Abracadbra
  var avatar = "*"

  init?(words: String) {
    guard let incantation = MagicWords(rawValue: words) else {
      return nil
    }
    self.magicWords = incantation
  }

  init?(magicWords: MagicWords) {
    self.magicWords = magicWords
  }
}

这是 Spell 的初始化方法,在第一部分内容的基础上修改而来。注意,MagicalTutorialObject 协议的使用,以及第二个允许失败的初始化方法,为了方便我们添加了它。

protocol Familiar: MagicalTutorialObject {
  var noise: String { get }
  var name: String? { get set }
  init()
  init(name: String?)
}

Familiar 协议会被使用到各种动物(比如蝙蝠和蟾蜍)。

注:Familiar 的意思是仆从,也就是男巫或女巫的动物精灵,拥有类人的特点。比如《哈利波特》中的猫头鹰(名为 Hedwig),或者《The Wizard of Oz》中的飞猴。

虽然它不是 Hewig,但仍然很漂亮,不是吗?

struct Witch: MagicalBeing {
  var avatar = "*"
  var name: String?
  var familiar: Familiar?
  var spells: [Spell] = []
  var hat: Hat?

  init(name: String?, familiar: Familiar?) {
    self.name = name
    self.familiar = familiar

    if let s = Spell(magicWords: .PrestoChango) {
      self.spells = [s]
    }
  }

  init(name: String?, familiar: Familiar?, hat: Hat?) {
    self.init(name: name, familiar: familiar)
    self.hat = hat
  }

  func turnFamiliarIntoToad() -> Toad {
    if let hat = hat {
      if hat.isMagical { // When have you ever seen a Witch perform a spell without her magical hat on ? :]
        if let familiar = familiar {   // Check if witch has a familiar
          if let toad = familiar as? Toad {  // Check if familiar is already a toad - no magic required
            return toad
          } else {
            if hasSpellOfType(.PrestoChango) {
              if let name = familiar.name {
                return Toad(name: name)
              }
            }
          }
        }
      }
    }
    return Toad(name: "New Toad")  // This is an entirely new Toad.
  }

  func hasSpellOfType(type: MagicWords) -> Bool { // Check if witch currently has appropriate spell in their spellbook
    return spells.contains { $0.magicWords == type }
  }
}

最后,是女巫。请看下面:

  • 女巫的初始化需要一个名字和一只精灵,或者一个名字、一只精灵和一顶帽子。
  • 女巫会念许多咒语,用一个 spells 保存,即一个 Spell 数组。
  • 女巫有一个嗜好,当她一念到咒语:“PrestoChango”,她的精灵就会被变成一只蟾蜍,这个动作用 turnFamiliarIntoToad() 方法

注意 turnFamiliarIntoToad() 方法中的缩进。在这个方法中,如果遇到任何错误,会返回一只全新的蟾蜍。这看起来有点不对劲(这是错误的!)。在下一部分,你将用自定义错误处理来解决这个问题。

用 Swift 错误进行重构

Swift 提供了运行时抛出、捕获、传递和操纵可恢复类型错误的支持。
-《The Swift Programming Language (Swift 2.2)》

与“死亡之庙”不同,在 Swift 或其它语言中,“厄运金字塔”是另外一种相反的模型。使用这种模型会在控制流中使用多级嵌套。例如上面的 turnFamiliarIntoToad() 方法,使用了 6 个 } 符号才能结束嵌套,基本构成了一条对角线。这样的代码阅读起来相当费劲。

厄运金字塔

使用先前提到的 guard 语句,以及可空绑定,能够避免出现“厄运金字塔”代码。do-catch 机制能够将错误处理从控制流中解耦出来,从减少“厄运金字塔”的出现。

do-catch 机制常用的关键字包括:

  • throws
  • do
  • catch
  • try
  • defer
  • ErrorType

要试一试 do-catch 机制,你将抛出多个自定义错误。首先,你需要定义一个枚举,将所有你想处理的状态列到其中,而这些状态可能表明某个地方东西出错了。

在 Witch 类定义之上添加如下代码:

enum ChangoSpellError: ErrorType {
  case HatMissingOrNotMagical
  case NoFamiliar
  case FamiliarAlreadyAToad
  case SpellFailed(reason: String)
  case SpellNotKnownToWitch
}

关于 ChangoSpellError 有两点需要注意:

  • 它采用了 ErrorType 协议,这是必须的。在 Swift 中, ErrorType 表明了这是一种错误。
  • 在 SpellFailed 的 case 分支,你可以指定一种自定义的原因,表示为什么咒语会念错。

注:ChangoSpellError 的名字来自于咒语“Presto Chango!”——女巫在将精灵变成蟾蜍时念的咒语。

好了,亲爱的,赶紧施展你的魔法吧。很好。在方法签名中添加一个 throws 关键字,表明方法调用时可能会抛出错误:

func turnFamiliarIntoToad() throws -> Toad {
Update it as well on the MagicalBeing protocol:
protocol MagicalBeing: MagicalTutorialObject {
  var name: String? { get set }
  var spells: [Spell] { get set }
  func turnFamiliarIntoToad() throws -> Toad
}

现在,你拥有了错误状态列表,接下来需要重新编写 turnFamiliarIntoToad() 方法,针对每个错误类型编写不同的处理语句。

处理帽子的错误

首先,修改下列语句,确保女巫已经佩戴了她永不离身的魔法师帽。

修改之前的代码:

if let hat = hat {

修改之后的代码:

guard let hat = hat else {
  throw ChangoSpellError.HatMissingOrNotMagical
}

注:不要忘记在方法底部将对应的 } 也删掉。否则 playground 会编译错误!

下一句是对一个布尔值进行检查,这也和魔法师帽有关:

if hat.isMagical {

你可以再用一个 guard 语句进行检查,也可以将两个检查合并到一个 guard 语句——这显然要清晰和简洁得多。因此将第一个 guard 语句修改为:

guard let hat = hat where hat.isMagical else {
  throw ChangoSpellError.HatMissingOrNotMagical
}

然后将 if hat.isMagical { 删除。

在接下来的部分,你将继续破解“金字塔”问题。

处理精灵的错误

接着,判断巫师是否有一只精灵:

if let familiar = familiar {

将这句用抛出一个 .NoFamiliar 错误来替换:

guard let familiar = familiar else {
  throw ChangoSpellError.NoFamiliar
}

忽略此时出现的任何错误,因为接下来的代码会让它们消失。

处理蟾蜍的错误

接下来一句,如果女巫在试图用 turnFamiliarIntoToad() 方法时发现她的精灵其实已经是一只蟾蜍了,则返回已有的蟾蜍。但这里更好的做法是,用一个错误来表示这种情况。将下列代码:

if let toad = familiar as? Toad {
  return toad
}

修改为:

if familiar is Toad {
  throw ChangoSpellError.FamiliarAlreadyAToad
}

注意,我们将 as? 改为了 is。在需要检查某个对象是否能够转换为某个协议,但同时不需要使用转换结果时,这种写法更加简洁。is 关键字也可以更加泛型化的方式进行类型比较。如果你想了解更多内容,请阅读The Swift Programming Language“类型转换”一节。

将 else 之内的代码移到 else 之外,然后删除 else 语句,它没用了。

处理咒语的错误

最后,调用了 hasSpellOfType(type:) 方法,以检查女巫的魔法书中确实有相应的咒语。将下列代码:

if hasSpellOfType(.PrestoChango) {
  if let toad = f as? Toad {
    return toad
  }
}

修改为:

guard hasSpellOfType(.PrestoChango) else {
  throw ChangoSpellError.SpellNotKnownToWitch
}

guard let name = familiar.name else {
  let reason = "Familiar doesn’t have a name."
  throw ChangoSpellError.SpellFailed(reason: reason)
}

return Toad(name: name)

现在,删除最后一行不安全的代码。也就是这行:

return Toad(name: "New Toad")

现在,你的方法变得更清晰和整洁,已经能够使用了。我在上述的代码添加了注释,以解释这个方法所做的工作:

func turnFamiliarIntoToad() throws -> Toad {

  // When have you ever seen a Witch perform a spell without her magical hat on ? :]
  guard let hat = hat where hat.isMagical else {
    throw ChangoSpellError.HatMissingOrNotMagical
  }

  // Check if witch has a familiar
  guard let familiar = familiar else {
    throw ChangoSpellError.NoFamiliar
  }

  // Check if familiar is already a toad - if so, why are you casting the spell?
  if familiar is Toad {
    throw ChangoSpellError.FamiliarAlreadyAToad
  }
  guard hasSpellOfType(.PrestoChango) else {
    throw ChangoSpellError.SpellNotKnownToWitch
  }

  // Check if the familiar has a name
  guard let name = familiar.name else {
    let reason = "Familiar doesn’t have a name."
    throw ChangoSpellError.SpellFailed(reason: reason)
  }

  // It all checks out! Return a toad with the same name as the witch's familiar
  return Toad(name: name)
}

你曾经在 turnFamiliarIntoToad() 方法中返回一个可空来表示“在念咒语时出了差错”,但使用自定义错误能够更加清晰地表达错误的状态,以便你根据这些状态采取对应措施。

自定义错误还有什么好处?

现在,你有一个方法抛出了一个自定义 Swift 错误,你需要处理它们。接下来的标准动作是使用 do-catch 语句,这就好比 Java 等语言中的 try-catch 语句。

在 playground 的底部加入下列代码:

func exampleOne() {
  print("") // Add an empty line in the debug area

  // 1
  let salem = Cat(name: "Salem Saberhagen")
  salem.speak()

  // 2
  let witchOne = Witch(name: "Sabrina", familiar: salem)
  do {
    // 3
    try witchOne.turnFamiliarIntoToad()
  }
  // 4
  catch let error as ChangoSpellError {
    handleSpellError(error)
  }
  // 5
  catch {
    print("Something went wrong, are you feeling OK?")
  }
}

一下是对这个方法的解释:

  • 创建女巫的精灵,它的名字叫 Salem。
  • 创建女巫,名字叫 Sabrina。
  • 试图将猫咪变成蟾蜍。
  • 捕获 ChangoSpellError 并进行相应的处理。
  • 捕获其它错误,打印友好信息。

写完上述代码,你会看到一个编译错误——让我们来搞定它。

handleSpellError() 方法还没有定义,在 exampleOne() 方法之上加入这个方法:

func handleSpellError(error: ChangoSpellError) {
  let prefix = "Spell Failed."
  switch error {
    case .HatMissingOrNotMagical:
      print("\(prefix) Did you forget your hat, or does it need its batteries charged?")

    case .FamiliarAlreadyAToad:
      print("\(prefix) Why are you trying to change a Toad into a Toad?")

    default:
      print(prefix)
  }
}

最后,在 playground 最后执行这个方法:

exampleOne()

点击 Xcode 工作空间左下角的上箭头,打开 Debug 控制台,你就会看到 playground 的输出了:

捕获错误

下面对上述代码中的每个语法特性进行简单讨论。

catch

你可以用 Swift 的模板匹配来处理某种错误,或者将错误类型进行分组处理。
前面的代码示范了 catch 的两个用法:一个是用于捕捉 ChangoSpell 错误,一种用于捕捉剩下的错误。

try

try 与 do-catch 语句配合使用,用于清晰定位是哪行语句或代码块将抛出错误。
try 语句有几种不同的用法,上面用到了其中之一:

  • try: 标准用法,在简单的、立即的 do-catch 语句中使用。就是前面代码中的用法。
  • try?: 处理错误,以忽略该错误的方式;如果有错误抛出,这个语句的结果是 nil。
  • try!: 类似强制解包,这个前缀会创建期望的对象,理论上这个语句会抛出错误,但实际上这种错误永远不会发生。 try! 可以用于执行加载文件的动作,特别是当你明确知道文件是肯定存在的。就如前置解包一样,使用这种结构需要特别谨慎。

让我们来体验一下 try? 的使用。复制粘贴下列代码到 playgournd 的底部:

func exampleTwo() {
  print("") // Add an empty line in the debug area

  let toad = Toad(name: "Mr. Toad")
  toad.speak()

  let hat = Hat()
  let witchTwo = Witch(name: "Elphaba", familiar: toad, hat: hat)

  let newToad = try? witchTwo.turnFamiliarIntoToad()
  if newToad != nil { // Same logic as: if let _ = newToad
    print("Successfully changed familiar into toad.")
  }
  else {
    print("Spell failed.")
  }
}

注意和 exampleOne 不同的地方。在这里我们不需要知道具体的错误输出了些什么,只是在它们抛出是捕获它们。Toad 对象最终会创建失败,因此 newToad 的值应当为 nil。

传递错误

throws

在 Swift 中,如果方法或函数代码中会抛出错误,则必须用到 throws 关键字。被抛出的错误会自动在调用堆栈中进行传递,但如果让错误从现场地向上冒泡太多并不是一个好主意。在代码库中充斥大量的错误传递会增加错误不被正确处理的可能性,因此 throws 是强制性的,以确保错误的传递被代码所记录——对于程序员来说是显而易见的。

rethrows

目前你所见到的例子都是关于 throws 的,而没有它的亲兄弟 rethrows 的吗?

rethrows 告诉编译器,这个函数会抛出一个错误,同时它的参数也会抛出一个错误。下面是一个例子(不需要将它加到你的 playground 里):

func doSomethingMagical(magicalOperation: () throws -> MagicalResult) rethrows -> MagicalResult {
  return try magicalOperation()
}

这个方法只会抛出 magicalOperation 参数抛出的那个错误。如果成功,它返回一个 MagicalResult 对象。

操纵错误处理的行为

defer

尽管大部分情况下,我们让错误自动传播就可,但某些情况下,你可能想控制错误在调用堆栈中传递时 app 的行为。

defer 语句提供一种机制,让你在当前作用域结束时执行某些“清理”动作,比如方法或函数返回时。它可以清理某些资源,而无论动作是否执行成功或失败,尤其在错误处理上下文中有用。

要测试这种行为,请在 Witch 结构中加入如下方法:

func speak() {
  defer {
    print("*cackles*")
  }
  print("Hello my pretties.")
}

在 playground 底部加入代码:

func exampleThree() {
  print("") // Add an empty line in the debug area

  let witchThree = Witch(name: "Hermione", familiar: nil, hat: nil)
  witchThree.speak()
}

exampleThree()

在 Debug 控制台,你将看到女巫在每说一句话之后都会“咯咯笑”(cackles)。
有趣的是,defer 语句的执行顺序与书写顺序相反。
在 speak() 方法中添加另一个 defer 语句,这样当女巫说完一句话后,会先尖叫,然后再发出“咯咯”的笑声。

func speak() {
  defer {
    print("*cackles*")
  }

  defer {
    print("*screeches*")
  }

  print("Hello my pretties.")
}

打印顺序是否如你所想?呵,神奇的 defer!

其它和错误有关的事

总而言之,Swift 已经和其他主流语言站到了同一起跑线上,同时 Swift 也不再采用 O-C 的基于 NSError 的错误处理机制。O-C 错误很多时候是被转换过的了,由编译器中的静态分析器帮你很好地完成了注入捕捉什么样的错误和错误何时发生的工作。

尽管 do-catch 和相关特性在其他语言中有不小的开销,但在 Swift 中,它们被视作和其它语句完全相同。这使得它们保持经济和高效。

虽然你可以创建自定义错误并随意抛出它们,但不意味着你就应该那样做。在每个项目中,当你需要抛出并捕捉错误时,你都应当遵循一定的开发指南。我建议:

  • 不管在哪个代码库中,你的错误类型都需要命名清晰。
  • 在只有一种错误状态时,使用可空就可以了。
  • 在超过一种以上的错误状态是,才使用自定义错误。
  • 不要让错误从错误现场传递到太远的地方。

Swift 将来的错误处理

在各大 Swift 论坛中,有很多关于未来的错误处理的想法。其中讨论得最多的一个是无类型传递。

“…我们觉得应该在当前的处理模型中增加对无类型传递的支持,以针对通用型的错误。做好这一点,尤其是在不增加代码尺寸和性能代价的前提下,需要有足够的决心和远见。因此,这被看成是 Swift 2.0 以后的任务。”
– from Swift 2.x Error Handling

无论你是否喜欢这种观点,Swift 3 中的错误处理必将有重大改变,或者你只关心眼前的一切,你也需要知道随着这门语言的演进,那种清晰的错误处理机制正在被激烈地讨论和改进当中。

结束

你可以下载本教程完整的 playgrounds

本文的补充内容,我建议阅读下列文章,本教程也引用了其中一些内容:

如果你渴望了解 Swift 3 中将会有什么,我推荐你去看[Swift Language Proposals](Swift Language Proposals)中当前开放的提议,干嘛你不提交你自己的提议呢?
希望现在你已经体会到 Swift 错误处理的魅力。如果你有任何问题或建议,请在下面的讨论中留言!

2015-06-03 22:55:43 qq_23860911 阅读数 425

作者:Scott Gardner   译者:TurtleFromMars
原文:CALayer in iOS with Swift: 10 Examples

如你所知,我们在iOS应用中看到的都是视图(view),包括按钮视图、表视图、滑动条视图,还有可以容纳其他视图的父视图等。

但你或许不知道在iOS中支撑起每个视图的是一个叫做"图层(layer)"的类,确切地说是CALayer。

本文中您会了解CALayer及其工作原理,还有应用CALayer打造酷炫效果的十则示例,比如绘制矢量图形、渐变色,甚至是粒子系统。

本文要求读者熟悉iOS应用开发和Swift语言的基础知识,包括利用Storyboard构建用户界面。

注:如果您尚未掌握这些基础,不必担心,我们有不少相关教程,例如使用Swift语言编写iOS应用iOS学徒

准备开始

要理解图层是什么,最简便的方式就是"实地考察"。我们这就创建一个简单的项目,从头开始玩转图层。

准备好写代码了吗?好!启动Xcode,然后:

1.选择File\New\Project菜单项。

2.在对话框中选择iOS\Application\Single View Application。

3.点击Next,Product Name填写CALayerPlayground,然后输入你自己的Organization Name和Identifier。

4.Language选Swift,Devices选Universal。

5.取消选择Core Data,点击Next。

6.把项目保存到合适的位置(个人习惯把项目放在用户目录下建立的Source文件夹),点击Create。

好,文件准备就绪,接下来就是创建视图了:

7.在项目导航栏(Project navigator)中选择Main.storyboard。

8.选择View\Assistant Editor\Show Assistant Editor菜单项,如果没有显示对象库(Object Library),请选择View\Utilities\Show Object Library。

9.然后选择Editor\Canvas\Show Bounds Rectangles,这样在向场景添加视图时就可以看到轮廓了。

10.把一个视图(View)从对象库拖入视图控制器场景,保持选中状态,在尺寸检查器(View\Utilities\Show Size Inspector)中将x和y设为150,Width和Height设为300。

11.视图保持选中,点击自动布局工具栏(Storyboard右下角)的Align按钮,选中Horizontal Center in Container和Vertical Center in Container,数值均为0,然后点击Add 2 Constraints。

12.点击Pin按钮,选中Width和Height,数值均设为300,点击Add 2 Constraints。

最后按住control从刚刚创建的视图拖到ViewController.swift文件中viewDidLoad()方法的上方,在弹框中将outlet命名为viewForLayer,如图:

014.png

点击Connect创建outlet。

将ViewController.swift中的代码改写为:

import UIKit
 
class ViewController: UIViewController {
 
  @IBOutlet weak var viewForLayer: UIView!
 
  var l: CALayer {
    return viewForLayer.layer
  }
 
  override func viewDidLoad() {
    super.viewDidLoad()
    setUpLayer()
  }
 
  func setUpLayer() {
    l.backgroundColor = UIColor.blueColor().CGColor
    l.borderWidth = 100.0
    l.borderColor = UIColor.redColor().CGColor
    l.shadowOpacity = 0.7
    l.shadowRadius = 10.0
  }
 
}

之前提到iOS中的每个视图都拥有一个关联的图层,你可以通过yourView.layer访问图层。这段代码首先创建了一个叫"l"(小写L)的计算属性,方便访问viewForLayer的图层,可让你少写一些代码。

这段代码还调用了setUpLayer方法设置图层属性:阴影,蓝色背景,红色粗边框。你马上就可以了解这些东西,不过现在还是先构建App,在iOS模拟器中运行(我选了iPhone 6),看看自定义的图层如何。

015.png

几行代码,效果还不错吧?还是那句话,每个视图都由图层支撑,所以你也可以对App中的任何视图做出类似修改。我们继续深入。

CALayer基本属性

CALayer有几个属性可以用来自定外观,想想刚才做的:

  • 把图层背景色从默认的无色改为蓝色

  • 通过把边框宽度从默认的0改为100来添加边框

  • 把边框颜色从默认的黑色改为红色

  • 最后把阴影透明度从0(全透明)改为0.7,产生阴影效果,此外还把阴影半径从默认的3改为10。

以上只是CALayer中可以设置的部分属性。我们再试两个,在setUpLayer()中追加以下代码:

l.contents = UIImage(named: "star")?.CGImage
l.contentsGravity = kCAGravityCenter

CALayer的contents属性可以把图层的内容设为图片,这里我们要设置一张"星星"的图片,为此你需要把图片添加到项目中,请下载图片并添加到项目中。

构建,运行,欣赏一下效果:

016.png

注意星星居中,这是因为contentsGravity属性被设为kCAGravityCenter,如你所想,重心也可以设为上、右上、右、右下、下、左下、左、左上。

更改图层外观

仅供娱乐,我们来添加几个手势识别器来控制图层外观。在Xcode中,向viewForLayer对象上拖一个轻触手势识别器(tap gesture recognizer),见下图:

017.png

注:如果你对手势识别器比较陌生,请参阅Using UIGestureRecognizer with Swift

以此类推,再添加一个捏合手势识别器(pinch gesture recognizer)。

然后按住control依次将两个手势识别器从Storyboard场景停靠栏拖入ViewController.swift,放在setUpLayer()和类自身的闭合花括号之间。

在弹框中修改连接为Action,命名轻触识别操作为tapGestureRecognized,捏合识别操作为pinchGestureRecognized,例如:

018.png

如下改写tapGestureRecognized(_:):

@IBAction func tapGestureRecognized(sender: UITapGestureRecognizer) {
  l.shadowOpacity = l.shadowOpacity == 0.7 ? 0.0 : 0.7
}

当令视图识别出轻触手势时,代码告知viewForLayer图层在0.7和0之间切换阴影透明度。

你说视图?嗯,没错,重写CALayer的hitTest(_:)也可以实现相同效果,本文后面也会看到这个方法,不过我们这里用的方法也有道理:图层本身并不能响应手势识别,只能响应点击测试,所以我们在视图上设置了轻触手势识别器。

然后如下修改pinchGestureRecognized(_:):

@IBAction func pinchGestureRecognized(sender: UIPinchGestureRecognizer) {
  let offset: CGFloat = sender.scale < 1 ? 5.0 : -5.0
  let oldFrame = l.frame
  let oldOrigin = oldFrame.origin
  let newOrigin = CGPoint(x: oldOrigin.x + offset, y: oldOrigin.y + offset)
  let newSize = CGSize(width: oldFrame.width + (offset * -2.0), height: oldFrame.height + (offset * -2.0))
  let newFrame = CGRect(origin: newOrigin, size: newSize)
  if newFrame.width >= 100.0 && newFrame.width <= 300.0 {
    l.borderWidth -= offset
    l.cornerRadius += (offset / 2.0)
    l.frame = newFrame
  }
}

此处基于用户的捏合手势创建正负偏移值,借此调整图层框架大小、边缘宽度和边角半径。

图层的边角半径默认值为0,意即标准的90度直角。增大半径会产生圆角,如果想将图层变成圆形,可以设边角半径为宽度的一半。

注意:调整边角半径并不会裁剪图层内容(星星图片),除非图层的masksToBounds属性被设为true。

构建运行,尝试在视图中使用轻触和捏合手势:

019.png

嘿,再好好装扮一下都能当头像用了! :]

CALayer体验

CALayer中的属性和方法琳琅满目,此外还有几个包含特有属性和方法的子类。

要遍历如此酷炫的API,Raywenderlich.com导游先生最好不过了。

接下来,你需要以下材料:

该App包含十种不同的CALayer示例,本文后面会依次介绍,十分方便。先来吊吊大家的胃口:

020.png

下面在讲解每个示例的同时,我建议在CALayer演示应用中亲自动手试验,还可以读读代码。不用写,只要深呼吸,轻松阅读就可以了。 :]

我相信这些酷炫的示例会启发您利用不同的CALayer为自己的App锦上添花,希望大家喜欢!

示例 #1:CALayer

023.png

前面我们看过使用CALayer的示例,也就是设置各种属性。

关于CALayer还有几点没提:

  • 图层可以包含子图层。就像视图可以包含子视图,图层也可以有子图层,稍加利用就能打造漂亮的效果!

  • 图层属性自带动画效果。修改图层属性时,存在默认的动画效果,你也可以自定义动画行为。

  • 图层是轻量概念。相对视图而言,图层更加轻量,因此图层可以帮助提升性能。

  • 图层有大量实用属性。前面你已经看过几条了,我们继续探索!

刚刚说CALayer图层有很多属性,我们来看一批实用属性:有些属性你可能第一次见,但真的很方便!

// 1
let layer = CALayer()
layer.frame = someView.bounds
 
// 2
layer.contents = UIImage(named: "star")?.CGImage
layer.contentsGravity = kCAGravityCenter
 
// 3
layer.magnificationFilter = kCAFilterLinear
layer.geometryFlipped = false
 
// 4
layer.backgroundColor = UIColor(red: 11/255.0, green: 86/255.0, blue: 14/255.0, alpha: 1.0).CGColor
layer.opacity = 1.0
layer.hidden = false
layer.masksToBounds = false
 
// 5
layer.cornerRadius = 100.0
layer.borderWidth = 12.0
layer.borderColor = UIColor.whiteColor().CGColor
 
// 6
layer.shadowOpacity = 0.75
layer.shadowOffset = CGSize(width: 0, height: 3)
layer.shadowRadius = 3.0
someView.layer.addSublayer(layer)

在以上代码中:

  • 创建一个CALayer实例,并把框架设为someView边框。

  • 将图层内容设为一张图片,并使其在图层内居中,注意赋值的类型是底层的Quartz图像数据(CGImage)。

  • 使用过滤器,过滤器在图像利用contentsGravity放大时发挥作用,可用于改变大小(缩放、比例缩放、填充比例缩放)和位置(中心、上、右上、右等等)。以上属性的改变没有动画效果,另外如果geometryFlipped未设为true,几何位置和阴影会上下颠倒。继续:

  • 把背景色设为Ray最爱的深绿色。:] 然后让图层透明、可见。同时令图层不要遮罩内容,意思是如果图层尺寸小于内容(星星图片),图像不会被裁减。

  • 图层边角半径设为图层宽度的一半,使边缘变为圆形,注意图层颜色赋值类型为Quartz颜色引用(CGColor)。

  • 创建阴影,设shouldRasterize为true(后文还会提到),然后将图层加入视图结构树。

结果如下:

021.png

CALayer还有两个附加属性有助于改善性能:shouldRasterize和drawsAsynchronously。

shouldRasterize默认为false,设为true可以改善性能,因为图层内容只需要一次渲染。相对画面中移动但自身外观不变的对象效果拔群。

drawsAsynchronously默认值也是false。与shouldRasterize相对,该属性适用于图层内容需要反复重绘的情况,此时设成true可能会改善性能,比如需要反复绘制大量粒子的粒子发射器图层(可以参考后面的CAEmitterLayer示例)。

谨记:如果想将已有图层的shouldRasterize或drawsAsynchronously属性设为true,一定要三思而后行,考虑可能造成的影响,对比true与false的性能差异,辨明属性设置是否有积极效果。设置不当甚至会导致性能大幅下降。

无论如何还是先回到图层演示应用,其中有些控件可以用来调整CALayer的属性:

024.png

调节试试看,感受一下,利用CALayer可以实现怎样的效果。

注:图层不属于响应链(responder chain),无法像视图一样直接响应触摸和手势,我们在CALayerPlayground中见识过。不过图层有点击测试,后面的CATransformLayer会提到。你也可以向图层添加自定义动画,CAReplicatorLayer中会出现。

示例 #2:CAScrollLayer

CAScrollLayer显示一部分可滚动图层,该图层十分基础,无法直接响应用户的触摸操作,也不能直接检查可滚动图层的边界,故可避免越界无限滚动。 

UIScrollView用的不是CAScrollLayer,而是直接改动图层边界。

CAScrollLayer的滚动模式可设为水平、垂直或者二维,你也可以用代码命令视图滚动到指定位置:

// In ScrollingView.swift
import UIKit
 
class ScrollingView: UIView {
  // 1
  override class func layerClass() -> AnyClass {
    return CAScrollLayer.self
  }
}
 
// In CAScrollLayerViewController.swift
import UIKit
 
class CAScrollLayerViewController: UIViewController {
  @IBOutlet weak var scrollingView: ScrollingView!
 
  // 2
  var scrollingViewLayer: CAScrollLayer {
    return scrollingView.layer as CAScrollLayer
  }
 
  override func viewDidLoad() {
    super.viewDidLoad()
    // 3
    scrollingViewLayer.scrollMode = kCAScrollBoth
  }
 
  @IBAction func tapRecognized(sender: UITapGestureRecognizer) {
    // 4
    var newPoint = CGPoint(x: 250, y: 250)
    UIView.animateWithDuration(0.3, delay: 0, options: .CurveEaseInOut, animations: {
      [unowned self] in
      self.scrollingViewLayer.scrollToPoint(newPoint)
      }, completion: nil)
  }
 
}

以上代码:

  • 定义一个继承UIView的类,重写layerClass()返回CAScrollLayer,该方法等同于创建一个新图层作为子图层(CALayer示例中做过)。

  • 一个用以方便简化访问自定义视图滚动图层的计算属性。

  • 设滚动模式为二维滚动。

  • 识别出轻触手势时,让滚动图层在UIView动画中滚到新建的点。(注:scrollToPoint(_:)和scrollToRect(_:)不会自动使用动画效果。)

案例研究:如果ScrollingView实例包含大于滚动视图边界的图片视图,在运行上述代码并点击视图时结果如下:

025.gif

图层演示应用中有可以锁定滚动方向(水平或垂直)的开关。

以下经验规律用于决定是否使用CAScrollLayer:

  • 如果想使用轻量级的对象,只需用代码操作滚动:可以考虑CAScrollLayer。

  • 如果想让用户操作滚动,UIScrollView大概是更好的选择。要了解更多,请参考我们的视频教程

  • 如果是滚动大型图片:考虑使用CATiledLayer(见后文)。

示例 #3:CATextLayer

CATextLayer能够对普通文本或属性字串进行简单快速的渲染。与UILabel不同,CATextLayer无法指定UIFont,只能使用CTFontRef或CGFontRef。

像下面这样的代码完全可以掌控文本的字体、字体大小、颜色、对齐、折行(wrap)和截断(truncation)规则,也有动画效果:

// 1
let textLayer = CATextLayer()
textLayer.frame = someView.bounds
 
// 2
var string = ""
for _ in 1...20 {
  string += "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce auctor arcu quis velit congue dictum. "
}
 
textLayer.string = string
 
// 3
let fontName: CFStringRef = "Noteworthy-Light"
textLayer.font = CTFontCreateWithName(fontName, fontSize, nil)
 
// 4
textLayer.foregroundColor = UIColor.darkGrayColor().CGColor
textLayer.wrapped = true
textLayer.alignmentMode = kCAAlignmentLeft
textLayer.contentsScale = UIScreen.mainScreen().scale
someView.layer.addSublayer(textLayer)

以上代码解释如下:

  • 创建一个CATextLayer实例,令边界与someView相同。

  • 重复一段文本,创建字符串并赋给文本图层。

  • 创建一个字体,赋给文本图层。

  • 将文本图层设为折行、左对齐,你也可以设自然对齐(natural)、右对齐(right)、居中对齐(center)或两端对齐(justified),按屏幕设置contentsScale属性,然后把图层添加到视图结构树。

不仅是CATextLayer,所有图层类的渲染缩放系数都默认为1。在添加到视图时,图层自身的contentsScale缩放系数会自动调整,适应当前画面。你需要为手动创建的图层明确指定contentsScale属性,否则默认的缩放系数1会在Retina显示屏上产生部分模糊。

如果创建的文本图层添加到了方形的someView,效果会像这样:

026.png

你可以设置截断(Truncation)属性,生效时被截断的部分文本会由省略号代替显示。默认设定为无截断,位置可设为开头、末尾或中间截断:

027.png

图层演示应用中,你可以随心所欲地修改很多CATextLayer属性:

028.png

示例 #4:AVPlayerLayer

AVPlayerLayer是建立在AVFoundation基础上的实用图层,持有一个AVPlayer,用来播放音视频媒体文件(AVPlayerItems),举例如下:

override func viewDidLoad() {
  super.viewDidLoad()
  // 1
  let playerLayer = AVPlayerLayer()
  playerLayer.frame = someView.bounds
 
  // 2
  let url = NSBundle.mainBundle().URLForResource("someVideo", withExtension: "m4v")
  let player = AVPlayer(URL: url)
 
  // 3
  player.actionAtItemEnd = .None
  playerLayer.player = player
  someView.layer.addSublayer(playerLayer)
 
  // 4
  NSNotificationCenter.defaultCenter().addObserver(self, selector: "playerDidReachEndNotificationHandler:", name: "AVPlayerItemDidPlayToEndTimeNotification", object: player.currentItem)
}
 
deinit {
  NSNotificationCenter.defaultCenter().removeObserver(self)
}
 
// 5
@IBAction func playButtonTapped(sender: UIButton) {
  if playButton.titleLabel?.text == "Play" {
    player.play()
    playButton.setTitle("Pause", forState: .Normal)
  } else {
    player.pause()
    playButton.setTitle("Play", forState: .Normal)
  }
 
  updatePlayButtonTitle()
  updateRateSegmentedControl()
}
 
// 6
func playerDidReachEndNotificationHandler(notification: NSNotification) {
  let playerItem = notification.object as AVPlayerItem
  playerItem.seekToTime(kCMTimeZero)
}

上述代码解释:

  1. 新建一个播放器图层,设置框架。

  2. 使用AV asset资源创建一个播放器。

  3. 告知命令播放器在播放完成后停止。其他选项还有暂停或自动播放下一个媒体资源。

  4. 注册AVPlayer通知,在一个文件播放完毕后发送通知,并在析构函数中删除作为观察者的控制器。

  5. 点击播放按钮时,触发控件播放AV asset并设置按钮文字。

注意这只是个入门示例,在实际项目中往往不会采用文字按钮控制播放。

AVPlayerLayer和其中创建的AVPlayer会像这样显示为AVPlayerItem实例的第一帧:

029.png

AVPlayerLayer还有一些附加属性:

  • videoGravity设置视频显示的缩放行为。

  • readyForDisplay检测是否准备好播放视频。

另一方面,AVPlayer也有不少附加属性和方法,有一个值得注意的是rate属性,对于0到1之间的播放速率,0代表暂停,1代表常速播放(1x)。

不过rate属性的设置是与播放行为联动的,也就是说调用pause()方法和把rate设为0是等价的,调用play()与把rate设为1也一样。

那快进、慢动作和反向播放呢?交给AVPlayerLayer把。rate大于1时会令播放器以相应倍速进行播放,例如rate设为2就是二倍速。

如你所想,rate为负时会让播放器以相应倍速反向播放。

然而,在以非常规速率播放之前,AVPlayerItem上会调用适当方法,验证是否能够以相应速率进行播放:

  • canPlayFastForward()对应大于1

  • canPlaySlowForward()对应0到1之间

  • canPlayReverse()对应-1

  • canPlaySlowReverse()对应-1到0之间

  • canPlayFastReverse()对应小于-1

绝大多数视频都支持以不同速率正向播放,可以反向播放的视频相对少一些。演示应用也包含了播放控件:

030.png

示例 #5:CAGradientLayer

CAGradientLayer简化了混合两种或更多颜色的工作,尤其适用于背景。要配置渐变色,你需要分配一个CGColor数组,以及标识渐变图层起止点的startPoint和endPoint。

注意:startPoint和endPoint并不是明确的点,而是用单位坐标空间定义,在绘制时映射到图层边界。也就是说x值为1表示点在图层右边缘,y值为1表示点在图层下边缘。

CAGradientLayer包含type属性,虽说该属性只有kCAGradientLayerAxial一个选择,由数组中的各颜色产生线性过渡渐变。

具体含义是渐变过渡沿startPoint到endPoint的向量A方向产生,设B与A垂直,则各条B平行线上的所有点颜色相同。

031.gif

此外,locations属性可以使用一个数组(元素取值范围0到1),指定渐变图层参照colors顺序取用下一个过渡点颜色的位置。

未设定时默认会平均分配过渡点。一旦设定就必须与colors的数量保持一致,否则会出错。 :[

下面是创建渐变图层的例子:

let gradientLayer = CAGradientLayer()
gradientLayer.frame = someView.bounds
gradientLayer.colors = [cgColorForRed(209.0, green: 0.0, blue: 0.0),
  cgColorForRed(255.0, green: 102.0, blue: 34.0),
  cgColorForRed(255.0, green: 218.0, blue: 33.0),
  cgColorForRed(51.0, green: 221.0, blue: 0.0),
  cgColorForRed(17.0, green: 51.0, blue: 204.0),
  cgColorForRed(34.0, green: 0.0, blue: 102.0),
  cgColorForRed(51.0, green: 0.0, blue: 68.0)]
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 0, y: 1)
someView.layer.addSublayer(gradientLayer)
 
func cgColorForRed(red: CGFloat, green: CGFloat, blue: CGFloat) -> AnyObject {
  return UIColor(red: red/255.0, green: green/255.0, blue: blue/255.0, alpha: 1.0).CGColor as AnyObject
}

上述代码创建一个渐变图层,框架设为someView边界,指定颜色数组,设置起止点,添加图层到视图结构树。效果如下:

032.png

五彩缤纷,姹紫嫣红! 

图层演示应用中,你可以随意修改起止点、颜色和过渡点:

033.png

示例 #6:CAReplicatorLayer

CAReplicatorLayer能够以特定次数复制图层,可以用来创建一些很棒的效果。

每个图层复件的颜色和位置都可以改动,而且可以在总复制图层之后延迟绘制,营造一种动画效果。还可以利用深度,创造三维效果。举个例子

// 1
let replicatorLayer = CAReplicatorLayer()
replicatorLayer.frame = someView.bounds
 
// 2
replicatorLayer.instanceCount = 30
replicatorLayer.instanceDelay = CFTimeInterval(1 / 30.0)
replicatorLayer.preservesDepth = false
replicatorLayer.instanceColor = UIColor.whiteColor().CGColor
 
// 3
replicatorLayer.instanceRedOffset = 0.0
replicatorLayer.instanceGreenOffset = -0.5
replicatorLayer.instanceBlueOffset = -0.5
replicatorLayer.instanceAlphaOffset = 0.0
 
// 4
let angle = Float(M_PI * 2.0) / 30
replicatorLayer.instanceTransform = CATransform3DMakeRotation(CGFloat(angle), 0.0, 0.0, 1.0)
someView.layer.addSublayer(replicatorLayer)
 
// 5
let instanceLayer = CALayer()
let layerWidth: CGFloat = 10.0
let midX = CGRectGetMidX(someView.bounds) - layerWidth / 2.0
instanceLayer.frame = CGRect(x: midX, y: 0.0, width: layerWidth, height: layerWidth * 3.0)
instanceLayer.backgroundColor = UIColor.whiteColor().CGColor
replicatorLayer.addSublayer(instanceLayer)
 
// 6
let fadeAnimation = CABasicAnimation(keyPath: "opacity")
fadeAnimation.fromValue = 1.0
fadeAnimation.toValue = 0.0
fadeAnimation.duration = 1
fadeAnimation.repeatCount = Float(Int.max)
 
// 7
instanceLayer.opacity = 0.0
instanceLayer.addAnimation(fadeAnimation, forKey: "FadeAnimation")

以上代码:

  • 创建一个CAReplicatorLayer实例,设框架为someView边界。

  • 设复制图层数instanceCount和绘制延迟,设图层为2D(preservesDepth = false),实例颜色为白色。

  • 为陆续的实例复件设置RGB颜色偏差值(默认为0,即所有复件保持颜色不变),不过这里实例初始颜色为白色,即RGB都为1.0,所以偏差值设红色为0,绿色和蓝色为相同负数会使其逐渐现出红色,alpha透明度偏差值的变化也与此类似,针对陆续的实例复件。

  • 创建旋转变换,使得实例复件按一个圆排列。

  • 创建供复制图层使用的实例图层,设置框架,使第一个实例在someView边界顶端水平中心处绘制,另外设置实例颜色,把实例图层添加到复制图层。

  • 创建一个透明度由1(不透明)过渡为0(透明)的淡出动画。

  • 设实例图层透明度为0,使得每个实例在绘制和改变颜色与alpha前保持透明。

这段代码会实现这样的东西:

034.gif

图层演示应用中,你可以改动这些属性:

035.png

示例 #7:CATiledLayer

CATiledLayer以图块(tile)为单位异步绘制图层内容,对超大尺寸图片或者只能在视图中显示一小部分的内容效果拔群,因为不用把内容完全载入内存就可以看到内容。

处理绘制有几种方法,一种是重写UIView,使用CATiledLayer绘制图块填充视图背景,如下:

// In ViewController.swift
import UIKit
 
class ViewController: UIViewController {
 
  // 1
  @IBOutlet weak var tiledBackgroundView: TiledBackgroundView!
 
}
 
// In TiledBackgroundView.swift
import UIKit
 
class TiledBackgroundView: UIView {
 
  let sideLength = CGFloat(50.0)
 
  // 2
  override class func layerClass() -> AnyClass {
    return CATiledLayer.self
  }
 
  // 3
  required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    srand48(Int(NSDate().timeIntervalSince1970))
    let layer = self.layer as CATiledLayer
    let scale = UIScreen.mainScreen().scale
    layer.contentsScale = scale
    layer.tileSize = CGSize(width: sideLength * scale, height: sideLength * scale)
  }
 
  // 4
  override func drawRect(rect: CGRect) {
    let context = UIGraphicsGetCurrentContext()
    var red = CGFloat(drand48())
    var green = CGFloat(drand48())
    var blue = CGFloat(drand48())
    CGContextSetRGBFillColor(context, red, green, blue, 1.0)
    CGContextFillRect(context, rect)
  }
 
}

代码解释:

  • tiledBackgroundView位于 (150, 150) ,宽高均为300。

  • 重写layerClass(),令该视图创建的图层实例为CATiledLayer。

  • 设置rand48()的随机数种子,用于在drawRect()中生成随机颜色。CATiledLayer类型转换,缩放图层内容,设置图块尺寸,适应屏幕。

  • 重写drawRect(),以随机色块填充视图。

代码绘制6×6随机色块方格,最终效果如下:

036.png

图层演示应用中除此之外还可以在图层背景上绘制轨迹:

037.png

在视图中放大时,上述截图中的星星图案会变得模糊:

038.png

产生模糊的根源是图层的细节层次(level of detail,简称LOD),CATiledLayer有两个相关属性:levelsOfDetail和levelsOfDetailBias。

levelsOfDetail顾名思义,指图层维护的LOD数目,默认值为1,每进一级会对前一级分辨率的一半进行缓存,图层的levelsOfDetail最大值,也就是最底层细节,对应至少一个像素点。

而levelsOfDetailBias指的是该图层缓存的放大LOD数目,默认为0,即不会额外缓存放大层次,每进一级会对前一级两倍分辨率进行缓存。

例如,设上述分块图层的levelsOfDetailBias为5会缓存2x、4x、8x、16x和32x的放大层次,放大的图层效果如下:

039.png

不错吧?别着急,还没讲完呢。

CATiledLayer裁刀,买不了吃亏,买不了上当,只要998…(译注:此处内容稍作本地化处理,原文玩的是1978年美国Ginsu刀具的梗,堪称询价型电视购物广告的万恶之源。) :]

开个玩笑。CATiledLayer还有一个更实用的功能:异步绘制图块,比如在滚动视图中显示一张超大图片。

在用户滚动画面时,要让分块图层知道哪些图块需要绘制,写代码在所难免,不过换来性能提升也值了。

图层演示应用的UIImage+TileCutter.swift中包含一个UIImage扩展,教程编纂组成员Nick Lockwood在著作iOS Core Animation: Advanced Techniques的一个终端应用程序中利用了这段代码。

代码的职责是把原图片拆分成指定尺寸的方块,按行列位置命名图块,比如第三行第七列的图块windingRoad62.png(索引从零开始)。

041.png

有了这些图块,我们可以自定义一个UIView子类,绘制分块图层:

import UIKit
 
class TilingViewForImage: UIView {
 
  // 1
  let sideLength = CGFloat(640.0)
  let fileName = "windingRoad"
  let cachesPath = NSSearchPathForDirectoriesInDomains(.CachesDirectory, .UserDomainMask, true)[0] as String
 
  // 2
  override class func layerClass() -> AnyClass {
    return CATiledLayer.self
  }
 
  // 3
  required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    let layer = self.layer as CATiledLayer
    layer.tileSize = CGSize(width: sideLength, height: sideLength)
  }
 
  // 4
  override func drawRect(rect: CGRect) {
    let firstColumn = Int(CGRectGetMinX(rect) / sideLength)
    let lastColumn = Int(CGRectGetMaxX(rect) / sideLength)
    let firstRow = Int(CGRectGetMinY(rect) / sideLength)
    let lastRow = Int(CGRectGetMaxY(rect) / sideLength)
 
    for row in firstRow...lastRow {
      for column in firstColumn...lastColumn {
        if let tile = imageForTileAtColumn(column, row: row) {
          let x = sideLength * CGFloat(column)
          let y = sideLength * CGFloat(row)
          let point = CGPoint(x: x, y: y)
          let size = CGSize(width: sideLength, height: sideLength)
          var tileRect = CGRect(origin: point, size: size)
          tileRect = CGRectIntersection(bounds, tileRect)
          tile.drawInRect(tileRect)
        }
      }
    }
  }
 
  func imageForTileAtColumn(column: Int, row: Int) -> UIImage? {
    let filePath = "\(cachesPath)/\(fileName)_\(column)_\(row)"
    return UIImage(contentsOfFile: filePath)
  }
 
}

以上代码:

  • 创建属性,分别是图块边长、原图文件名、供TileCutter扩展保存图块的缓存文件夹路径。

  • 重写layerClass()返回CATiledLayer。

  • 实现init(_:),把视图的图层转换为分块图层,设置图块大小。注意此处不必设置contentsScale适配屏幕,因为是直接修改视图自身的图层,而不是手动创建子图层。

  • 重写drawRect(),按行列绘制各个图块。

像这样,原图大小的自定义视图就可以塞进一个滚动视图:

042.png

多亏CATiledLayer,滚动5120 x 3200的大图也会这般顺滑:

043.gif

如你所见,快速滚动时绘制图块的过程还是很明显,你可以利用更小的分块(上述例子中分块为640 x 640),或者自己创建一个CATiledLayer子类,重写fadeDuration()返回0:

class TiledLayer: CATiledLayer {
 
  override class func fadeDuration() -> CFTimeInterval {
    return 0.0
  }
 
}

示例 #8:CAShapeLayer

CAShapeLayer利用可缩放的矢量路径进行绘制,绘制速度比使用图片快很多,还有个好处是不用分别提供常规、@2x和@3x版本的图片,好用。

另外还有各种属性,让你可以自定线粗、颜色、虚实、线条接合方式、闭合线条是否形成闭合区域,还有闭合区域要填充何种颜色等。举例如下:

import UIKit
 
class ViewController: UIViewController {
 
  @IBOutlet weak var someView: UIView!
 
  // 1
  let rwColor = UIColor(red: 11/255.0, green: 86/255.0, blue: 14/255.0, alpha: 1.0)
  let rwPath = UIBezierPath()
  let rwLayer = CAShapeLayer()
 
  // 2
  func setUpRWPath() {
    rwPath.moveToPoint(CGPointMake(0.22, 124.79))
    rwPath.addLineToPoint(CGPointMake(0.22, 249.57))
    rwPath.addLineToPoint(CGPointMake(124.89, 249.57))
    rwPath.addLineToPoint(CGPointMake(249.57, 249.57))
    rwPath.addLineToPoint(CGPointMake(249.57, 143.79))
    rwPath.addCurveToPoint(CGPointMake(249.37, 38.25), controlPoint1: CGPointMake(249.57, 85.64), controlPoint2: CGPointMake(249.47, 38.15))
    rwPath.addCurveToPoint(CGPointMake(206.47, 112.47), controlPoint1: CGPointMake(249.27, 38.35), controlPoint2: CGPointMake(229.94, 71.76))
    rwPath.addCurveToPoint(CGPointMake(163.46, 186.84), controlPoint1: CGPointMake(182.99, 153.19), controlPoint2: CGPointMake(163.61, 186.65))
    rwPath.addCurveToPoint(CGPointMake(146.17, 156.99), controlPoint1: CGPointMake(163.27, 187.03), controlPoint2: CGPointMake(155.48, 173.59))
    rwPath.addCurveToPoint(CGPointMake(128.79, 127.08), controlPoint1: CGPointMake(136.82, 140.43), controlPoint2: CGPointMake(129.03, 126.94))
    rwPath.addCurveToPoint(CGPointMake(109.31, 157.77), controlPoint1: CGPointMake(128.59, 127.18), controlPoint2: CGPointMake(119.83, 141.01))
    rwPath.addCurveToPoint(CGPointMake(89.83, 187.86), controlPoint1: CGPointMake(98.79, 174.52), controlPoint2: CGPointMake(90.02, 188.06))
    rwPath.addCurveToPoint(CGPointMake(56.52, 108.28), controlPoint1: CGPointMake(89.24, 187.23), controlPoint2: CGPointMake(56.56, 109.11))
    rwPath.addCurveToPoint(CGPointMake(64.02, 102.25), controlPoint1: CGPointMake(56.47, 107.75), controlPoint2: CGPointMake(59.24, 105.56))
    rwPath.addCurveToPoint(CGPointMake(101.42, 67.57), controlPoint1: CGPointMake(81.99, 89.78), controlPoint2: CGPointMake(93.92, 78.72))
    rwPath.addCurveToPoint(CGPointMake(108.38, 30.65), controlPoint1: CGPointMake(110.28, 54.47), controlPoint2: CGPointMake(113.01, 39.96))
    rwPath.addCurveToPoint(CGPointMake(10.35, 0.41), controlPoint1: CGPointMake(99.66, 13.17), controlPoint2: CGPointMake(64.11, 2.16))
    rwPath.addLineToPoint(CGPointMake(0.22, 0.07))
    rwPath.addLineToPoint(CGPointMake(0.22, 124.79))
    rwPath.closePath()
  }
 
  // 3
  func setUpRWLayer() {
    rwLayer.path = rwPath.CGPath
    rwLayer.fillColor = rwColor.CGColor
    rwLayer.fillRule = kCAFillRuleNonZero
    rwLayer.lineCap = kCALineCapButt
    rwLayer.lineDashPattern = nil
    rwLayer.lineDashPhase = 0.0
    rwLayer.lineJoin = kCALineJoinMiter
    rwLayer.lineWidth = 1.0
    rwLayer.miterLimit = 10.0
    rwLayer.strokeColor = rwColor.CGColor
  }
 
  override func viewDidLoad() {
    super.viewDidLoad()
 
    // 4
    setUpRWPath()
    setUpRWLayer()
    someView.layer.addSublayer(rwLayer)
  }
 
}

代码解释:

  • 创建颜色、路径、图形图层对象。

  • 绘制图形图层路径。如果不喜欢编写生硬的绘图代码的话,你可以尝试PaintCode这款软件,可以利用简便的工具进行可视化绘制,支持导入现有的矢量图(SVG)和Photoshop(PSD)文件,并自动生成代码。

  • 设置图形图层。路径设为第二步中绘制的CGPath路径,填充色设为第一步中创建的CGColor颜色,填充规则设为非零(non-zero),即默认填充规则。

  • 填充规则共有两种,另一种是奇偶(even-odd)。不过示例代码中的图形没有相交路径,两种填充规则的结果并无差异。

  • 非零规则记从左到右的路径为+1,从右到左的路径为-1,累加所有路径值,若总和大于零,则填充路径围成的图形。

  • 从结果上来讲,非零规则会填充图形内部所有的点。

  • 奇偶规则计算围成图形的路径交叉数,若结果为奇数则填充。这样讲有些晦涩,还是有图有真相:

右图围成中间五边形的路径交叉数为偶数,故中间没有填充,而围成每个三角的路径交叉数为奇数,故三角部分填充颜色。

CAShapeLayerFillRules-480x256.png

  • 调用路径绘制和图层设置代码,并把图层添加到视图结构树。

上述代码绘制raywenderlich.com的图标:

RayWenderlichLogo-250x250.png

顺便看看使用PaintCode的效果图:

PaintCodeRayWenderlichLogo-480x265.png

图层演示应用中,你可以随意修改很多CAShapeLayer属性:

CAShapeLayer_5.5.png

注:我们先跳过演示应用中的下一个示例,因为CAEAGLLayer多少显得有些过时了,iOS 8 Metal框架有更先进的CAMetalLayer。在此推荐iOS 8 Metal入门教程

示例 #9:CATransformLayer

CATransformLayer不像其他图层类一样把子图层结构平面化,故适宜绘制3D结构。变换图层本质上是一个图层容器,每个子图层都可以应用自己的透明度和空间变换,而其他渲染图层属性(如边宽、颜色)会被忽略。

变换图层本身不支持点击测试,因为无法直接在触摸点和平面坐标空间建立映射,不过其中的子图层可以响应点击测试,例如:

import UIKit
 
class ViewController: UIViewController {
 
  @IBOutlet weak var someView: UIView!
 
  // 1
  let sideLength = CGFloat(160.0)
  var redColor = UIColor.redColor()
  var orangeColor = UIColor.orangeColor()
  var yellowColor = UIColor.yellowColor()
  var greenColor = UIColor.greenColor()
  var blueColor = UIColor.blueColor()
  var purpleColor = UIColor.purpleColor()
  var transformLayer = CATransformLayer()
 
  // 2
  func setUpTransformLayer() {
    var layer = sideLayerWithColor(redColor)
    transformLayer.addSublayer(layer)
 
    layer = sideLayerWithColor(orangeColor)
    var transform = CATransform3DMakeTranslation(sideLength / 2.0, 0.0, sideLength / -2.0)
    transform = CATransform3DRotate(transform, degreesToRadians(90.0), 0.0, 1.0, 0.0)
    layer.transform = transform
    transformLayer.addSublayer(layer)
 
    layer = sideLayerWithColor(yellowColor)
    layer.transform = CATransform3DMakeTranslation(0.0, 0.0, -sideLength)
    transformLayer.addSublayer(layer)
 
    layer = sideLayerWithColor(greenColor)
    transform = CATransform3DMakeTranslation(sideLength / -2.0, 0.0, sideLength / -2.0)
    transform = CATransform3DRotate(transform, degreesToRadians(90.0), 0.0, 1.0, 0.0)
    layer.transform = transform
    transformLayer.addSublayer(layer)
 
    layer = sideLayerWithColor(blueColor)
    transform = CATransform3DMakeTranslation(0.0, sideLength / -2.0, sideLength / -2.0)
    transform = CATransform3DRotate(transform, degreesToRadians(90.0), 1.0, 0.0, 0.0)
    layer.transform = transform
    transformLayer.addSublayer(layer)
 
    layer = sideLayerWithColor(purpleColor)
    transform = CATransform3DMakeTranslation(0.0, sideLength / 2.0, sideLength / -2.0)
    transform = CATransform3DRotate(transform, degreesToRadians(90.0), 1.0, 0.0, 0.0)
    layer.transform = transform
    transformLayer.addSublayer(layer)
 
    transformLayer.anchorPointZ = sideLength / -2.0
    applyRotationForXOffset(16.0, yOffset: 16.0)
  }
 
  // 3
  func sideLayerWithColor(color: UIColor) -> CALayer {
    let layer = CALayer()
    layer.frame = CGRect(origin: CGPointZero, size: CGSize(width: sideLength, height: sideLength))
    layer.position = CGPoint(x: CGRectGetMidX(someView.bounds), y: CGRectGetMidY(someView.bounds))
    layer.backgroundColor = color.CGColor
    return layer
  }
 
  func degreesToRadians(degrees: Double) -> CGFloat {
    return CGFloat(degrees * M_PI / 180.0)
  }
 
  // 4
  func applyRotationForXOffset(xOffset: Double, yOffset: Double) {
    let totalOffset = sqrt(xOffset * xOffset + yOffset * yOffset)
    let totalRotation = CGFloat(totalOffset * M_PI / 180.0)
    let xRotationalFactor = CGFloat(totalOffset) / totalRotation
    let yRotationalFactor = CGFloat(totalOffset) / totalRotation
    let currentTransform = CATransform3DTranslate(transformLayer.sublayerTransform, 0.0, 0.0, 0.0)
    let rotationTransform = CATransform3DRotate(transformLayer.sublayerTransform, totalRotation,
      xRotationalFactor * currentTransform.m12 - yRotationalFactor * currentTransform.m11,
      xRotationalFactor * currentTransform.m22 - yRotationalFactor * currentTransform.m21,
      xRotationalFactor * currentTransform.m32 - yRotationalFactor * currentTransform.m31)
    transformLayer.sublayerTransform = rotationTransform
  }
 
  // 5
  override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
    if let location = touches.anyObject()?.locationInView(someView) {
      for layer in transformLayer.sublayers {
        if let hitLayer = layer.hitTest(location) {
          println("Transform layer tapped!")
          break
        }
      }
    }
  }
 
  override func viewDidLoad() {
    super.viewDidLoad()
 
    // 6
    setUpTransformLayer()
    someView.layer.addSublayer(transformLayer)
  }
 
}

上述代码解释:

  • 创建属性,分别为立方体的边长、每个面的颜色,还有一个变换图层。

  • 创建六个面,旋转后添加到变换图层,构成立方体,然后设置变换图层的z轴锚点,旋转立方体,将其添加到视图结构树。

  • 辅助代码,用来创建指定颜色的面,还有角度和弧度的转换。在变换代码中利用弧度转换函数在某种程度上可以增加代码可读性。 :]

  • 基于指定xy偏移的旋转,注意变换应用对象设为sublayerTransform,即变换图层的子图层。

  • 监听触摸,遍历变换图层的子图层,对每个图层进行点击测试,一旦成功相应立即跳出循环,不用继续遍历。

  • 设置变换图层,添加到视图结构树。

注:currentTransform.m##是啥?问得好,是CATransform3D属性,代表矩阵元素。想学习如上代码中的矩阵变换,请参考RW教程组成员Rich Turton的三维变换娱乐教学,还有Mark Pospesel初识矩阵项目

在250 x 250的someView视图中运行上述代码结果如下:

CATransformLayer-250x250.png

再试试点击立方体的任意位置,控制台会输出“Transform layer tapped!”信息。

图层演示应用中可以调整透明度,此外Bill Dudney轨迹球工具, Swift移植版可以基于简单的用户手势应用三维变换。

CATransformLayer_5.5.png

示例 #10:CAEmitterLayer

CAEmitterLayer渲染的动画粒子是CAEmitterCell实例。CAEmitterLayer和CAEmitterCell都包含可调整渲染频率、大小、形状、颜色、速率以及生命周期的属性。示例如下:

import UIKit
 
class ViewController: UIViewController {
 
  // 1
  let emitterLayer = CAEmitterLayer()
  let emitterCell = CAEmitterCell()
 
  // 2
  func setUpEmitterLayer() {
    emitterLayer.frame = view.bounds
    emitterLayer.seed = UInt32(NSDate().timeIntervalSince1970)
    emitterLayer.renderMode = kCAEmitterLayerAdditive
    emitterLayer.drawsAsynchronously = true
    setEmitterPosition()
  }
 
  // 3
  func setUpEmitterCell() {
    emitterCell.contents = UIImage(named: "smallStar")?.CGImage
 
    emitterCell.velocity = 50.0
    emitterCell.velocityRange = 500.0
 
    emitterCell.color = UIColor.blackColor().CGColor
    emitterCell.redRange = 1.0
    emitterCell.greenRange = 1.0
    emitterCell.blueRange = 1.0
    emitterCell.alphaRange = 0.0
    emitterCell.redSpeed = 0.0
    emitterCell.greenSpeed = 0.0
    emitterCell.blueSpeed = 0.0
    emitterCell.alphaSpeed = -0.5
 
    let zeroDegreesInRadians = degreesToRadians(0.0)
    emitterCell.spin = degreesToRadians(130.0)
    emitterCell.spinRange = zeroDegreesInRadians
    emitterCell.emissionRange = degreesToRadians(360.0)
 
    emitterCell.lifetime = 1.0
    emitterCell.birthRate = 250.0
    emitterCell.xAcceleration = -800.0
    emitterCell.yAcceleration = 1000.0
  }
 
  // 4
  func setEmitterPosition() {
    emitterLayer.emitterPosition = CGPoint(x: CGRectGetMidX(view.bounds), y: CGRectGetMidY(view.bounds))
  }
 
  func degreesToRadians(degrees: Double) -> CGFloat {
    return CGFloat(degrees * M_PI / 180.0)
  }
 
  override func viewDidLoad() {
    super.viewDidLoad()
 
    // 5
    setUpEmitterLayer()
    setUpEmitterCell()
    emitterLayer.emitterCells = [emitterCell]
    view.layer.addSublayer(emitterLayer)
  }
 
  // 6
  override func traitCollectionDidChange(previousTraitCollection: UITraitCollection?) {
    setEmitterPosition()
  }
 
}

以上代码解析:

1.创建粒子发射器图层和粒子胞(Creates an emitter layer and cell.)。

2.按照下方步骤设置粒子发射器图层:

  • 为随机数生成器提供种子,随机调整粒子胞的某些属性,如速度。

  • 在图层背景色和边界之上按renderMode指定的顺序渲染粒子胞。

注:渲染模式默认为无序(unordered),其他模式包括旧粒子优先(oldest first),新粒子优先(oldest last),按z轴位置从后至前(back to front)还有叠加式渲染(additive)。

  • 由于粒子发射器需要反复重绘大量粒子胞,设drawsAsynchronously为true会提升性能。

  • 然后借助第四条中会提到的辅助方法设置发射器位置,这个例子有助于理解把drawsAsynchronously设为true为何能够提升性能和动画流畅度。

3.这段代码设了不少东西。

  • 配置粒子胞,设内容为图片(图片在图层演示项目中)。

  • 指定初速及其变化量范围(velocityRange),发射器图层利用上面提到的随机数种子创建随机数生成器,在范围内产生随机值(初值+/-变化量范围),其他以“Range”结尾的相关属性的随机化规则类似。

  • 设颜色为黑色,使自变色(variance)与默认的白色形成对比,白色形成的粒子亮度过高。

  • 利用随机化范围设置颜色,指定自变色范围,颜色速度值表示粒子胞生命周期内颜色变化快慢。

  • 接下来这几行代码指定粒子胞分布范围,一个全圆锥。设置粒子胞转速和发射范围,发射范围emissionRange属性的弧度值决定粒子胞分布空间。

  • 设粒子胞生命周期为1秒,默认值为0,表示粒子胞不会出现。birthRate也类似,以秒为单位,默认值为0,为使粒子胞显示出来,必须设成正数。

  • 最后设xy加速度,这些值会影响已发射粒子的视角。

4.把角度转换成弧度的辅助方法,还有设置粒子胞位置为视图中点。

5.设置发射器图层和粒子胞,把粒子胞添加到图层,然后把图层添加到视图结构树。

6.iOS 8的新方法,处理当前设备形态集(trait collection)的变化,比如设备旋转。不熟悉形态集的话可以参阅iOS 8教程。 

总算说完了!信息量很大,但相信各位聪明的读者可以高效吸收。

上述代码运行效果如下:

CAEmitterLayer2.gif

图层演示应用中,你可以随意调节很多属性:

CAEmitterLayer_5.5.png

 

何去何从?

victorious-e1419192669236.png

恭喜,看完十则示例和各种图层子类,CALayer之旅至此告一段落。

但现在才刚刚开始!新建一个项目,或者打开已有项目,尝试利用图层提升性能或营造酷炫效果!实践出真知。

2016-06-30 18:00:32 shylone1024 阅读数 657

初探swift~~

以下原文出自:www.hangge.com

应网友要求,我这里总结了下 as、as!、as? 这三种类型转换操作符的异同,以及各自的使用场景。

1,as使用场合
(1)从派生类转换为基类,向上转型(upcasts)
class Animal {}
class Cat: Animal {}
let cat = Cat()
let animal = cat as Animal

(2)消除二义性,数值类型转换
let num1 = 42 as CGFloat
let num2 = 42 as Int
let num3 = 42.5 as Int
let num4 = (42 / 2) as Double

(3)switch 语句中进行模式匹配
如果不知道一个对象是什么类型,你可以通过switch语法检测它的类型,并且尝试在不同的情况下使 用对应的类型进行相应的处理。
switch animal {
case let cat as Cat:
print(“如果是Cat类型对象,则做相应处理”)
case let dog as Dog:
print(“如果是Dog类型对象,则做相应处理”)
default: break
}

2,as!使用场合
向下转型(Downcasting)时使用。由于是强制类型转换,如果转换失败会报 runtime 运行错误。

class Animal {}
class Cat: Animal {}
let animal :Animal = Cat()
let cat = animal as! Cat

3,as?使用场合
as? 和 as! 操作符的转换规则完全一样。但 as? 如果转换不成功的时候便会返回一个 nil 对象。成功的话返回可选类型值(optional),需要我们拆包使用。
由于 as? 在转换失败的时候也不会出现错误,所以对于如果能确保100%会成功的转换则可使用 as!,否则使用 as?

let animal:Animal = Cat()
if let cat = animal as? Cat{
print(“cat is not nil”)
} else {
print(“cat is nil”)
}

以下原文出处未知,也不管是不是有用,先看看,发现问题再更正
4,?、!使用
Swift语言使用var定义变量,但和别的语言不同,Swift里不会自动给变量赋初始值,也就是说变量不会有默认值,所以要求使用变量之前必须要对其初始化。如果在使用变量之前不进行初始化就会报错:
var stringValue : String
//error: variable ‘stringValue’ used before being initialized
//let hashValue = stringValue.hashValue
// ^
let hashValue = stringValue.hashValue

上面了解到的是普通值,接下来Optional值要上场了。经喵神提醒,Optional其实是个enum,里面有None和Some两种类型。其实所谓的nil就是Optional.None, 非nil就是Optional.Some, 然后会通过Some(T)包装(wrap)原始值,这也是为什么在使用Optional的时候要拆包(从enum里取出来原始值)的原因, 也是PlayGround会把Optional值显示为类似{Some “hello world”}的原因,这里是enum Optional的定义:

enum Optional<T> : LogicValue, Reflectable {
case None
case Some(T)
init()
init(_ some: T)

/// Allow use in a Boolean context.
func getLogicValue() -> Bool

/// Haskell's fmap, which was mis-named
func map<</span>U>(f: (T) -> U) -> U?
func getMirror() -> Mirror

}

声明为Optional只需要在类型后面紧跟一个?即可。如:

var strValue: String? //?相当于下面这种写法的语法糖
var strValue: Optional<String>

上面这个Optional的声明,意思不是”我声明了一个Optional的String值”, 而是”我声明了一个Optional类型值,它可能包含一个String值,也可能什么都不包含”,也就是说实际上我们声明的是Optional类型,而不是声明了一个String类型,这一点需要铭记在心。
建议再读一遍上段文字。
一旦声明为Optional的,如果不显式的赋值就会有个默认值nil。判断一个Optional的值是否有值,可以用if来判断:

if strValue {
//do sth with strValue
}

然后怎么使用Optional值呢?文档中也有提到说,在使用Optional值的时候需要在具体的操作,比如调用方法、属性、下标索引等前面需要加上一个?,如果是nil值,也就是Optional.None,会跳过后面的操作不执行,如果有值,就是Optional.Some,可能就会拆包(unwrap),然后对拆包后的值执行后面的操作,来保证执行这个操作的安全性,比如:

let hashValue = strValue?.hashValue

strValue是Optional的字符串,如果strValue是nil,则hashValue也为nil,如果strValue不为nil,hashValue就是strValue字符串的哈希值(其实也是用Optional wrap后的值)

另外,?还可以用在安全地调用protocol类型方法上,比如:

@objc protocol Downloadable {
@optional func download(toPath: String) -> Bool;
}

@objc class Content: Downloadable {
//download method not be implemented
}

var delegate: Downloadable = Downloadable()
delegate.download?(“some path”)

因为上面的delegate是Downloadable类型的,它的download方法是optional,所以它的具体实现有没有download方法是不确定的。Swift提供了一种在参数括号前加上一个?的方式来安全地调用protocol的optional方法。

另外如果你需要像下面这样向下转型(Downcast),可能会用到 as?:

if let dataSource = object as? UITableViewDataSource {
let rowsInFirstSection = dataSource.tableView(tableView, numberOfRowsInSection: 0)
}

到这里我们看到了?的几种使用场景:
1.声明Optional值变量
2.用在对Optional值操作中,用来判断是否能响应后面的操作
3.用于安全调用protocol的optional方法
4.使用 as? 向下转型(Downcast)
另外,对于Optional值,不能直接进行操作,否则会报错:

//error: ‘String?’ does not have a member named ‘hashValue’
//let hashValue = strValue.hashValue
// ^ ~~~~~

let hashValue = strValue.hashValue

上面提到Optional值需要拆包(unwrap)后才能得到原来值,然后才能对其操作,那怎么来拆包呢?拆包提到了几种方法,一种是Optional Binding, 比如:

if let str = strValue {
let hashValue = str.hashValue
}

还有一种是在具体的操作前添加!符号,好吧,这又是什么诡异的语法?!

直接上例子,strValue是Optional的String:

let hashValue = strValue!.hashValue

这里的!表示“我确定这里的的strValue一定是非nil的,尽情调用吧” ,比如这种情况:

if strValue {
let hashValue = strValue!.hashValue
}

{}里的strValue一定是非nil的,所以就能直接加上!,强制拆包(unwrap)并执行后面的操作。 当然如果不加判断,strValue不小心为nil的话,就会出错,crash掉。

考虑下这一种情况,我们有一个自定义的MyViewController类,类中有一个属性是myLabel,myLabel是在viewDidLoad中进行初始化。因为是在viewDidLoad中初始化,所以不能直接声明为普通值:var myLabel : UILabel,因为非Optional的变量必须在声明时或者构造器中进行初始化,但我们是想在viewDidLoad中初始化,所以就只能声明为Optional:var myLabel: UILabel?, 虽然我们确定在viewDidLoad中会初始化,并且在ViewController的生命周期内不会置为nil,但是在对myLabel操作时,每次依然要加上!来强制拆包(在读取值的时候,也可以用?,谢谢iPresent在回复中提醒),比如:

myLabel!.text = “text”
myLabel!.frame = CGRectMake(0, 0, 10, 10)

对于这种类型的值,我们可以直接这么声明:var myLabel: UILabel!, 果然是高(hao)大(gui)上(yi)的语法!, 这种是特殊的Optional,称为Implicitly Unwrapped Optionals, 直译就是隐式拆包的Optional,就等于说你每次对这种类型的值操作时,都会自动在操作前补上一个!进行拆包,然后在执行后面的操作,当然如果该值是nil,也一样会报错crash掉。

var myLabel: UILabel! //!相当于下面这种写法的语法糖
var myLabel: ImplicitlyUnwrappedOptional

那么!大概也有两种使用场景
1.强制对Optional值进行拆包(unwrap)
2.声明Implicitly Unwrapped Optionals值,一般用于类中的属性

2016-04-11 00:54:07 cg1991130 阅读数 11692

本人原创,长文慎入,但此文可能会改写你的编程风格。我认为数据和模型交互的关键问题是如何处理数据源和视图源本身的异构性。通过面向协议编程的不断实践,总结他人的理论经验,我发现了使用两个极简的通用协议可以完美破解异构的问题,也就是本文想要介绍的MV架构。在最初版的版本中我想把这个架构命名为MVP(Model-View-Protocol),因为本文浏览的人比较多,这个命名容易和现有的MVP(Model-View-Presenter)造成混淆,但其实二者有着天壤之别,由于在本架构中Protocol部分的语法非常简短精炼,数据的处理分发给了Model和View本身,所以我觉得P可以去掉了,这个架构就叫MV,如幽灵般鬼魅。下面是原文内容:
WWDC2015已经过去一段时间了,我发现自从更新了Swift2.0到现在的Swift2.2,我只是跟着版本更新了所有需要更新的语法,依旧自以为是很熟练的Swift程序员。刚入职比较闲碰巧看到了1月份的中国首届Swift大会上大牛们的分享,突然陷入了思考,有了很多新想法又重温了几遍WWDC2015大会的视频,尤其是408和414号视频!!!我下定决心重构自己的代码,下面步入正题,结合Swift开发大会的一些分享,让我们谈谈架构。
通过一个简单的Demo:一个事件提醒的小应用。
这个应用会使用一个TableView混合展示一个时间段的所有待办事宜和这个时间段的节日提醒,由于待办事件和节日的数据构成是不同的,所以需要创建两个模型,它们在TableView上展示的样式也应该有所不同,很常规的我们还需要一个TableViewCell的子类。
现在数据工程里面的目录是这样的:
这里写图片描述

模型代码:

struct Event {
    var date = ""
    var eventTitle = ""
    init(date:String,title:String){
        self.date = date
        self.eventTitle = title
    }
}

struct Festival {
    var date = ""
    var festivalName = ""
    init(date:String,name:String){
        self.date = date
        self.festivalName = name
    }
}

为了简单我都使用了String类型的数据,至于为什么要使用struct而不使用class,大家可以参考WWDC2015的414号视频,讲的非常清楚,我自己的项目中的数据模型已经全部转成struct了,我会在后面专门写博文讲解struct,这里就不赘述了。这里需要啰嗦一下,注意创建的时候使用的是字面量的方法,而不是可选型,我一直认为使用字面量的方法是更好的选择,可选型很容易被当做语法糖滥用。尤其是数据的初始化中,你确定你真的需要一个空值?拿一个空值能做什么?做某种标志位么?请和你的后台开发人员商议,让他给你一个Bool类型的标志位,而不是一个空值。在可能的情况下,给你的数据模型的属性赋一个语义明确的字面量初始值,比如这里我们使用空字符串作为初始值。如果你的数据只是做展示的不会存在修改情况,你也可以使用如下的方法做初始化,以达到效率的最大化:

struct Event {
    let date:String
    let eventTitle:String
    init(date:String = "",eventTitle:String = ""){
        self.date = date
        self.eventTitle = eventTitle
    }
}

在Swift1.2版本之后,let定义的数据也支持延迟加载了,这里使用了默认参数值做非空的保障。
模型否则在创建一个实例的时候各种可选型的解包或可选绑定会让你吃尽苦头,空值的访问是程序carsh的元凶!
如果如果你更新了Xcode7.3,你会发现在创建一个属性的时候Xcode的提示是“ =“,没错,Xcode推荐你用字面量去做初始化。
有了数据模型后,在Cell上创建两个Label

class ShowedTableViewCell: UITableViewCell {
    //用来展示事件主题或节日名称的Label
    @IBOutlet weak var MixLabel: UILabel!
    //用来展示日期的Label
    @IBOutlet weak var dateLabel: UILabel!


}

MVC架构:
从这里我们将展示传统的MVC的写法,但是包含了一些关键的知识点,所以还是建议您不要跳过。我们通过控制器中的代码去控制数据的展示,由于数据源包含两种数据类型,可以构造两个数组避免数组的异构:

    var eventList = [Event]()
    var festivalList = [Festival]()
    let loadedEventList = [Event(date: "2月14", eventTitle: "送礼物")]
    let loadedFestivalList = [Festival(date: "1月1日", festivalName: "元旦"),Festival(date: "2月14", festivalName: "情人节")]

这里使用了struct的默认构造器构造对象,有两个节日提醒:元旦节和情人节,元旦节没什么事情做,情人节那天有个事件提醒”送礼物“,我们使用GCD去模拟数据刷新,整个控制器的代码如下:

import UIKit

let cellReusedID = "ShowedTableViewCell"
class ShowedTableViewController: UITableViewController {

    var eventList = [Event]()
    var festivalList = [Festival]()
    let loadedEventList = [Event(date: "2月14", eventTitle: "送礼物")]
    let loadedFestivalList = [Festival(date: "1月1日", festivalName: "元旦"),Festival(date: "2月14", festivalName: "情人节")]
    override func viewDidLoad() {
        super.viewDidLoad()
        let delayInSeconds = 2.0
        let popTime = dispatch_time(DISPATCH_TIME_NOW,
            Int64(delayInSeconds * Double(NSEC_PER_SEC)))
        dispatch_after(popTime, dispatch_get_main_queue()) { () -> Void in
            self.eventList = self.loadedEventList
            self.festivalList = self.loadedFestivalList
            self.tableView.reloadData()
        }
    }



    // MARK: - Table view data source

    override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        // #warning Incomplete implementation, return the number of sections
        return 1
    }

    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // #warning Incomplete implementation, return the number of rows
        return eventList.count + festivalList.count
    }

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier(cellReusedID, forIndexPath: indexPath) as! ShowedTableViewCell
        //传统的MVC,你需要在这里处理数据本身的同构与异构情况,还得处理数据与视图的逻辑关系
        //这里我们把事件提醒放在节日的前面展示
        if indexPath.row > eventList.count - 1{
            cell.MixLabel.text = festivalList[indexPath.row - eventList.count].festivalName
            cell.dateLabel.text = festivalList[indexPath.row - eventList.count].date
            cell.backgroundColor = UIColor.whiteColor()
            return cell
        } else {
            cell.MixLabel.text = eventList[indexPath.row].eventTitle
            cell.dateLabel.text = eventList[indexPath.row].date
            cell.backgroundColor = UIColor.redColor()
            return cell
        }
    }


}

运行一下看看:
这里写图片描述

似乎还不错,我们把两个不同的数据结构展现在一张页面上了,并且复用了cell,但是设置cell的代理方法中的代码似乎有点多,而且如果我需要按照时间去排序,那么两个同构的数组作为数据源不好排序,那么重构首先从把同构变成异构开始。由于struct没有继承,按照Swift2.0的精神,此时我们需要提炼两个数据模型的共性,方法是利用protocol,观察到Event和Festival都有date属性,所以写一个协议:

protocol HasDate{
    var date:String {get}
}

这里这个协议只有一个属性date,Swift协议中定义的属性只有声明,遵守协议的对象必须实现这个属性,但是不限于存储属性还是计算属性。协议中定义的属性必须指定最低的访问级别,这里的date必须是可读的,至于可写的权限取决于实现该协议的数据类型中的定义。由于我们的Event和Festival都具有了date属性,直接让二者遵守HasDate协议,不要用扩展的方式让二者遵守协议,编译器报错的,很怪0 0.
修改并化简控制器中的数据源,使用异构数据源,现在控制器的代码如下:

import UIKit

let cellReusedID = "ShowedTableViewCell"
class ShowedTableViewController: UITableViewController {

    var dataList = [HasDate]()
    var loadeddataList:[HasDate] = [Event(date: "2月14", eventTitle: "送礼物"),Festival(date: "1月1日", festivalName: "元旦"),Festival(date: "2月14", festivalName: "情人节")]
    override func viewDidLoad() {
        super.viewDidLoad()
        let delayInSeconds = 2.0
        let popTime = dispatch_time(DISPATCH_TIME_NOW,
            Int64(delayInSeconds * Double(NSEC_PER_SEC)))
        dispatch_after(popTime, dispatch_get_main_queue()) { () -> Void in
            //注意这里,我故意把loadeddataList中的数据打乱了,为了实现异构数据的按照某个公共类型的属性的排序,使用了Swift内置的sort函数,String遵守了Compareable协议,这里为了简单吧date指定为String类型,如果是NSDate,你可以在sort的闭包中指定合适的排序规则。
            self.dataList = self.loadeddataList.sort{$0.date < $1.date}
            self.tableView.reloadData()
        }
    }



    // MARK: - Table view data source

    override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dataList.count
    }

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier(cellReusedID, forIndexPath: indexPath) as! ShowedTableViewCell
        //注意这里,通过可选绑定进行异构数据的类型控制
        if let event = dataList[indexPath.row] as? Event{
            cell.MixLabel.text = event.eventTitle
            cell.dateLabel.text = event.date
            cell.backgroundColor = UIColor.redColor()
            return cell
        } else if let festival = dataList[indexPath.row] as? Festival{
            cell.MixLabel.text = festival.festivalName
            cell.dateLabel.text = festival.date
            cell.backgroundColor = UIColor.whiteColor()
            return cell
        } else {
            return cell
        }
    }
}

运行一下:
这里写图片描述
没有任何问题。对异构数组的类型判断的写法来自于WWDC2015上的408号视频,现在控制器里的代码已经精简了很多了,我们解决了异构的问题,对于MVC来说,这似乎已经精简到极限了。这是一个简单的Demo,在真正的工程中一个控制器当中的代码可能有几百上千行,或者有多个TableView,这个时候MVC的弊端就显现了,在几百行代码中可能有一百行都用来做数据与视图的绑定,而数据模型和视图本身的代码定义中却只有寥寥数十行,控制器的负担太重了!因此有人提出了将控制器中有关模型与视图的逻辑的代码提出到一个单独的区域进行处理,这就是MVVM架构的由来。
MVVM架构
对MVVM架构的解读我想引用Swift开发者大会上李信洁前辈的示例写法,通过POP来实现一个MVVM,并且对其写法进行了一些精简。我们先不修改View和Modal的代码,因为需要更新的是一个cell,所以首先需要写一个传递Modal中数据的协议:

protocol CellPresentable{
    var mixLabelData:String {get set}
    var dateLabelData:String {get set}
    var color: UIColor {get set}
    func updateCell(cell:ShowedTableViewCell)
}

这个协议的思想是显示地声明一个更新cell的方法,并根据cell需要的数据声明两个属性,我们并不关心mixLabel和dateLabel的数据从哪里来,叫什么名字,但他们的功能是确定的,Swift2.0之后可以扩展协议,下面通过协议扩展给这个协议增加默认的实现,这样在绑定数据时可以减少代码量:

extension CellPresentable{
    func updateCell(cell:ShowedTableViewCell){
        cell.MixLabel.text = mixLabelData
        cell.dateLabel.text = dateLabelData
        cell.backgroundColor = color
    }
}

好了,我们写好了,下一步我们要修改cell的代码,增加一个方法接受一个CellPresentable:

class ShowedTableViewCell: UITableViewCell {
    //用来展示事件主题或节日名称的Label
    @IBOutlet weak var MixLabel: UILabel!
    //用来展示日期的Label
    @IBOutlet weak var dateLabel: UILabel!

    func updateWithPresenter(presenter: CellPresentable) {
        presenter.updateCell(self)
    }
}

这里也做了一些改进,李信洁前辈的示例中是针对每一个控件去定义方法的,其实对一个View的所有IBOutlet做更新不就是更新它自己么,所以这里我的写法是直接传入self。然后(我也不想多说然后,但是步骤就是这么多)为了绑定异构的Model和View你还需要定义一个ViewModel,并且通过定义不同的init实现数据绑定:

struct ViewModel:CellPresentable{
    var dateLabelData = ""
    var mixLabelData = ""
    var color = UIColor.whiteColor()
    init(modal:Event){
        self.dateLabelData = modal.date
        self.mixLabelData = modal.eventTitle
        self.color = UIColor.redColor()
    }
    init(modal:Festival){
        self.dateLabelData = modal.date
        self.mixLabelData = modal.festivalName
        self.color = UIColor.whiteColor()
    }
}

最后我们终于可以去修改我们的控制器了,控制器中需要更改的是与cell有关的datasource方法:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier(cellReusedID, forIndexPath: indexPath) as! ShowedTableViewCell
        if let event = dataList[indexPath.row] as? Event{
            let viewModel = ViewModel(modal: event)
            cell.updateWithPresenter(viewModel)
            return cell
        } else if let festival = dataList[indexPath.row] as? Festival{
            let viewModel = ViewModel(modal: festival)
            cell.updateWithPresenter(viewModel)
            return cell
        } else {
            return cell

        }
    }
}

这段代码写的我满头大汗,编译运行,幸运的是运行的结果是正确的:
这里写图片描述

我在想MVVM模式的意义是什么?我在使用MVVM之前甚至需要考虑一下值不值得花时间去写成MVVM的模样,因为MVVM需要给所有的view提供协议,并且将所有的数据模型的绑定过程写进一个新的数据结构ViewModal中,但其实这个ViewModel的价值非常之小,除了数据绑定,没有其他作用了,里面甚至只有空洞的init构造器,我想我已经决定放弃这个思路了。
MV的萌芽阶段
我继续着自己的思考,大会上傅若愚前辈分享的示例给了我很大的启发,因为他提供了一个没有中间层的模型!我一直在思考这个模型,并且在入职的第一个项目中一直在按照他的模型来组织自己的代码,直到我顿悟了自己的MV模型。下面简单介绍一下傅若愚前辈的思路,这个思路的优势在于所有的数据和模型绑定都只需要两个通用的协议:

//视图使用的协议
protocol RenderContext{
    func renderText(texts:String...)
    func renderImage(images:UIImage...)
}
//数据使用的协议
protocol ViewModelType{
    func renderInContext(context:RenderContext)
}

上面是大会上傅若愚前辈的原版,在介绍这个协议的用法之前,我觉得应该先做一点点改进,ViewModalType应该改成:

protocol ViewModelType{
    func renderInContext<R:RenderContext>(context:R)
}

这两个版本都可以通过编译,差别在运行的效率上,下面我在playground中展示一个示例,这个示例来源于《Advanced Swift》这本书,其实苹果的WWDC2015 408号视频中也明确表述了不要把协议当做参数类型,而写成泛型的约束,但是没有详细讲解为什么,下面是示例:

func takesProtocol(x: CustomStringConvertible) { //
    print ( sizeofValue(x))
}
func takesPlaceholder<T: CustomStringConvertible>(x: T) {
    print ( sizeofValue(x))
}

两个方法,前者使用协议作为参数的类型,后者使用协议作为泛型的约束条件,两个方法都会打印参数的长度,调用一下试试:

takesProtocol(1 as Int16)
takesPlaceholder(1 as Int16)

打印结果:
这里写图片描述
换成类再打印一次:
这里写图片描述
没错,由于协议本身既可以被类遵守、也可以被结构体、枚举遵守,也就是说既可以被引用类型遵守也可以被值类型遵守,把协议当做参数类型,实际上会创造一个Box类型,里面会为引用类型遵守者预留地址也会为值类型遵守者预留地址,甚至需要存储一个指针长度找到协议的真正继承类型。而Swift2.0之后编译器得到了加强,具有了泛型特化的功能,对代码中的泛型在编译时就会确定其真正的类型,不耗费任何性能。
下面我们用改造后的傅若愚前辈的协议来改造Demo,你需要让你的数据模型去遵守RenderContext,然后根据模型的参数类型将每一个参数存入对应类型方法的参数列表中,这些方法都是可变参数,不限制数量,但是参数的类型是确定的。这种使用参数类型做通用类型的写法消灭了中间的ViewModel层,把Model和View直接对接了。由于Swift要求每一个协议的遵守者都必须实现协议的全部方法,而有些方法的数据模型并没有,所以你在使用之前需要使用协议扩展为这些方法实现一个空的实现:

protocol RenderContext{
    func renderText(texts:String...)
    func renderImage(images:UIImage...)
}

extension RenderContext{
    func renderText(texts:String...){

    }
    func renderImage(images:UIImage...){

    }
}

现在你的模型应该是下面这样:

struct Event:HasDate,ViewModelType{
    var date = ""
    var eventTitle = ""
    func renderInContext<R : RenderContext>(context: R) {
        context.renderText(date,eventTitle)
    }
}

struct Festival:HasDate,ViewModelType{
    var date = ""
    var festivalName = ""
    func renderInContext<R : RenderContext>(context: R) {
        context.renderText(date,festivalName)
    }
}

视图的代码应该是这样的:

class ShowedTableViewCell: UITableViewCell,RenderContext {
    //用来展示事件主题或节日名称的Label
    @IBOutlet weak var MixLabel: UILabel!
    //用来展示日期的Label
    @IBOutlet weak var dateLabel: UILabel!

    func renderText(texts: String...) {
        dateLabel.text = texts[0]
        MixLabel.text = texts[1]
    }
}

由于遵守了多个协议,所以控制器中原本的异构类型不合适了,此时可以给多个协议类型写一个别名方便使用,记得顺便更新一下你的Model,提高可读性:

typealias DateViewModel = protocol<HasDate,ViewModelType>

现在控制器中的数据源可以使用新的异构类型了:

var dataList = [DateViewModel]()
    var loadeddataList:[DateViewModel] = [Event(date: "2月14", eventTitle: "送礼物"),Festival(date: "1月1日", festivalName: "元旦"),Festival(date: "2月14", festivalName: "情人节")]

然后更新cell的代理方法:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier(cellReusedID, forIndexPath: indexPath) as! ShowedTableViewCell
        dataList[indexPath.row].renderInContext(cell)
        return cell
    }

不错,代码简洁了很多,运行一下:
这里写图片描述
等等,我们似乎遗漏了一些东西,cell的背景颜色呢?好吧让我们加上,可是我该去哪里加呢?去控制器中吗?不不坚决不能碰控制器,那么只能去cell中了,现在问题出现了,当两个模型共享一个视图的时候,我该如何判断数据源从哪里来?renderText(texts: String…)这样的写法已经完全失去了异构的特性,那么试着这样写,在数据传递参数的时候多传一个String好了,反正参数是我们的自由:

struct Event:DateViewModel{
    var date = ""
    var eventTitle = ""
    func renderInContext<R : RenderContext>(context: R) {
        context.renderText(date,eventTitle,"red")
    }
}

这样在检验的时候就看最后一个参数就好了:

class ShowedTableViewCell: UITableViewCell,RenderContext {
    //用来展示事件主题或节日名称的Label
    @IBOutlet weak var MixLabel: UILabel!
    //用来展示日期的Label
    @IBOutlet weak var dateLabel: UILabel!

    func renderText(texts: String...) {
        dateLabel.text = texts[0]
        MixLabel.text = texts[1]
        if texts[2] == "red"{
            backgroundColor = UIColor.redColor()
        }
    }
}

这里有个语法糖,可变参数的方法,在取参时不会发生越界,因为Festival的renderText方法只传了两个值,运行结果又正常了。那么如果我粗心把参数写错顺序了呢?结果成了这样:
这里写图片描述
如果我的Festival中多了一个Int类型,而Event中恰巧没有呢?按照值去区分参数不是一个好主意,因为你用下标从一个数组中取值的时候除了它的类型不能得到任何信息,甚至都不知道这个值存不存在!我再次陷入了思考,既然View需要的是Model中的属性,这不就等于需要Model自己么,那么为什么我们不能直接传递Modal自己呢?
MV!
所以我再次改造了傅若愚前辈的协议,顺便把名字改的好辨认一点,原来的名字太容易出错了- -现在它是这样子的:

//视图使用的协议
protocol ViewType{
    func getData<M:ModelType>(model:M)
}
//数据使用的协议
protocol ModelType{
    func giveData<V:ViewType>(view:V)
}

不需要在扩展中写默认实现,因为传值是相互且确定的,所以方法一定会被实现。
模型是这样子的:

typealias DateViewModel = protocol<HasDate,ModelType>
struct Festival:DateViewModel{
    var date = ""
    var festivalName = ""
    func giveData<V : ViewType>(view: V) {
        view.getData(self)
    }
}

struct Event:DateViewModel{
    var date = ""
    var eventTitle = ""
    func giveData<V : ViewType>(view: V) {
        view.getData(self)
    }
}

视图:

class ShowedTableViewCell: UITableViewCell,ViewType {
    //用来展示事件主题或节日名称的Label
    @IBOutlet weak var MixLabel: UILabel!
    //用来展示日期的Label
    @IBOutlet weak var dateLabel: UILabel!

    func getData<M : ModelType>(model: M) {
        //这里不能写成guard let dateModel = model as? DateViewModel else{}令我有些意外
        guard let dateModel = model as? HasDate else{
            //不满足Cell基本需求的Model直接淘汰掉
            return
        }
        //处理相同属性
        dateLabel.text = dateModel.date
        //处理数据源异构
        if let event = dateModel as? Event{
            MixLabel.text = event.eventTitle
            backgroundColor = UIColor.redColor()
        } else if let festival = dateModel as? Festival{
            MixLabel.text = festival.festivalName
            backgroundColor = UIColor.whiteColor()
        }
    }
}

再次用苹果官方给出的异构判断方法解决异构,协议不同于类,没有那么多继承上的检查,所以使用as?是很高效的,最后只要给控制器中的代码换个名字就够了:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier(cellReusedID, forIndexPath: indexPath) as! ShowedTableViewCell
        dataList[indexPath.row].giveData(cell)
        return cell
    }

完成,运行效果:
这里写图片描述
你会发现即便你删掉你的控制器代码,View和Model中的逻辑也不会发生改变,而在控制器中进行数据绑定的时候,因为使用了协议来实现数据源的异构,你甚至不需要对数据源做筛选,只要它们是使用相同的cell来做展示。
等等,还没完!即便这个架构已经非常精简了,但是你也许发现了在Model中我们写了很多重复的代码,也就是giveData的实现部分,这个实现是完全相同的,而且我们不希望这个方法被复习,所以,最后的优化,把giveData的定义和实现放在ModelType的协议扩展中:

//视图使用的协议
protocol ViewType{
    func getData<M:ModelType>(model:M)
}
//数据使用的协议
protocol ModelType{
}
//ModelType的默认实现
extension ModelType{
    func giveData<V:ViewType>(view:V){
        view.getData(self)
    }
}

现在你可以删掉Event和Festival中的giveData的实现了,Model、View、Controller和Protocol的代码都变得极其简单,依靠Swift强大的语言特性,让数据的传递与异构的处理似乎看不见摸不着,却又真实地发生了,幽灵般鬼魅。寥寥数行代码解决了MVC和MVVM争论多年的问题,运行一下,享受幽灵架构MV吧!
在项目的最初阶段,开发人员拿到的是原型和设计图,即便我们不清楚该如何开始编写复杂的处理逻辑,但是数据的状态与视图的样式的对应关系是大致确定的,因此可以直接使用MV架构绑定数据源和视图的逻辑,即便后台开发者提供的最终接口里有数据的改变,那么在我们修改对应的Model的时候,View中的代码也会以报错的形式提示你修改,开发的效率会得到一个显著的提升!
代码打包了一份,放这里了:http://pan.baidu.com/s/1qYTAs3M
有需要的自取
写在后面:
博主欠了欠身子,从吃完晚饭写到了半夜,一口气完成了本文,如果你喜欢我的文章并且得到了启发,欢迎转载传阅,注明出处即可。在Swift1.X时代我觉得Swift脆弱的像只小猫,Swift2.0之后我才突然发现苹果缔造的是一只野兽。苹果很聪明,在推进Swift替代OC的道路上采取了温柔的手段:仅仅是让Swift变得更优秀。通过不断锻炼自己面向协议编程的能力,我有了很多新的体会,想起了迪杰斯特拉老爷子著名的goto有害论,请准许我大胆预言一下,在面向协议的世界中AnyObject也是有害的,点到为止。

关于博主本人:
《Swift开发手册:技巧与实战》作者。国内计算机领域的某名校毕业,学习不差,初入社会,曾只身离校北漂妄图以退学抗议畸形的研究生教育,后心疼父母返校完成学业。从2014年底开始接触Swift后一发不可收拾,至今保持狂热,小人物大梦想,孜孜不倦致力于改善iOS编程体验。欢迎大家留言交流,力所能及之处,必倾囊相授。

swift相机启动

阅读数 263