iOS,Swift,Sprite Kit

Swift 的函数式编程

Swift 相比原先的 Objective-C 最重要的优点之一,就是对函数式编程提供了更好的支持。 Swift 提供了更多的语法和一些新特性来增强函数式编程的能力,本文就在这方面进行一些讨论。

Swift 概览

对编程语言有了一些经验的程序员,尤其是那些对多种不同类型的编程语言都有经验的开发者, 在学习新的语言的时候更加得心应手。原因在于编程语言本身也是有各种范式的, 把握住这些特点就可以比较容易的上手了。

在入手一门新的语言的时候,一般关注的内容有:

  1. 原生数据结构
  2. 运算符
  3. 分支控制
  4. 如果是面向对象的编程语言,其面向对象的实现是怎样的
  5. 如果是函数式编程语言,其面向函数式编程的实现是怎样的

通过这几个点,其实只要阅读 Swift 文档的第一章,你就可以对这个语言有一个大概的印象。 比如对于数据结构,Swift 和其他的编程语言大体一样,有 Int, Float, Array, Dictionary 等, 运算符也基本与 C 语言一致等。 本文主要集中于对 Swift 函数式编程方面的特点进行一些盘点,因此在这里假设大家对 Swift 的基本语法已经有所了解。

对于一种编程范式,要掌握它也要抓住一些要点。对于支持函数式编程的语言,其一般的特点可能包含以下几种:

  1. 支持递归
  2. 函数本身是语言 First Class 的组成要素,且支持高阶函数和闭包
  3. 函数调用尽可能没有副作用 (Side Effect) 的条件

接下来我们来逐个盘点这些内容。

递归

Swift 是支持递归的,事实上现在不支持递归的编程语言已经很难找到了。在 Swift 里写一个递归调用和其他编程语言并没有什么区别:

func fib(n: Int) -> Int {
  if n <= 1 {
    return 1
  }
  else {
    return fib(n-1) + fib(n-2)
  }
}
fib(6) // output 13

关于 Swift 的递归没有什么好说的。作为一个常识,我们知道递归是需要消耗栈空间的。 在函数式编程语言中,递归是一个非常常用的方法,然而使用不慎很容易导致栈溢出的问题。 如果将代码改写为非递归实现,又可能会导致代码的可读性变差,因此有一个技巧是使用“尾递归”, 然后让编译器来优化代码。

一个 Common Lisp 的尾递归的例子是

(defun fib(n)
    (fib-iter 1 0 n))

(defun fib-iter(a b count)
    (if (= count 0)
        b
        (fib-iter (+ a b) a (- count 1))))

我们可以把我们上述的 Swift 代码也改写成相同形式

func fibiter(a: Int, b: Int, count: Int) -> Int {
  if count==0 {
    return b
  }
  else {
    return fibiter(a + b, a, count-1)
  }
}

func fib(n: Int) -> Int {
  return fibiter(1, 1, n);
}

我们可以 Playground 里观察是否使用尾递归时的迭代结果变化。

值得注意的是,这里出现了一个 Swift 的问题。虽然 Swift 支持嵌套函数,但是当我们将fibiter 作为一个高阶函数包含在fib函数之内的时候却发生了 EXC_BAD_ACCESS 报错, 并不清楚这是语言限制还是 Bug。

Swift 的高阶函数和闭包

在 Objective-C 时代,使用 block 来实现高阶函数或者闭包已经是非常成熟的技术了。 Swift 相比 Objective-C 的提高在于为函数式编程添加了诸多语法上的方便。 首先是高阶函数的支持,可以在函数内定义函数,下面就是一个很简洁的例子。

func greetingGenerator(object:String) -> (greeting:String) -> String {
  func sayGreeting(greeting:String) -> String {
    return greeting + ", " + object
  }
  return sayGreeting
}

let sayToWorld = greetingGenerator("world")
sayToWorld(greeting: "Hello") // "Hello, World"
sayToWorld(greeting: " 你好 ") // " 你好, World"

如果使用 block 实现上述功能,可读性就不会有这么好。而且 block 的语法本身也比较怪异, 之前没少被人吐槽。Swift 从这个角度来看比较方便。事实上,在 Swift 里可以将函数当做对象赋值, 这和很多函数式编程语言是一样的。 作为一盘大杂烩,Swift 的函数系统也很有 JavaScript 的影子在里面。比如可以向下面这样定义函数:

let add = {
  (a:Int, b:Int) -> Int in
  return a+b
}

add(1, 2) // 3

等号之后被赋予变量

add的是一个闭包表达式,因此更准确的说, 这是将一个闭包赋值给常量了。注意在闭包表达式中,in关键字之前是闭包的形式定义,之后是具体代码实现。 Swift 中的闭包跟匿名函数没有什么区别。 如果你将它赋值给对象,就跟 JavaScript 中相同的实践是一样的了。幸好 Swift 作为 C 系列的语言, 其分支语句 if 等本身是有作用域的,因此不会出现下列 JavaScript 的坑:

if (someNum>0) {
  function a(){ alert("one") };
}
else {
  function a(){ alert("two") };
}

a() // will always alert "two" in most of browsers

Swift 的闭包表达式和函数都可以作为函数的参数,从下面的代码我们可以看出闭包和函数的一致性:

func function() {
  println("this is a function")
}

let closure = {
  () -> () in
  println("this is a closure")
}

func run(somethingCanRun:()-> ()) {
  somethingCanRun()
}

run(function)
run(closure)

类似于 Ruby,Swift 作为函数参数的闭包做了一点语法糖。 在 Ruby 中使用 Block 的时候,我们可以这样写:

(1...5).map {|x| x*2} // => [2, 4, 6, 8]

在 Swift 当中我们可以得到几乎一样的表达式。

var a = Array(1..5).map {x in x*2}
// a = [2, 4, 6, 8]

也就是说, 如果一个函数的最后一个参数是闭包,那么它在语法上可以放在函数调用的外面。 闭包还可以用

$0$1等分别来表示第 0、第 1 个参数等。 基本的运算符也可以看做函数。 下面的几种方式都可以实现逆序倒排的功能。

let thingsToSort = Array(1..5)
var reversed1 = sort(thingsToSort) { a, b in a

总体来说,Swift 在添加方便函数操作、添加相关语法糖方面走的很远,基本上整合了目前各种语言中比较方便的特性。 实用性较好。

Side Effects

在计算机科学中,函数副作用指当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响。例如修改全局变量 (函数外的变量) 或修改参数 (wiki)。 函数副作用会给程序带来一些不必要的麻烦。

为了减少函数副作用,很多函数式编程语言都力求达到所谓的“纯函数”。 纯函数是指函数与外界交换数据的唯一渠道是参数和返回值, 而不会受到函数的外部变量的干扰。 乍看起来这似乎跟闭包的概念相抵触,因为闭包本身的一个重要特点就是可以访问到函数定义时的上下文环境。

事实上,为了在这种情况下支持纯函数,一些编程语言如 Clojure 等提供的数据结构都是不可变 (或者说 Persist) 的。 因此其实也就没有我们传统意义上的所认为的“变量”的概念。比如说,在 Python 中,字符串str就是一类不可变的数据结构。 你不能在原来的字符串上进行修改,每次想要进行类似的操作,其实都是生成了一个新的str对象。 然而 Python 中的链表结构则是可变的。且看下面的代码,在 Python 中对a字符串进行修改并不会影响b, 但是同样的操作作用于链表就会产生不一样的结果:

a = "hello, "
b = a
a += "world"
print a # hello, world
print b # hello,

Swift 的数据结构的 Persist 性质跟 Python 有点类似。需要注意的是,Swift 有变量和常量两种概念, 变量使用

var声明,常量使用let声明,使用var声明的时候,Swift 中的字符串的行为跟 Python 相似, 因此修改字符串可以被理解为生成了一个新的字符串并修改了指针。同样, 使用var声明的数组和字典也都是可变的。

在 Swift 中使用let声明的对象不能被赋值,基本数据结果也会变得不可变,但是情况更复杂一点。

let aDict = ["k1":"v1"]
let anArray = [1, 2, 3, 4]

aDict["k1"] = "newVal" // !! will fail !!
anArray.append(5) // !! will fail !!
anArray[0] = 5 // anArray = [5, 2, 3, 4] now !

从上面的代码中可以看出,使用

let声明的字典是完全不可变的,但是数组虽然不可以改变长度, 却可以改变数组元素的值!Swift 的文档中指出这里其实是将 Array 理解为定长数组从而方便编译优化, 来获得更好的访问性能。

综上所述,对象是否可变的关系其实略有复杂的,可以总结为:

  1. 使用varletIntString类型都是不可变的,但是var时可以对变量重新赋值
  2. 使用let声明的常量不可以被重新赋值
  3. 使用let声明的Dictionary是完全不可变的
  4. 使用let声明的Array长度不可变,但是可以修改元素的值
  5. 使用let声明的类对象是可变的 综上所述,即使是使用

let声明的对象也有可能可变,因此在多线程情况下就无法达到“无副作用”的要求了。

此外 Swift 的函数虽然没有指针,但是仍通过参数来修改变量的。只要在函数的参数定义中加入inout关键字即可。 这个特性很有 C 的风格。

个人觉得在支持通过元组来实现多返回值的情况下,这个特性不但显得鸡肋,也是一个导致程序产生“副作用”的特性。 Swift 支持这样的特性,恐怕更多的是为了兼容 Objective-C 以及方便在两个语言之间搭建 Bridge。

func inc(inout a:Int) {
  a += 1
}
var num = 1
inc(&num) // num = 2 now!

综上所述,使用 Swift 自带的数据结构并不能很好的实现“无副作用”的“纯函数式”编程, 它并没有比 Python、Ruby 这类语言走的更远。幸好作为一种关注度很高的语言, 已经有开发者为其实现了一套完全满足不可变要求的数据结构和库:

Swiftz。 坚持使用let和 Swiftz 提供的数据结构来操作,就可以实现“纯函数式”编程。

总结

在我看来,Swift 虽然实现了很多其他语言的亮点特性,但是总体实现来说并不是很整齐。 它在函数式编程方面添加了很多特性,但在控制副作用方面仅能达到平均水准。 有些特性看起来像是为了兼容原来的 Objective-C 才加入的。

Swift 写起来相对比 Objective-C 更方便一点,脱离 Xcode 这样的 IDE 来写也是应该是可以的。 目前 Swift 只支持集中少量的原生数据结构而没有标准库,更不具备跨平台特性,这是一个缺点。 在仔细阅读了文档之后发现 Swift 本身的语法细节还是很多的,就比如

switch分置语句的用法就有很多内容。 入门学习的容易程度并没有原来想象的那么好。我个人并不觉得这门语言会对其他平台的开发者有很大吸引力。 Swift 是一门很强大的语言,在其稳定版本发布之后我认为我会从 Objective-C 转向 Swift 来进行编程, 它在未来很可能成为 iOS 和 Mac 开发的首选。

关于Swift,开发者最需要了解的7个方面



1.在同一个应用中使用Objective-C和Swift进行混编

你可以在现有的项目中添加组件,并使用Swift进行编写;整个项目不需要坚持使用一种语言。同样,我们可以在任何新的Swift应用中快速使用现有的OC编写的库。
2.在开发环境中进行实时动态评估
在开发应用的过程中看看你的代码修改是如何影响app的。你可以通过Xcode的Playground完全控制应用的执行,包括快进、暂停、回放以及重放等。
3.泛型可让你类型化集合
Generics可以让你创建更灵活的代码来处理多个数据类型,而无需代码复用。而且,要处理的对象类型都是已知的,以确保运行时的安全,这样可以尽量减少代码错误。
4.高级运算符
  • 运算符过载(重载):和在C++中一样,你现在可以实现自己的自定义运算符。它已经针对集合和字符串内置。
  • 范围评估:通过值域设定((1……5))和列表评估([‘apple’, ‘banana’, ‘cherry’])简化loop结构。
  • 通过“&”字符进行错误保护:运算符前添加“&”符号可以避免出错。例如,使用“&/”运算符代替“/”避免除以0的错误。
  • Overflow/ underflow检测:检测你的运算符是否会溢出或者下溢,可以利用“&”运算符处理。
5.类型推断
Swift是一种类型化的语言,但是大部分类型声明的责任都由类型推断承担。我们还有一个类型化的安全环境,但是缺少boilerplate代码。
6.类和结构的区别
Swift结构是“value-types”的,这也就意味着当它们在你的代码中传递时,它们总会被复制,但是不参与ARC。这就允许将旧有的C类型结构简单封装为Swift,并且很容易把这些结构传回传统C函数中。
7.默认参数值 
允许函数提供默认参数值以减少过载和代码重复需求。

Swift 编程风格指南(raywenderlich.com 版本)

官方 raywenderlich.com Swift 编程风格指南

1405122209554

本文版权归 raywenderlich.com 、The Official raywenderlich.com Swift Style Guide项目以及所有贡献者所有。译者翻译仅供知识传播使用。

本风格指南的目标是让Swift代码更简洁、可读更强。

语言

推荐使用跟苹果API文档风格统一的英语。

推荐:

var color = "red"

不推荐:

var colour = "red" 

空白

  • 使用2个空白符作为缩进,不要使用Tab制表符,请务必在Xcode中进行设置;
  • 方法定义时的大括号或其它大括号(if/else/switch/while等)中的左括号写在当前语句的结尾,右括号需要另起一行。

推荐:

if user.isHappy {
  //Do something
} else {
  //Do something else
}
if user.isHappy
{
    //Do something
}
else {
    //Do something else
}
  • 方法定义之间需要插入一个空行,让代码在视觉上更清晰,也便于管理。方法内部也需要使用空行来分隔不同功能的代码块,但如果一个方法中有太多代码块,你最好将它们拆分为多个方法(重构)。

注释

只有在必要的时候才写注释来解释某段代码为什么那么做。注释必须跟代码同步更新,该删掉时就删掉。

尽量避免使用多行注释,而是让代码本身去解释自己的功能。

命名

使用可描述性的驼峰式命名法为类、函数、方法、变量等命名。模块中的类名和常量名首字母需要大写,而方法名和变量名的首字母需要小写。

推荐:

let MaximumWidgetCount = 100

class WidgetContainer {
  var widgetButton: UIButton
  let widgetHeightPercentage = 0.85
}

不推荐:

let MAX_WIDGET_COUNT = 100

class app_widgetContainer {
  var wBut: UIButton
  let wHeightPct = 0.85
}

对于函数和构造器(init),除非能一目了然,否则建议为每个参数命名。如果能让函数可读性更强,请提供每个参数的外部参数名。

func dateFromString(dateString: NSString) -> NSDate
func convertPointAt(#column: Int, #row: Int) -> CGPoint
func timedAction(#delay: NSTimeInterval, perform action: SKAction) -> SKAction!

// would be called like this:
dateFromString("2014-03-14")
convertPointAt(column: 42, row: 13)
timedAction(delay: 1.0, perform: someOtherAction)

对于类中的方法,请遵循苹果惯例,将方法名作为第一个参数的外部名:

class Guideline {
  func combineWithString(incoming: String, options: Dictionary?) { ... }
  func upvoteBy(amount: Int) { ... }
}

如果在非代码文本(包括教程、书籍以及注释中)中引用了某个函数,请提供函数所有参数的外部参数名:

The dateFromString() function is great.
Call convertPointAt(column:, row:) from your init() method.
The return value of timedAction(delay:, perform:) may be nil.
Guideline objects only have two methods: combineWithString(options:) and upvoteBy()
You shouldn't call the data source method tableView(cellForRowAtIndexPath:) directly.

类前缀

Swift中的模块(module)包含了命名空间,所有在模块中定义的类型名都不会与其它模块冲突。所以我们不再需要使用前缀命名来减少命名冲突。如果两个来自不同模块的相同名字需要同时引用,你可以使用模块名+点+类型名的方式来处理:

import MyModule

var myClass = MyModule.MyClass()

再次重申,请不要在Swift类型命名时加前缀。

如果你需要将Swift类型暴露给Objective-C,你可以为其指定一个在Objective-C中使用的合适前缀(请参考ObjC编程风格指南

@objc (RWTChicken) class Chicken {
   ...
}

分号

Swift不需要在每条语句后加分号。但如果将多条语句写在一行代码中,这时需要加上分号。 然而我们并不推荐这种将多行语句写在一行的做法。 唯一例外是for-conditional-increment结构,它必须使用分号。但你应该尽量使用for-in结构来替代这种行为: 推荐:

var swift = "not a scripting language"

不推荐:

var swift = "not a scripting language";
注意: Swift跟Javascript有很大区别,Javascript中如果忽略分号会被认为造成代码的不安全

类和结构体

请将类和结构体中的代码按以下顺序进行组织:

  • 变量和常量属性
  • 构造器
  • 公共方法
  • 私有方法
class Circle: Shape {
  var x: Int, y: Int
  var radius: Double
  var diameter: Double {
    get {
      return radius * 2
    }
    set {
      radius = newValue / 2
    }
  }

  init(x: Int, y: Int, radius: Double) {
    self.x = x
    self.y = y
    self.radius = radius
  }

  convenience init(x: Int, y: Int, diameter: Double) {
    self.init(x: x, y: y, radius: diameter / 2)
  }

  func describe() -> String {
    return "I am a circle at (\(x),\(y)) with an area of \(computeArea())"
  }

  func computeArea() -> Double {
    return M_PI * radius * radius
  }  
}

上面的例子还展示了以下风格:

  • 定义属性、变量、常量、参数以及其他语句时,在其后面加上空格,而不是在前面加。比如说,x: IntCircle: Shape
  • 属性的gettersetter以及属性观察器willSetdidSet的实现都需要缩进;
  • 如果多个变量和结构体有相同的使用目的或使用环境,可以将它们定义在同一行代码中。

Self 的使用

请避免在Swift中使用self,因为我们不需要使用self来访问一个对象的属性或调用它的方法。

唯一需要使用的场景是在类或结构体的构造器中。你可以使用self来区分传入的参数和类/结构体的属性:

class BoardLocation {
  let row: Int, column: Int

  init(row: Int,column: Int) {
    self.row = row
    self.column = column
  }
}

函数定义

尽量将较短的函数名定义在一行,并以一个左大括号结尾:

func reticulateSplines(spline: Double[]) -> Bool {
  // reticulate code goes here
}

如果函数名较长,请在适当的时候换行,并对下一行函数名进行缩进:

func reticulateSplines(spline: Double[], adjustmentFactor: Double,
    translateConstant: Int, comment: String) -> Bool {
  // reticulate code goes here
}

闭包

请尽量使用掉尾(就是将最后一个闭包参数直接附在方法调用后,看起来像是控制语句的body一样)闭包语法。无论何时,请给闭包中每个参数一个描述性的名字:

return SKAction.customActionWithDuration(effect.duration) { node, elapsedTime in 
  // more code goes here
}
笔者:@aemaeth 提出将闭包参数另起一行的写法,我认为更合理,解决了多个闭包参数过长的问题。

对于一行表达式闭包,当使用场景明确时,可以使用隐式返回:

attendeeList.sort { a, b in
  a > b
}

类型

请尽量使用Swift提供的原生类型。Swift也提供了原生类型对象桥接到Objective-C对象的办法,所以必要时你能随意使用这些桥接对象提供的方法。

推荐:

let width = 120.0                                           //Double
let widthString = width.bridgeToObjectiveC().stringValue    //String

不推荐:

let width: NSNumber = 120.0                                 //NSNumber
let widthString: NSString = width.stringValue               //NSString

Sprite Kit代码中,请多使用CGFloat,这样代码会更简洁,也能避免非常多的类型转换。

常量

常量使用let关键字定义,而变量使用var关键字定义。

如果一个值是常量,那就必须使用let关键字来准确定义。最终你会发现,你使用let的频率远大于var

小技巧:一开始你可以将所有值都定义为常量,然后如果编译器报错了再作适当的调整。

可选

如果可以接受nil值,请将变量或函数返回值的类型定义为可选类型(加?)。

当你明确知道实例变量在使用前会完成初始化,比如说视图控制器中的子视图subviews在使用前会在viewDidLoad中初始化,那么你可以将这些变量定义为隐式解析类型(使用!)。

当访问一个可选值时,如果只访问一次,或者方式时有多种可能性,请使用可选链:

myOptional?.anotherOne?.optionalView?.setNeedsDisplay()

使用可选绑定optional binding对只拆包(unwrap)一次,但执行多次操作的情况非常合适:

if let view = self.optionalView {
  // do many things with view
}

类型推断

Swift编译器可以推断出变量和常量的类型。你可以为每个常量或变量提供一个显示的类型(加个冒号,并在后面写上类型名),但大部分情况不必这么做。

我们建议多使用类型推断让编译器自动推断出常量或变量的类型,这样代码会更紧凑。

推荐:

let message = "Click the button"
var currentBounds = computeViewBounds()

不推荐:

let message: String = "Click the button"
var currentBounds: CGRect = computeViewBounds()
注意:遵循此规则意味着在命名时,需要更慎重的选择具有描述性的名字。

流程控制

建议多使用for-in风格的for循环,而不是传统的for-condition-increment风格。

推荐:

for _ in 0..5 {
  println("Hello five times")
}

for person in attendeeList {
  // do something
}

不推荐:

for var i = 0; i < 5; i++ {
  println("Hello five times")
}

for var i = 0; i < attendeeList.count; i++ {
  let person = attendeeList[i]
  // do something
}

笑脸

笑脸在raywenderlich.com网站中是一个非常突出的特性。正确的使用笑脸来表达在编程时的一种极大的快乐和兴奋,至关重要。我们使用右方括号],因为它代表了ASCII艺术字符中最大的微笑;而使用右小括号)的笑脸显得有点不那么诚心,所以我们不推荐使用。

推荐:

:]

不推荐:

:)

原文转自:https://swiftist.org/topics/165#20

Swift自适应布局(Adaptive Layout)教程(二)

原文出自:Beginning Adaptive Layout Tutorial,由@DevTalking翻译!

本文紧接第一章的内容,继续介绍如何从头开始创建一个自适应布局的天气应用程序的用户界面!

给TextContainer中添加内容

打开 Main.storyboard ,从组件库(Object Library)中拖拽两个 Label 组件到TextContainer中,位置可以随意摆放:

图片

先选择靠上的Label,然后点击底部的 Align 按钮,添加一个 Horizontal Center in Container约束,再点击 Pin 按钮,添加一个 Top Spacing to nearest neighbor 约束,设置其值为10:

图片
图片

然后选择右侧工具栏中的 Attribute Inspector 页签,将该Label的 Text 属性设置为 CupertinoColor 属性设置为 WhiteFont 属性设置为 Helvetica Neue, ThinSize 属性设置为 150

这时同学们可能会发现基本看不到刚才设置的文字的全貌,这是因为Label大小的原因。别着急,我们很快就会解决这个问题。

现在选择另一个Label,按照上述的方法给它也添加一个 Horizontal Center in Container 约束以及一个 Bottom Spacing to nearest neighbor 约束,将其值设置为10。打开右侧工具栏中的 Size Inspector 看看:

图片

然后选择 Attribute Inspector 将该Label的 Text 属性设置为 28CColor 属性设置为 White, 将 Font 属性设置为 Helvetica Neue, Thin, 将 Size 属性设置为 250

现在是时候解决Label大小的问题了。选中view controller的view,点击底部的 Resolve Auto Layout Issues 按钮,在弹出菜单中选择 All Views\Update Frames ,现在看看storyboard中发生了什么:

图片

我们看到了刚才设置的城市和温度,但是他们有一点点重叠,这可不是我们想要的结果。在我们修改这个问题之前,先看看预览编辑区的显示情况。我们发现在iPad下显示貌似还挺完美:

图片

但是在iPhone下不出所料的无法直视,字体太大了:

图片

接下来让我们解决这个重叠和字体大小的问题。

Size Classes

通用的storyboard文件固然很好,但是你想真正把它玩转还是得花功夫去研究它,这是一件很有挑战性的工作,当然我们也要懂得运用一些现有的工具来帮助我们。Xcode6就为我们提供了一些工具和技巧,帮助我们更好的实现自适应布局。

自适应布局有一个很重要的概念就是 Size Classes。它并不代表真正的尺寸,而是我们从感官上感觉尺寸的种类,通过这种种类的组合,表示出不同屏幕尺寸设备的横屏及竖屏。

Xcode6为我们提供了两个种类:普通(Regular)和紧凑(Compact)。虽然它们涉及到视图的物理尺寸,但一般它们只代表视图的语义尺寸,即不是真正的尺寸,而是我们从感官上分出的尺寸种类。

下面这个表格向同学们展示了size classes和各个尺寸设备竖屏、横屏之间的关系:

图片

上表中的这些size classes组合都是我们在开发应用中经常碰到的。然而你也可以在视图的任何一个层级中覆盖这些size classes。当以后我们开发Apple Watch应用时显得尤其有用。

Size Classes与开发者

何为设计应用的UI?虽然现在你们的应用已经知道要使用Size Classes,并且你们在storyboard文件中设计应用界面时已经抛开了具体尺寸大小的束缚。但是你们难道没有发现在所有尺寸的设备中,不管是竖屏还是横屏,应用的界面布局都是一致的吗?只是自适应了尺寸大小而已。这还远远不是设计。

当你们决心要设计自适应的界面并已经开始设计的时候,有一点很关键。那就是要知道界面在不同的Size Classes要有继承的关系。你们应该首先设计一个基础的界面,然后根据不同尺寸的横竖屏在基础的界面上进行自定义。千万不要把不同的Size Classes当做独立的屏幕尺寸去设计UI。应该在你们的脑海中建立起界面的一个继承关系的思想,也就是大多数的设备使用基础界面,然后特别的尺寸及横竖屏再根据情况基于基础界面修改。

在本文中,一直没有像大家介绍过如何设置特殊设备的布局,那是应为自适应布局的核心概念Size Classes本身就是由各种特殊设备的特点抽象而来的。也就是说一个Size Classes就意味着一种特殊设备的布局特点,其实普通情况也是特殊情况中的一种。所以说我们可以组合不同的Size Classes来满足各种特殊的布局情况,比如一个支持自适应的视图,它可以在应用的父视图控制器中自适应,也可以在某一个功能的视图控制器容器中自适应。但是两者自适应后的布局却不相同。

这种改进对Apple本身也是有益的,因为他们不断的改变移动设备的尺寸,但从来没有强迫开发者和设计者重新开发和设计他们的应用以适应新尺寸的设备。这就不会让开发者和设计者对Apple不断改变设备尺寸这件事有抗拒心理。

接下来,我们将自定义Size Classes以适应iPhone横屏的时候,因为现在的布局在横屏时用户体验很糟糕。

使用Size Classes

回到 Main.storyboard,点击底部的 w Any h Any,你就可以看到Size Classes的选择器了:

图片

在这个由9个方格组成的网格中,你就可以选择你想在storyboard中显示的Size Class。一共有9种组合方式:3种垂直的也就是竖屏的(任意尺寸(Any),普通(regular),紧凑(compact))选择和3种水平的也就是横屏(任意尺寸(Any),普通(regular),紧凑(compact))的选择。

注意:这里有一点需要大家注意。在Size Classes中,有两个重要的概念叫做

水平(Horizontal)

垂直(Vertical)

。但是在IB中叫做

宽(Width)

高(Height)

。但他们是等价的,所以大家记住这个概念有两种叫法就可以了。

目前我们的布局在紧凑高度(Compact Height)时显示的很糟糕,也就是iPhone横屏时。我们来解决这个问题,在Size Classes选择器中选择 Any Width | Compact Height 的组合:

图片

这时你会发现在storyboard中会立即出现2个变化:

图片

  1. storyboard中的view controller变成了我们刚才设置的size class。
  2. storyboard底部会出现蓝色的长条区域,并显示出当前我们正在使用的size class。

为了在该size class下改变布局,我们要临时改变一些之前设置好的约束。在自动布局中这种操作有个术语叫做 装配(installing)卸载(uninstalling) 约束。当一个约束在当前的size class中是适用的,我们就将该约束装配在当前的size class中,如果不适用,我们就卸载它。

选择image view,在右侧工具栏中选择 Size Inspector。你可以看到在image view上添加的所有约束:

图片

单击鼠标左键选择 Align Center X to: Superview 约束,然后按下键盘上的 Delete 键来卸载该约束。这时我们可以看到在storyboard中这个约束就立即消失了,并且在storyboard的结构目中和 Size Inspector中该约束都变成了灰色:

图片
图片

注意:你可以在Size Inspector中点击 All 来查看当前size class卸载掉的约束。

鼠标双击刚才卸载的那条约束,我们可以看到在约束编辑界面的底部出现了额外的2个选项:

图片

这两个选项的意思就是这条约束在基础布局中是可用的,但在当前的 Any Width | Compact Height 布局中是不可用的。

按照上面的步骤卸载掉image view上的另外3个约束:

图片
图片

现在你就可以添加适合当前size class的约束了。我们添加一个 Align/Vertical Center in Container 约束,再添加一个 Pin/Left Spacing to nearest neighbor 约束,其值设置为10:

图片
图片

选择image view,按住 Ctrl 键从image view上拖拽至view controller的view上,在弹出的菜单中选择 Equal Widths 约束。

打开右侧工具栏中的 Size Inspector 页签,双击 Equal Width to: Superview 打开该约束的属性编辑界面。如果 First Item 属性的值不是 cloud.Width ,那么点击输入框,在下拉菜单中选择 Reverse First and Second Item。然后将 Multiplier 属性的值设置为 0.45

现在image view在所有的size class中显示应该都没有什么问题了。但是text container view还有点问题。你需要给它添加一个约束,让它显示在该size class屏幕的右侧。

TextContainer view现在有两种约束在身。一种是内部约束,它约束了两个Label的位置,这些约束在各size class中表现的还不错。另一种是外部的约束,它们限制了text container view的左、右、底部与它容器的左、右、底部的间距。这些约束在当前的size class中表现的就不尽如人意了。如果想使text container view在当前size class中位于容器的右下角位置,你得卸载掉左侧的约束。

选中 Left Spacing to nearest neighbor 约束:

图片

Cmd-Delete 卸载该约束,和之前一样,被卸载的约束显示为灰色。

现在你需要再添加两个约束将TextContainer限制在正确的位置上。一个是让text container view的宽度为它容器(view controller的view)宽度的一半。另一个是将text container view固定在顶部。

按理来说,你现在需要选中text container view然后按住Ctrl键和鼠标左键拖动鼠标到view controller view上,然后选择约束。但是目前的情况由于image view和text container view占满了整个view controller,所以你很难选中view controller的view。同学们可以通过storyboard的结构树上进行该操作,会容易很多。

在结构树中选中TextContainer,按住 Ctrl 键和鼠标左键,拖动鼠标到结构树的View上:

图片

弹出菜单中显示了可用的约束,按住 Shift 键点击 Top Space to Top Layout GuideEqual Widths 约束:

图片

然后选中TextContainer,在 Size Inspector 中设置刚刚添加的两个约束:

  • Top Space to: Top Layout Guide约束的值设置为0。
  • Equal Width 约束的 Multiplier 的值设置为0.5。这里要注意 First ItemSecond Item 这两个属性的值。前者应为TextContainer view,后者为view controller view。如果不一致,那么点击任意一个输入框,选择 Reverse First and Second Item

现在点击storyboard界面底部的 Resolve Auto Layout Issues 按钮,然后选择 All Views\Update frames 。看看发生了什么变化呢:

图片

到目前为止,我们的布局已经越来越接近完美了,唯一一点不足的就是字体大小的自适应,我们会在下一节解决它!

文字属性的自适应

目前TextContainer中的文字尺寸在iPad设备上,也就是使用普通(Regular)size class显示还比较正常。但是当使用紧凑(Compact)size class时文字尺寸就显得太大了,以至于都超出了视图。不过同学们不要怕,我们照样可以在不同的Size Classes中设置不同的文字尺寸来做到自适应。

注意:与重写布局不同,在不同的size class中改变文字的属性始终会影响基础布局中的文字。它不能像布局一样,在不同的size class中设置不同的属性值。我们通过下面的方法来解决这一问题。

回到storyboard文件中,将目前的size class改为最基础的 Any Width | Any Height

选择显示Cupertino的Label,打开 Attribute Inspector 。点击 Font 属性前面的 + 号:

图片

弹出的菜单内容是让我们选择一种size class的组合来重写该组合下的文字属性。我们选择Compact Width > Any Height

图片

这时就会出现另外一个文字属性下拉框,针对于我们刚才选择的 Compact Width | Any Height size class,我们将字体大小改为90:

图片

再选择显示温度的Label,重复刚才的操作,选择size class组合时选择 Compact Width > Any Height。设置字体大小为150。

在预览区域会自动更新我们刚才的设置:

图片

现在看起来稍微好一些了,但是显示 Cupertino 的Label被截掉了两头。同学们可能会继续调整字体大小使Cupertino显示完全,虽然目前看起来完美了,但是当换一个城市名称后或许又会出现刚才的问题。比如Washington, D.C这么长的名称,又比如Kleinfeltersville, PA这个更长的名称。那么我们应该如何设计呢?

我们的救世主 自动布局(Auto Layout) 再次出马。你只需要给显示城市名称和温度的这两个Label设置一个相对于TextContainer view的宽度约束即可。选中显示Cupertino的Label,按住 Ctrl 键和鼠标左键,拖动鼠标到TextContainer view,在弹出菜单中选择 Equal Widths 约束。对显示温度的Label做相同的操作。之后在预览界面看看发生了什么:

图片

呃……貌似还是有问题,城市名显示不完全。Label中的文字长度超出了允许显示的空间。不过我们可以通过一个选项,让Label自动判断当前的空间可以显示多大的字体。

选择显示Cupertino的Label,然后打开 Attribute Inspector。将 AutoShrink 属性设置为Minimum font scale,将其值设置为0.5。将 Alignment 属性设置为 Centered

图片

对显示温度的Label做相同的操作。

再来看看预览区域,是不是在不同尺寸的iPhone横屏、竖屏下显示都比较完美了:

图片

是时候在不同的设备上编译运行我们的程序了。用设备来检验才是最保险的。iPhone下的横屏、竖屏是多么的完美:

图片
图片

同学们,至此你们已经学习到了自适应布局最基本的技能。恭喜你们!

原文地址:Beginning Adaptive Layout Tutorial

Swift自适应布局(Adaptive Layout)教程(一)

原文出自:Beginning Adaptive Layout Tutorial,由@DevTalking翻译!

通用的Storyboard

通用的stroyboard文件是通向自适应布局光明大道的第一步。在一个storyboard文件中适配iPad和iPhone的布局在iOS8中已不再是梦想。我们不必再为不同尺寸的Apple移动设备创建不同的storyboard文件,不用再苦逼的同步若干个storyboard文件中的内容。这真是一件美好的事情。

我们打开Xcode,新建一个项目:

图片

选择iOS\Application\Single View Application创建一个单视图应用:

图片

设置项目名称AdaptiveWeather,语言选择Swift,设备选择Universal

图片

创建好项目后,我们在项目目录结构中可以看到只存在一个storyboard文件:

图片

Main.storyboard文件就是一个通用的storyboard文件,它可以适配目前所有屏幕尺寸的Apple移动设备。打开该文件,同学们会看到一个View Controller,以及一个我们不太熟悉的界面尺寸:

图片

同学们不要吃惊,没错,你们看到的就是一个简单的、有点大的正方形!大伙都知道,在上一个版本的Xcode中,storyboard里的屏幕尺寸都对应着我们所选的目标设备的尺寸,但是这样无法让我们达到“用一个storyboard搞定所有设备”的宏伟目标。所以在iOS8中,Apple将storyboard中屏幕的尺寸进行了抽象处理,也就是说我们看到的这个正方形是一个抽象的屏幕尺寸。

我们接着往下走,选中Main.storyboard文件,然后在右侧工具栏中选择File Inspector页签,然后勾选Use Size Classes选项:

图片

在新的iOS8项目中,该选项默认是勾选的。但当你使用老版本的项目创建新的storyboard文件时就需要你手动进行勾选了。

设置你的Storyboard文件

首先,我们打开Main.storyboard文件,从组件库(Object Library)中选择Image View拖拽到View Controller中。选中刚刚拖入的Image View,在右侧工具栏选择Size Inspector页签,设置X坐标为150,Y坐标为20,为300,为265。

然后再拖入一个View组件,设置X坐标为150,Y坐标为315,为300,为265。

选择你刚才拖入的View,在右侧工具栏中选择Identity Inspector页签,在Document面板中的Label属性输入框中输入TextContainer。这个属性的作用就是给View起一个名字,方便我们辨认。这里要注意一下,Document面板有可能是隐藏的,我们需要点击它后面的 Show按钮来显示它。我们拖入的这个View最后是显示城市和温度Label的容器。

图片

完成上面的设置后,同学们可能会发现刚才拖入的View貌似看不到,这是因为它的背景色和View Controller的背景色是相同的,都是白色,所以我们不太容易辨别。我们来解决这个问题,选中View Controller的View,然后在右侧工具栏中选择Attribute Inspector页签,设置背景色为 红:74,绿:171,蓝:247。然后再选择TextContainer,就是我们拖入的View,设置背景色为 红:55,绿:128,蓝:186。此时Main.storyboard文件中应该是这番景象:

图片

到目前为止,我们在View Controller中添加了两个组件Image View和View,这也是仅有的两个组件,接下来我们就要给它们添加一些布局约束了。

添加布局约束

选择image view,点击底部自动布局工具栏中的Align按钮,勾选Horizontal Center in Container选项,将后面的值设置为0,点击 Add 1 Constraint按钮添加第一个约束。

图片

这个约束的意思是让image view在它的容器(View Controller的View)中保持居中。

然后再点击底部自动布局工具栏中的Pin按钮,添加一个image view顶部与容器顶部间距的约束,我们设置为0:

图片

上面这两个约束使image view处于容器居中的位置,并且它的顶部与容器顶部有一个固定的间距。现在我们需要添加image view和text container view之间的约束。同学们先选中image view,然后按住Ctrl键和鼠标左键,从image view往text container view移动鼠标:

图片

松开鼠标左键后会弹出一个约束菜单,我们选择Vertical Spacing

图片

这个约束决定了image view底部和text container view顶部之间的距离。

现在选中image view然后点击右侧工具栏中的Size Inspector页签,同学们会发现这里在Xcode6中和之前的Xcode版本有所不同:

图片

你会看到之前添加的三个布局约束,你可以在Size Inspector中很方便的修改这些布局约束。比如点击Bottom Space To: TextContainer约束后的 Edit按钮,会弹出约束属性编辑框,我们让Constant的值等于20:

图片

然后点击该弹出框之外的任意地方关闭该弹出框。

你先已经将TextContainer view顶部与image view底部的间距调整到了20,我们还需要添加TextContainer view另外三个边的间距约束。

继续选择TextContainer view,点击底部的Pin按钮弹出 Add New Constraints窗口,在Spacing to nearest neighbor面板中设置左、右、底部的约束,将值设置为0,然后点击Add 3 Constraints按钮添加约束。这里要注意的是,在设置约束时要将 Constrain to margins选项的勾去掉,这样可以避免TextContainer view产生内边距:

图片

这三个约束会让TextContainer view的左、右、底部三个边与容器的左、右、底部的间距始终为0。

现在Main.storyboard中应该是这番景象:

图片

此时同学们应该会注意到在view上有几个橘黄色的约束线,这意味着还有一些约束上的问题需要我们注意。不过在运行时storyboard会自动更新view的大小来满足它与容器的约束条件。我们也可以点击底部 Resolve Auto Layout Issues 按钮,在弹出框中选择 All Views in View Controller/Update Frames 来修复提示的约束问题,但是如果我们这样做,那么image view的尺寸就会压缩成零,也就是会看不到image view。

这是因为我们的image view还有没有任何内容,但是它有一个缺省的高和宽,并且值为0。进行自动布局的时候,如果被约束的view没有实际的高和宽,那么会依照缺省的高和宽来满足约束条件。

我们接着学习,在项目结构中打开 Images.xcassets ,然后点击左下角的 +号,在弹出菜单中选择 New Image Set

图片

双击左上角的 Image 标题将其改为 cloud

图片

我们刚才新建的这个image set其实就是若干图片文件的一个集合,其中的每一个图片都会对应一个特定的应用场景,也就是针对与不同分辨率的Apple移动设备。比如说,一个图片集合可能会包含针对非视网膜、视网膜、视网膜高清三种分辨率的图片。自从Xcode中的资源库与UIKit完美结合后,在代码中引入图片时我们只需要写图片的名称,程序在运行时会根据当前运行的设备自动选择对应分辨率的图片。

注意:如果你以前使用过通过资源库管理图片,那么你可能会发现在Xcode6中会有所不同。那就是3x图片是怎么回事?

这个新的分片率是专为iPhone 6 Plus提供的。这意味着每一个点是由3个像素点组成,也就是说3x的图片比1x图片的像素多9倍。

目前你的图片集合中还是空的,同学们可以在这里下载需要的图片cloud_images.zip,然后将图片拖入刚才创建的名为cloud的图片集合中,将 cloud_small.png图片拖到 1x图片区域:

图片

由于我们的图片背景颜色是透明的,所以在图片集合中看到的都是白色的图片。你可以选中某一个图片,然后按下空格键来预览图片。比如选中 1x 图片,按下空格:

图片

现在将 cloud_small@2x.png 图片拖至 2x 图片区域,将 cloud_small@3x.png 图片拖至 3x 图片区域。和之前情况一样,我们看到的只是白色的图片,但我们可以通过空格键来预览图片集合中的图片。

现在你就可以在image view中设置图片了。我们回到 Main.storyboard 中,选中image view,在右侧工具栏中选择 Attribute Inspector 页签,将 Image View 面板中的 Image 属性设置为cloud,然后将 View 面板中的 Mode 属性设置为 Aspect Fit

图片

现在你的Main.storyboard中应该是这番景象:

图片

我们看到storyboard中一直有橘黄色的约束提示,是时候让我们来修复它们了。首先选中view controller的view:

图片

然后点击底部的 Resolve Auto Layout Issues 按钮,在弹出菜单的 All Views in View Controller 面板中选择 Update Frames

图片

这时,storyboard会自动根据约束条件重新计算view的大小以满足约束:

图片

预览助手编辑器(Preview Assistant Editor)

一般情况下,在这个时候我们应该会在iPad、iPhone4s、iPhone5s、iPhone6、iPhone6 Plus这几个不同尺寸的设备上编译运行程序,以便测试通用的storyboard是否能在不同尺寸的设备上正确的自适应。但这确实是个体力活,一遍一遍的更改设备、编译、运行,多么苦逼。但上天总是会眷顾我们这些苦逼的程序员,Xcode6提供了Preview Assistant Editor,能在一个界面上显示出不同尺寸设备的程序运行情况,是否有问题一目了然。

我们打开 Main.storyboard ,然后选择 View\Assistant Editor\Show Assistant Editor ,这时编辑区会分隔为两部分。再点击顶部导航栏中的 Automatic ,在弹出菜单中选择 Preview ,最后选择 Main.storyboard (Preview)

图片

现在在 Assistant Editor 区域会显示一个4寸的界面:

图片

我们还可以点击预览界面底部,名字(比如图中的iPhone 4-inch)旁边的地方让屏幕翻转为横屏:

图片

这无疑是针对检查不同尺寸设备的自适应情况的一项重大改进,但还远远不止于此!点击预览界面左下角的 + 按钮,会弹出当前storyboard文件支持的各种尺寸的设备,可供我们预览:

图片

分别选择iPhone 5.5-inch和iPad,此时我们在预览界面就可以同时显示三种尺寸的屏幕:

图片

此时同学们是否注意到4寸的横屏显示有点别扭呢?没错,它的那朵元太大了,我们可以通过对image view添加其他的约束条件来改善这个问题。

回到 Main.storyboard ,选择image view,然后按住 Ctrl建和鼠标左键,拖动鼠标到View Controller的View上,松开鼠标后会弹出一个菜单,我们选择 Equal Heights

图片

这时会出现一些红色的约束提示,这是因为我们刚才加的这个约束条件与之前加过的约束条件有冲突。因为之前我们添加过image view和TextContainer view之间的垂直间距(Vertical Margins)约束,所以image view的高度不可能等于它容器(View Controller的View)的高度。

让我们来修复该问题,首先在storyboard的结构目录中选择我们刚才添加的 Equal Heights约束,然后选择右侧工具栏中的 Attribute Inspect 页签,如果 First Item 属性不是cloud.Height ,那么在下拉菜单中选择 Reverse First and Second Item 这一项让 First Item 的值成为 cloud.Height

图片

接下来将 Relation 属性的值设置为 Less Than or Equal ,将 Multiplier 的值设置为 0.4

图片

这一系列设置的作用是让cloud这张图片的高度要么等于它自身的高度,要么等于屏幕高度的40%,最后呈现的效果选择这两者中较小的一个高度。

现在你应该注意到了在预览面板中,4寸的横屏显示即时的对你刚才的约束改动做出了响应:

图片

你看看其他尺寸的预览自动更新了么?答案那是必须的,所以说 Preview Assistant Editor 确实是一项重大改进,是程序员和设计人员的福音!

由于本文的示例是一个天气应用,所以光有天气图标不行,我们还得加上城市和温度才行,所以敬请期待下一篇教程吧!

如何使在Swift中使用UIScrollView进行滚动和缩放


原文地址:http://www.raywenderlich.com/76436/use-uiscrollview-scroll-zoom-content-swift
泰然翻译组:Niwa。校对:glory。

Learn how to use UIScrollViews for paging, zooming, scrolling, and more!

UIScrollView是IOS中非常有用的一个组件。它是通用组件UITableView的基类也是放置尺寸大于一个屏幕的内容时很好的组织方式。本教程会介绍UIScrollView的下列用法:

  • 如何使用UIScrollView浏览大尺寸图片。
  • 如何在缩放时保持UIScrollView中的内容居中.
  • 如何在UIScrollView中嵌入复杂的显示层次关系。
  • 如何使用UIScrollView的分页(paging)特性, 联合使用UIPageControl在不同页面之间移动。
  • 通过“偷窥”UIScrollView得到前一页,后一页还有当前页的缩略图。
  • 其他内容!

本文假设你熟悉Swift语言和IOS开发。如果你完全是一名初学者, 你可能需要先学习一下本站的其他课程

本文还假设你知道如何使用Interface Builder在view中添加对象和在你的代码和Storyboard连接outlets。 如果你刚刚接触Storyboards和Interface Builder, 你可以在开始本教程之前线学习一下本站的Storyboards教程

开始

打开Xcode创建工程。选择File\New\Project…, 接下来选择iOS\Application\Single View Application模版。 在product name输入框中输入ScrollViews, language选择Swift, devices选择 iPhone.

Create Project

单击Next选择工程文件的保存位置。

之后下载工程的资源压缩包, 然后将解加压后的文件拖拽到工程的根目录。并确保选中了“Copy items if needed”复选框。

Copy Resources

本文会介绍Scroll view 的四种不同使用方式, 工程采用一个tableview来提供这四个选项。每个选项打开一个新的视图控制器(view controller)来展示scroll views的一种用法。

下图展示了完成时storyboard的状态:

Storyboard Overall

通过下列步骤创建一个tableview:

  1. 打开Main.storyboard删除当前场景。 选择view controller (单击 storyboard) 然后删除它。

  2. 关闭Auto Layout功能。在Utilities面板中选择File Inspector, 取消Use Auto Layout复选框的选中状态。这会弹出一个警告窗口。在弹出窗口中确保“Keep size class data for:”选中了iPhone。然后选择Disable Size Classes。
    Disable Size Classes

  3. 接下来, 添加一个Table View Controller,从Object Library中拖拽一个Table View Controller到storyboard。

  4. 选中刚添加的table, 然后在菜单中选择Editor\Embed In\Navigation Controller。

  5. 同时选中table view和table view controller, 将Attributes Inspector中的content type修改为Static Cells(如下图所示)。
    Static Cells

  6. 在文档概要视图中Table View单击下面的按钮显示storyboard层次关系视图,然后选中Table View Section. 将行数设置为4。如下图所示
    UIScrollView table view section

  7. 依次编辑table view的每一行, 将style设置为Basic,编辑显示的标签:



    • Image Scroll
    • Custom View Scroll
    • Paged
    • Paged with Peeking

注释: 由于你将每一行的style设置成了“Basic”, 表单的每一行会出现额外的标签子组件。你要再次展开每行的子项目来进行编辑。

保存storyboard, 之后构建并运行。你会看到你编辑的table view如下图所示.不过现在table view中还没有任何东西 - 但你可以改好它!

Scroll View Run 1

浏览和缩放大尺寸图片

第一步我们先来学习设置scroll view来允许用户缩放和滚动浏览图片。
首先, 你需要设置view controller。打开ViewController.swift, 修改文件头部的类声明使它符合UIScrollViewDelegate协议:

    class ViewController: UIViewController, UIScrollViewDelegate {

在类的声明中添加下面的outlet属性:

    @IBOutlet var scrollView: UIScrollView!

下一步你将会将它连接到真正的scroll view。

打开storyboard从objects library拖拽一个View Controller到画布。选择新的view controller在Identity Inspector中 将class修改为ViewController。

View controller class type

这个view controller展示图片的滚动. 从table view中选中Image,按住Control键,从这一行拖动到新添加的view controller。在弹出的菜单中选择Selection Segue下面的Push按键。 当用户选择第一行时,view controller将会被压到导航栈(navigation stack)上。

从Object Library中拖拽一个Scroll View来填充这个新添加的view controller。

Add Scroll View

连接scroll view和view controller, 将view controller附加到scroll view的outlet中并设置view controller作为scroll view的代理。

Scroll View Outlets

现在你要处理代码了。打开ViewController.swift在类中添加新的属性:

    var imageView: UIImageView!

这个变量代表用户滚动时使用的image view。

现在我们要进行设置scroll view的过程中最有趣的部分。用下面的方法替换viewDidLoad:

    override func viewDidLoad() {
      super.viewDidLoad()

      // 1
      let image = UIImage(named: "photo1.png")
      imageView = UIImageView(image: image)
      imageView.frame = CGRect(origin: CGPoint(x: 0, y: 0), size:image.size)
      scrollView.addSubview(imageView)

      // 2
      scrollView.contentSize = image.size

      // 3
      var doubleTapRecognizer = UITapGestureRecognizer(target: self, action: "scrollViewDoubleTapped:")
      doubleTapRecognizer.numberOfTapsRequired = 2
      doubleTapRecognizer.numberOfTouchesRequired = 1
      scrollView.addGestureRecognizer(doubleTapRecognizer)

      // 4
      let scrollViewFrame = scrollView.frame
      let scaleWidth = scrollViewFrame.size.width / scrollView.contentSize.width
      let scaleHeight = scrollViewFrame.size.height / scrollView.contentSize.height
      let minScale = min(scaleWidth, scaleHeight);
      scrollView.minimumZoomScale = minScale;

      // 5
      scrollView.maximumZoomScale = 1.0
      scrollView.zoomScale = minScale;

      // 6
      centerScrollViewContents()
    }

好像有点复杂,让我们一步一步的进行解释,你会发现并没有那么坏。

  1. 首先, 你需要使用photo1.png(已经添加到工程中)创建一个image view。然后设置image view的frame (大小和位置)尺寸为图片的尺寸,位置在上层视图的(0,0)点。 最后将image view添加为scroll view的子视图。

  2. 告诉scroll view它里面内容的尺寸, 这样他就知道在水平方向和竖直方向上如何滚动。 在这里它的大小是图片的尺寸。

  3. 这里我们设置了一个双击的手势来实现缩放的功能。UIScrollView已经内建实现了捏合的手势(UIPinchGestureRecognizer)实现缩放功能,因此你不需要自己添加了。

  4. 下一步, 你要计算出scroll view的最小的缩放尺寸。 当缩放尺寸的值是1时表示它的内容会正常显示。小于1的值表示内容会被缩小,大于1的值表示内容会被放大。 为了计算最小的缩放尺寸, 你要计算出图片正好符合scroll view的宽度的值scaleWidth和正好符合scroll view的高度的值scaleHeight,scaleWidth和scaleHeight的最小值就是scroll view的最小缩放尺寸。这样在缩到最小的时候,你能够看到整个图片。

  5. 将最大的缩放尺寸设置成1是因为,放大图片超过它的分辨率会引起图片的模糊。将初始的缩放尺寸设置成了最小尺寸,就是说开始时整个图片缩到了最小,你能看到完整的图片。

  6. 调用centerScrollViewContents函数将图片放置到了scroll view的中央。centerScrollViewContents是如何函数的实现的?稍后会做详细解释!

在类中添加centerScrollViewContents的实现:

    func centerScrollViewContents() {
        let boundsSize = scrollView.bounds.size
        var contentsFrame = imageView.frame

        if contentsFrame.size.width < boundsSize.width {
        contentsFrame.origin.x = (boundsSize.width - contentsFrame.size.width) / 2.0
        } else {
        contentsFrame.origin.x = 0.0
        }

        if contentsFrame.size.height < boundsSize.height {
        contentsFrame.origin.y = (boundsSize.height - contentsFrame.size.height) / 2.0
        } else {
        contentsFrame.origin.y = 0.0
        }

        imageView.frame = contentsFrame
    }

函数中设置的位置有一点不好理解:如果scroll view的内容大小比它的边界小,那么会将位置放在左上角而不是中心。这是因为你允许用户缩小图片,将图片放置在中心会好一些。函数通过这样放置图片使它在scroll view的中心。

最后,在类中添加scrollViewDoubleTapped的实现来处理双击的手势:

    func scrollViewDoubleTapped(recognizer: UITapGestureRecognizer) {
      // 1        
      let pointInView = recognizer.locationInView(imageView)

      // 2
      var newZoomScale = scrollView.zoomScale * 1.5
      newZoomScale = min(newZoomScale, scrollView.maximumZoomScale)

      // 3
      let scrollViewSize = scrollView.bounds.size
      let w = scrollViewSize.width / newZoomScale
      let h = scrollViewSize.height / newZoomScale
      let x = pointInView.x - (w / 2.0)
      let y = pointInView.y - (h / 2.0)

      let rectToZoomTo = CGRectMake(x, y, w, h);

      // 4
      scrollView.zoomToRect(rectToZoomTo, animated: true)
    }

当识别出双击的手势时这个函数就会被调用。 下面详细解释这个函数的实现:

  1. 首先,计算出双击在image view中的位置。就像用户期望的那样,我们将会使用这个点作为中心进行放大。

  2. 接下来, 我们将新的缩放尺寸放大到现在的1.5倍,但不要超过之前在viewDidLoad函数中设置的最大缩放尺寸。

  3. 然后使用第一步计算出的位置计算要放大的矩形尺寸。

  4. 最后, 告诉scroll view进行缩放并使用动画效果是缩放看起来更美观。

想了解IOS中其他可用的手势,可以参考本站的 UIGestureRecognizer教学

现在, 还记得我们怎么设置ViewController实现UIScrollViewDelegate的吗? 接下来, 你将会实现这个协议中需要用到的若干方法。 在类中添加下面的方法:

    func viewForZoomingInScrollView(scrollView: UIScrollView!) -> UIView! {
      return imageView
    }

这是scroll view实现缩放机制的核心。你指出了当进行捏合操作的时候哪一个view将会被放大或者缩小。也就是我们的imageView。

最后在类中添加代理方法:

    func scrollViewDidZoom(scrollView: UIScrollView!) {
      centerScrollViewContents()
    }

当用户完成缩放操作时这个方法就会被调用。这里你需要重新将试图放置到中心。 如果不这样做,scroll view就不会自然地进行缩放,他会出现在左上角。

深呼吸, 调整一下你的姿势然后重新构建和运行你的工程! 单击Image Scroll,如果事情进展的很顺利的话,你会看到一张图片,你可以缩放浏览和双击。耶!

Scroll View Run 2

滚动和缩放视图层级

如果在scroll view里有多于一张图片会怎么样呢?如果你有一个复杂的显示层次需要缩放和浏览呢?好的,这里就有这样一个scroll view!此外,你只需要在上一步的基础上做很少的改动。

使用iOS\Source\Cocoa Touch Class subclass模版创建一个新的文件。将类命明为CustomScrollViewController并将父类设置为UIViewController。确保“Also create XIB file”没有选中,语言使用Swift。 单击Next将文件保存到工程目录中。

打开CustomScrollViewController.swift替换为下面内容:

    import UIKit

    class CustomScrollViewController: UIViewController, UIScrollViewDelegate {
      @IBOutlet var scrollView: UIScrollView!

    }

接下来, 打开Main.storyboard, 添加一个新的View Controller并和table的第二行通过push segue 进行连接。 将view controller的class修改成刚创建的CustomScrollViewController。

添加一个新的Scroll View并连接outlet和设置view controller作为它的delegate,就和上面的步骤一样。

然后, 打开CustomScrollViewController.swift并在你的scrollView outlet下面添加如下属性:

    var containerView: UIView!

和上一节的view controller的不同点是,你有一个叫做containerView的UIView,而上一节你用的是UIImageView。 这对如何他将工作是一个提示。

现在, 实现viewDidLoad方法.

    override func viewDidLoad() {
      super.viewDidLoad()

      // 设置container view来保持你定制的视图层次
      let containerSize = CGSize(width: 640.0, height: 640.0)
      containerView = UIView(frame: CGRect(origin: CGPoint(x: 0, y: 0), size:containerSize))
      scrollView.addSubview(containerView)

      // 设置你定制的视图层次
      let redView = UIView(frame: CGRect(x: 0, y: 0, width: 640, height: 80))
      redView.backgroundColor = UIColor.redColor();
      containerView.addSubview(redView)

      let blueView = UIView(frame: CGRect(x: 0, y: 560, width: 640, height: 80))
      blueView.backgroundColor = UIColor.blueColor();
      containerView.addSubview(blueView)

      let greenView = UIView(frame: CGRect(x: 160, y: 160, width: 320, height: 320))
      greenView.backgroundColor = UIColor.greenColor();
      containerView.addSubview(greenView)

      let imageView = UIImageView(image: UIImage(named: "slow.png"))
      imageView.center = CGPoint(x: 320, y: 320);
      containerView.addSubview(imageView)

      // 告诉scroll view内容的尺寸
      scrollView.contentSize = containerSize;

      // 设置最大和最小的缩放系数
      let scrollViewFrame = scrollView.frame
      let scaleWidth = scrollViewFrame.size.width / scrollView.contentSize.width
      let scaleHeight = scrollViewFrame.size.height / scrollView.contentSize.height
      let minScale = min(scaleWidth, scaleHeight)

      scrollView.minimumZoomScale = minScale
      scrollView.maximumZoomScale = 1.0
      scrollView.zoomScale = 1.0

      centerScrollViewContents()
    }

viewDidLoad使用一个独立的根视图(root view)设置一个现实层级, 这个root view就是你的实例变量containerView。 然后你讲这个独立的视图添加到scroll view中。 这里就是关键所在 – 由于代理的回调viewForZoomingInScrollView只能返回一个视图,因此你只能想scroll view中添加一个视图。将zoomScale设置为1而不是minScale,这样内容视图就会在正常尺寸而不是适合屏幕的大小。

再次实现centerScrollViewContents和UIScrollViewDelegate的两个代理方法, 将原始版本中的imageView替换为containerView。

    func centerScrollViewContents() {
      let boundsSize = scrollView.bounds.size
      var contentsFrame = containerView.frame

      if contentsFrame.size.width < boundsSize.width {
        contentsFrame.origin.x = (boundsSize.width - contentsFrame.size.width) / 2.0
      } else {
        contentsFrame.origin.x = 0.0
      }

      if contentsFrame.size.height < boundsSize.height {
        contentsFrame.origin.y = (boundsSize.height - contentsFrame.size.height) / 2.0
      } else {
        contentsFrame.origin.y = 0.0
      }

      containerView.frame = contentsFrame
    }

    func viewForZoomingInScrollView(scrollView: UIScrollView!) -> UIView! {
      return containerView
    }

    func scrollViewDidZoom(scrollView: UIScrollView!) {
      centerScrollViewContents()
    }

注意: 你会发现这里没有使用UITapGestureRecognizer. 这只是为了使这部分的教学更简单明了。你可以添加这个功能作为额外的练习。

现在构建和运行你的工程。 这次选择Custom View Scroll,你会惊奇的发现你可以浏览和缩放你手动创建的一个UIView场景!

Scroll View Run 3

使用UIScrollView进行分页

在本教程的第三部分, 你会创建一个允许翻页的scroll view。 也就是说当你停止拖拽的时候scroll view会锁定到一页。就像在AppStore中你查看某个应用的截图一样。

使用iOS\Source\Cocoa Touch Class subclass模版创建名为PagedScrollViewController的类, PagedScrollViewControllerand继承自UIViewController。 确保Also create XIB file的复选框没有选中,编程语言选择Swift. 单击Next保存到工程。

打开PagedScrollViewController.swift替换城下面的内容:

    import UIKit

    class PagedScrollViewController: UIViewController, UIScrollViewDelegate {
      @IBOutlet var scrollView: UIScrollView!
      @IBOutlet var pageControl: UIPageControl!
    }

接下来, 打开Main.storyboard,像之前操作的那样添加View Controller使用push segue连接table的第三行。 设置view controller的class为刚创建的PagedScrollViewController。

设置指示图的background color为黑色(black), 这样你要添加的page control才能可见 – background color缺省情况下是白色, 你是无法看到覆盖在白色view上的白色view的!

添加Page Control元素到view的底部并使他宽度上充满整个view。 连接到pageControl outlet。

你还要添加并链接一个Scroll View到创建的outlet, 设置view controller作为代理, 具体步骤不再赘述。 调整Scroll View的尺寸填满view controller但要为Page Control留下一些空间。

这一次, 通过Attributes Inspector打开scroll view的Paging Enabled选项。

Paging-enabled

现在PagedScrollViewController.swift文件并在outlets之后添加下列属性:

    var pageImages: [UIImage] = []
    var pageViews: [UIImageView?] = []

你会注意带这次和之前有些不同, 这次没有container view,而是两个数组。

  • pageImages: 这个数组保存要显示的图片,每页显示一张。
  • pageViews: 这个数组保存每页显示图片用的UIImageView对象。由于我们会延迟加载(当使用的时候再加载),因此它是一个可选数组,因此你要处理数组中的空值。

接下来实现viewDidLoad:

    override func viewDidLoad() {
      super.viewDidLoad()

      // 1
      pageImages = [UIImage(named: "photo1.png"),
        UIImage(named: "photo2.png"),
        UIImage(named: "photo3.png"),
        UIImage(named: "photo4.png"),
        UIImage(named: "photo5.png")]

      let pageCount = pageImages.count

      // 2
      pageControl.currentPage = 0
      pageControl.numberOfPages = pageCount

      // 3
      for _ in 0..

将上面的函数进行分解,详细介绍一下每个步骤:

  1. 首先, 设置pageImages数组. 之前向工程中添加了5张照片。这个数组会包含着5张图片。

  2. 页面索引从0开始, 设置pageControl的第一页和总页数。

  3. 接下来, 设置pageViews,它包含UIImageView对象. 开始时还没有页面载入进来,我们使用nil对象占位-数组下标对应一个页面。之后会使用可选绑定检查页面是否已经加载。

  4. scrollView需要知道他的内容的尺寸。 你需要一个支持水平的页面滚动(如果想使用竖直页面滚动也是很容易修改的)的scroll view,使用页面总数乘以pagesScrollViewSize的宽度的乘积作为scrollView的内容宽度,内容的高度与pagesScrollViewSize的高度相同。

  5. 在初始化是你需要显示某些页面,所以调用了loadVisiblePages方法, 之后我们会实现它.

向类中添加谢列方法:

    func loadPage(page: Int) {
      if page < 0 || page >= pageImages.count {
        // 如果超出了页面显示的范围,什么也不需要做
        return
      }

      // 1
      if let pageView = pageViews[page] {
        // 页面已经加载,不需要额外操作
      } else {
        // 2
        var frame = scrollView.bounds
        frame.origin.x = frame.size.width * CGFloat(page)
        frame.origin.y = 0.0

        // 3
        let newPageView = UIImageView(image: pageImages[page])
        newPageView.contentMode = .ScaleAspectFit
        newPageView.frame = frame
        scrollView.addSubview(newPageView)

        // 4
        pageViews[page] = newPageView
      }
    }

每个页面都存储在了可选数组中。当view controller加载时, 使用nil填充数组, 这个方法会加载每个页面的内容:

  1. 首先, 使用可先绑定检查是否已经加载了视图,如果pageView包含UIImageView就什么也不需要做,忽略后续处理。

  2. 如果pageView是nil就需要创建一个页面。 先计算出页面的frame大小。 它与scrollView具有相同的尺寸,y方向偏移为0,将页面尺寸乘以页面序号作为x方向(水平方向)的偏移量。

  3. 创建新的UIImageView设置并添加到scrollView.

  4. 最后, 使用新创建的newPageView替换原来的nil对象。这样如果需要再次加载这个页面,程序会进入if分支并且不需要额外的操作了,因为页面已经创建过了。

接下来, 向类中添加如下函数:

    func purgePage(page: Int) {
      if page < 0 || page >= pageImages.count {
        // 如果超出要显示的范围,什么也不做
        return
      }

      // 从scrollView中移除页面并重置pageViews容器数组响应页面
      if let pageView = pageViews[page] {
        pageView.removeFromSuperview()
        pageViews[page] = nil
      }
    }

这个函数会清理之前使用loadPage创建的一个页面,它首先检查pageViews数组中这个页面的对象是否是nil,如果不是nil就从scroll view中移除这个对象并把pageViews中对应页面再次设置成nil,表明这个页面已经清理完成。

你问为什么要这么麻烦的使用延迟加载和清理页面?好的, 在本例中, 你在开始的时候加载所有的页面并没什么关系,因为一共只加载5个页面,这并不会消耗太多内存。但如果你有100个页面要加载,每张图5MB。如果你一次加载所有的页面这将会占用500MB的内存! 你的应用很快就会耗尽可用的内存空间并被操作系统杀死。延迟加载意味着你在给定时刻在内存里只有一定数量的页面。

上面的loadPage和purgePage两个函数通过loadVisiblePages函数联合在一起。向类中添加这个方法:

    func loadVisiblePages() {
      // 首先确定当前可见的页面
      let pageWidth = scrollView.frame.size.width
      let page = Int(floor((scrollView.contentOffset.x * 2.0 + pageWidth) / (pageWidth * 2.0)))

      // 更新pageControl
      pageControl.currentPage = page

      // 计算那些页面需要加载
      let firstPage = page - 1
      let lastPage = page + 1

      // 清理firstPage之前的所有页面
      for var index = 0; index < firstPage; ++index {
        purgePage(index)
      }

      // 加载范围内(firstPage到lastPage之间)的所有页面
      for index in firstPage...lastPage {
        loadPage(index)
      }

      // 清理lastPage之后的所有页面
      for var index = lastPage+1; index < pageImages.count; ++index {
        purgePage(index)
      }
    }

这里先计算出scroll view当前所在的页面,更新pageControl然后加载和清理相关的页面。计算当前所在页面的方式有些可怕,但也没那么坏。你可以通过计算几个页面来确信这是正确的。(注意floor()函数会取里传入的值最近的最小整数)

你预先加载当前页面和当前页面相邻的页面。因此当用户开始滚动页面的时候用户可以在下一个页面居中的时候可以看到下一个页面。如果你想的话你也可以加载前后两个甚至三个页面,但这除了增加内存外没有其他用处。

最后要做的事就是实现UIScrollView协议。 这次只需要实现scrollViewDidScroll(). 添加到 PagedScrollViewController.swift:

    func scrollViewDidScroll(scrollView: UIScrollView!) {
      // Load the pages that are now on screen
      loadVisiblePages()
    }

所有这些是为了确保当页面滚动完后相关的页面总会加载完毕(不需要的页面被清理完毕)。

构建和运行工程, 选择Paged然后对你创建的scroll view的奇妙页面感到惊奇吧!

Scroll View Run 4

显示前面后面的页面

这个工程的最后一部分, 我会向你展示如何是scroll view像App Store中的应用截图那样展示。你可以看到前个页面和后一个页面的一部分,这是一个有用的技术,因为用户可以立即看到是否有额外的内容可以滚动。

使用iOS\Source\Cocoa Touch Class subclass模版创建名为PeekPagedScrollViewController的类并将他的父类设置为UIViewController。确保Also create XIB file没有选中,语言使用Swift。单击 Next保存到工程.

打开PeekPagedScrollViewController.swift替换为如下内容:

    import UIKit

    class PeekPagedScrollViewController: UIViewController, UIScrollViewDelegate {
      @IBOutlet var scrollView: UIScrollView!
      @IBOutlet var pageControl: UIPageControl!

      var pageImages: [UIImage] = []
      var pageViews: [UIImageView?] = []
    }

实现viewDidLoad方法, 这和前一部分得内容是一样的:

    override func viewDidLoad() {
      super.viewDidLoad()

      // 设置需要滚动和缩放的图片
      pageImages = [UIImage(named: "photo1.png"),
        UIImage(named: "photo2.png"),
        UIImage(named: "photo3.png"),
        UIImage(named: "photo4.png"),
        UIImage(named: "photo5.png")]

      let pageCount = pageImages.count

      // 设置pageControl
      pageControl.currentPage = 0
      pageControl.numberOfPages = pageCount

      // 设置保存每个页面的view的数组
      for _ in 0..

之后, 实现loadVisiblePages, loadPage:, purgePage:和scrollViewDidScroll: UIScrollView 代理函数, 这些函数也和上一部分的函数内容相同, 只有loadPage有一点不一样,下面会进行解释:

    func loadVisiblePages() {
      // 首先确定当前可见页面
      let pageWidth = scrollView.frame.size.width
      let page = Int(floor((scrollView.contentOffset.x * 2.0 + pageWidth) / (pageWidth * 2.0)))

      // 更新pageControl
      pageControl.currentPage = page

      // 计算哪些页面需要加载
      let firstPage = page - 1
      let lastPage = page + 1

      // 清理firstPage之前页面
      for var index = 0; index < firstPage; ++index {
        purgePage(index)
      }

      // 加载范围内的页面
      for index in firstPage...lastPage {
        loadPage(index)
      }

      // 清理lastPage之后的页面
      for var index = lastPage+1; index < pageImages.count; ++index {
        purgePage(index)
      }
    }

    func loadPage(page: Int) {
      if page < 0 || page >= pageImages.count {
        // 如果在显示的范围外,什么也不做
        return
      }

      // 加载特定页面,首先检查是否已经加载
      if let pageView = pageViews[page] {
        // 已经加载,什么也不需要做
      } else {
        var frame = scrollView.bounds
        frame.origin.x = frame.size.width * CGFloat(page)
        frame.origin.y = 0.0
        frame = CGRectInset(frame, 10.0, 0.0)

        let newPageView = UIImageView(image: pageImages[page])
        newPageView.contentMode = .ScaleAspectFit
        newPageView.frame = frame
        scrollView.addSubview(newPageView)
        pageViews[page] = newPageView
      }
    }

    func purgePage(page: Int) {
      if page < 0 || page >= pageImages.count {
        // 如果在显示的范围外,什么也不做
        return
      }

      // 从scroll view移除页面并重置容器数组
      if let pageView = pageViews[page] {
        pageView.removeFromSuperview()
        pageViews[page] = nil
      }
    }

    func scrollViewDidScroll(scrollView: UIScrollView!) {
      // 加载现在在屏幕中的页面
      loadVisiblePages()
    }

和上一节的实现不同的是loadPage函数的下面一行代码:

    frame = CGRectInset(frame, 10.0, 0.0)

这行代码设置image view的frame有一个小的水平偏移,以至于这些页面不能触摸。就像App Store中应用截图显示的那样。

现在打开Main.storyboard, 像之前的示例那样,添加View Controller并使用push segue连接到table的第四行,设置view controller的class为刚刚添加的PeekPagedScrollViewController。

像之前一样, 设置主视图的背景为黑色, 添加Page Control元素并进行连接. 同样添加并连接一个“Paging Enabled” Scroll View到创建按的outlet, 设置view controller作为他的代理。

使scroll view比屏幕稍小一点 – 建议240×312 – 放置到屏幕中心. 最后会如下图所示:

Peek paged scroll view

之后, 去掉scroll view的Clip Subviews选项. 这将允许绘制视图外面的部分,这对查看页面很重要。

Clip subviews

构建和运行, 选择Paged with peeking, 这就是你想要的! 干得好!

Scroll View Run 5

检测Scroll View外面的触摸

你可能已经注意到查看的页面,你不能点击到scroll view外面的部分。这还不太理想,不是吗?我们可以修正它!

问题是scroll view只能得到在他的边界内的触摸, 现在它的边界比绘制区域小(因为关闭了Clip SubViews), 这会丢失一些触摸。你将通过将scroll view包装到一个容器类来修复这个问题,这个容器类的任务是拦截触摸时间并将它切换到scroll view。

使用iOS\Source\Cocoa Touch Class subclass模版创建类名为ScrollViewContainer的文件,将他的父类设置为UIView。 语言设置为Swift单击Next保存到工程。

打开ScrollViewContainer.swift替换长下面的内容:

    import UIKit

    class ScrollViewContainer: UIView {
      @IBOutlet var scrollView: UIScrollView!

      override func hitTest(point: CGPoint, withEvent event: UIEvent!) -> UIView? {
        let view = super.hitTest(point, withEvent: event)?
        if let theView = view {
          if theView == self {
            return scrollView
          }
        }

        return view
      }
    }

很简单,对吗?我打赌你认为会有很有很多代码。好的,不是现在。它所要做的事情是将在容器边界内触发的触摸移交到scroll view。

现在你需要使用你新创建的容器。

打开Main.storyboard后退这个例子中的Peek Paged Scroll View Controller。 选择scroll view然后选择Editor\Embed In\View. 这件创建一个新的view,然后将scroll view放到新的view中。

设置新的view的宽度(width)为屏幕宽度高度为scroll view的高度。然后设置他的class为 ScrollViewContainer。 连接容器的outletdao scroll, 如下图所示:

Embed in container

将容器的背景色设置为明晰的颜色, 因为他在主视图的上层, 而容器类的默认背景是白色的。

构建并运行。选择Paged with peeking。 就像你想要的那样,现在你可以点击到scroll view的边界之外了。耶!只用几行代码就搞定了是多么酷啊!

Scroll View Run 6

下一步可以做什么?

这里是包含本教程所有代码的示例工程

你已经研究了scroll view支持的许多有趣的特性。如果你对本片教程所做的事情很有信心你可以尝试下面的内容:

  • 创建竖直翻页的scroll view。
  • 在分页的scroll view中嵌入可以缩放的scroll view这样每个页面都可以独立的浏览和缩放。
  • 通过在水平翻页的scroll view中嵌入数值翻页的scroll view制造2D网格的假象。

现在来使用你获得的scroll view的技能制作优秀的应用吧!

关于本教程,如果你遇到任何问题或者有任何反馈,加入下面的讨论吧。

Swift中使用NSURLProtocol(NSURL协议)


原文地址:http://www.raywenderlich.com/76735/using-nsurlprotocol-swift
泰然翻译组:独轩。校对:glory。

友情提示:本教程中内容由Zouhair Mahieddine编写,适用于ios8系统swift语言,在xcode6 beta 7下编译通过。由Rocir Santiago整理。

NSURL协议对URL来说就像一把神奇的钥匙。它可以允许你通过定义定制的URL方案和重新定义现有的URL方案的行为来重新定义苹果系统的URL加载系统的操作。

听起来很神奇吧?那是必须的必啊~~。因为,如果仔细观察的话,对URLS就会有一种感受-就如同我们周围无时不在的爱!UIWebView和WKWebView使用的是什么?

URLS.MPMoviePlayer的视频流又是用的什么?URLS。你是怎么将自己的app上传到Itunes,新兴起的FaceTime和Skype?如何在系统中运行一个不存在的软件?在一个网页中载入一张图像?这些都是使用的URLS。看一眼NSFileManager的详细情况吧,你会发现有许多操作方法都需要或者返回一个URLS。

在本篇教程中,您将了解如何定义一个协议处理程序来修改URL方案。它将添加一个简陋的,提前准备好的缓存层,它会在Core data存储检索资源。启用它后,一个普通的UIWebView能够做到在一段时间后离线查看浏览器缓存过的页面。

在正式开始之前,你需要明白或者熟悉一些关于网络的概念,来弄明白NSURLConnection的工作原理。如果你对NSURLConnection的概念并不是很熟悉,那么我建议你看一下这个教程,或者苹果提供的文档

准备好学习如何使用NSURLProtocol了吗?好,自己去整一杯咖啡或者其它此类的东西然后坐到一个软软的长椅上,放松你的头脑一步一步根着做。

正式开始喽

本教程将会带领你做一个简单的手机网页浏览器,正如下面要说的那样的浏览器。

它会有一个简易用来让用户输入网页地址的界面。最变态的地方是你的浏览器需要成功缓存并恢复网页。这样用户才能在一眨眼的工夫后查看已经访问过的网页,因为这些网页不在从网络中获得,而是从程序的本地缓存中读取。

你知道网页加载速度越快,就相当于用户越喜欢,开心,所以,这是一个NSURLProtocol如何提高程序艺术性的好例子。

以下是你将要做的步骤:

·使用UIWebView来显示一个网页。

·使用Core Data来缓存这些网页数据。

如果你对Core Data并不熟悉,可以看一下这个教程。不管怎么样,我感觉这个教程中所使用的所有代码对于学习NSURLProtocol已经是足够了。使用Core Data只是实现本地缓存的一种方法,所以它不是本教程的重点。

工程概述

你可以在这里下载初始工程。下载完成后,解压并打开项目文件。当你打开工程后,主要有2个文件。第一个文件是Main.storyboard文件。他包含了一个UIViewController设定好了你需要实现的类。注意UITextField(用来输入地址),UIButton(用来打开地址请求)和UIWebView视图。

打开BrowserViewController.swift文件。这里你会看到为界面组件设定的基本方法。UIViewController实现了UITextFieldDelegate协议,所以当用户点击确认键后你可以调用相应的请求方法。IBAction方法用来响应按钮事件,如果前面是一样的效果。最后,这个sendRequest()方法获得文本框中的字符串,创建一个NSURL请求对象,并调用了loadRequest()方法,UIWebView随后进行加载显示。

当你熟悉该工程后,编译并运行!程序启动后,在地址栏中输入“http://raywenderlich.com”,然后点击"Go"按钮。程序中的UIWeb视图会加载并显示出请求结果。很简单的一个开始。现在,是时候锻炼下你手指的肌肉了,下一步,敲代码!

网络请求拦截

在IOS系统中有一个用于URL请求的类集URL Loading System.在URL Loading System中最核心的类是NSURL。对于网络请求,该类告知应用程序试图去连接到什么主机并指向那台主机的资源。附加的,在NSURLRequest对象中会添加HTTP头,通信内容,等...加载系统提供了一些不同各类的类用来处理请求,最常用的就是NSURLConnection和NSURLSession.

现在,是时候让我们的程序拦截所有的NSURL的请求了。因为你需要创建并实现你自己的NSURLProtocol。

点击File\new\File...选择IOS\Source\Cocoa Touch Class然后 点击Next按钮。在类名中,输入MyURLProtocol,在子类名称中输入NSURLProtocol。确认一下使用的语言为Swift。最后,点击NEXT,当对象框出现后点击CREATE。

打开MyURLProtocol.swift并用下面的内容替换它:

import UIKit



var requestCount = 0



class MyURLProtocol: NSURLProtocol {

    override class func canInitWithRequest(request: NSURLRequest) -> Bool {

    println("Request #\(requestCount++): URL = \(request.URL.absoluteString)")

        return false

    } 

}

每次URL Loading System收到一个请求就会去加载一个URL,它会寻找一个协议注册句柄来处理请求。每个处理程序会告诉系统是否能处理给定的请求通过canInitWithRequest(_:)方法。

该方法中的参数request如果可以进行操作将会被访问。如果方法返回值为true,那么loading system会依懒NSURLProtocol的子类来处理这个请求,并忽略其它的任何句柄。

如果自定义处理中没有可使用的注册句柄,那么URL Loading System将会使用系统默认的方法来进行处理。

如果你要实现一个新的协议,比如foo://,那么你需要检查一下该请求中的URL方案为foo。但在上面的例子中,你只是直接的返回了一个false,那么你的程序不会去处理任何请求。在过一会儿,你的程序就可以来处理这些请求拉!

NOTE:NSURLProtocol是用一个抽象类来实现的。你可以创建的自定义行为的URL协议,但永远不要直接去实例化一个NSURLProtocol对象。

打开AppDelegate.swift并使用下面的代码替换application(_:didFinishLaunchingWithOptions:)方法:

func application(application: UIApplication,

             didFinishLaunchingWithOptions launchOptions: NSDictionary?) -> Bool {

    NSURLProtocol.registerClass(MyURLProtocol)

    return true

}

现在,当你运行你的程序后,它会注册URL Loading System协议。那就意味着他将会有机会来处理每一个请求然后交付给URL Loading System。包括代码直接调用loading system,以及许多系统组件依赖于加载框架的URL,比如UIWebView。

编译并运行工程。在地址栏中输入http://raywenderlich.com网站地址,点击Go按钮,然后查看一下Xcode的控制台输出。现在,对于每一个请求,程序都会输出,URL Loading System会调用你的类如果它可以处理该请求的话。

在控制台输出中,你会看到这些内容:

Request #0: URL = http://raywenderlich.com/

Request #1: URL = http://raywenderlich.com/

Request #2: URL = http://raywenderlich.com/

Request #3: URL = http://raywenderlich.com/

Request #4: URL = http://raywenderlich.com/

Request #5: URL = http://raywenderlich.com/

Request #6: URL = http://www.raywenderlich.com/

Request #7: URL = http://www.raywenderlich.com/

Request #8: URL = http://www.raywenderlich.com/

Request #9: URL = http://www.raywenderlich.com/

Request #10: URL = http://www.raywenderlich.com/

Request #11: URL = http://www.raywenderlich.com/

Request #12: URL = http://raywenderlich.com/

Request #13: URL = http://cdn3.raywenderlich.com/wp-content/themes/raywenderlich/style.min.css?ver=1402962842

Request #14: URL = http://cdn3.raywenderlich.com/wp-content/plugins/swiftype-search/assets/autocomplete.css?ver=3.9.1

Request #15: URL = http://cdn4.raywenderlich.com/wp-content/plugins/videojs-html5-video-player-for-wordpress/plugin-styles.css?ver=3.9.1

Request #16: URL = http://vjs.zencdn.net/4.5/video-js.css?ver=3.9.1

Request #17: URL = http://cdn3.raywenderlich.com/wp-content/themes/raywenderlich/style.min.css?ver=1402962842

...

现在看来,你的类输出了记录请求的URL的字符串的日志并返回了false,也就是说你自定义的类不对处理该请求。但是如果你仔细看一下日志后,你会看到所有的请求都来自UIWebView。他输出了该网站的.html文件和所有的资源文件 ,比如JPEG文件和CSS文件。每次UIWebView需要发送请求的时候,它都会先输出到日志中在它实际发送请求前。计数器变量已经展示出请求的总个数-大概超过500个-原因是该网站中的所有资源。

这是你的实现方式:你的自定义类被每一个URL的请求所调用,下一步,你可以对每一个请求进行一些处理!

自定义URL加载

没有用户说“我喜欢页面一直处于加载状态。”所以,现在你需要确定你的程序真的可以处理这个请求操作。当方法canInitWithRequest(_:)返回true后,那完全表明,你的类的职责就是处理该请求的每个内容。这就意味着你需要得到该请求的数据,然后把它交给URL Loading System。

你该如何获得请求的数据呢?

如果你碰巧实现了一个新的网络协议(比如,添加一个foo://协议),那么这就是你接受实现一个网络协议的严酷乐趣。但是由于你的目标只是插入一个定制高速缓存层,你可以通过使用NSURLConnection获取数据。

实际上你只需要使用NSURLConnection拦截获取到这个请求然后把它传递给本地的URL loading System就可以了。

你的自定义子类NUSRLProtocol实现NSURLProtocolClient协议后可以返回一个数组对象数据。可能有一些困惑一直在你的脑海:NSURLProtocol是一个类,而NSURLProtocolClient是一种协议!

通过客户端端,连接到URL Loading System实现状态改变,响应请求和数据交互。

打开MyURLProtocol.swift然后添加下面的变量到MyURLProtocol类定义的上方。

var connection: NSURLConnection!

下一步,找到canInitWithRequest(_:)方法。将返回值那行的值修改为返回true。

return true

现在添加四个方法:

override class func canonicalRequestForRequest(request: NSURLRequest) -> NSURLRequest {

    return request

}



override class func requestIsCacheEquivalent(aRequest: NSURLRequest,

                               toRequest bRequest: NSURLRequest) -> Bool {

    return super.requestIsCacheEquivalent(aRequest, toRequest:bRequest)

}



override func startLoading() {

    self.connection = NSURLConnection(request: self.request, delegate: self)

}



override func stopLoading() {

    if self.connection != nil {

        self.connection.cancel()

    }

    self.connection = nil

}

上面的内容定义了协议的“标准请求”都包含了什么,至少它应会对相同的请求当返回一个相同标准请求。所以如果两个语义相等(即不一定===)这种内容传入到方法中,那么输出请求也应该语义相等。例如,如果你的自定义URL方案不分大小写,那么你可能会决定,规范化的URL都是小写的。

为了满足这个最低要求,只需要返回请求本身。通常,这是一个可靠的首选解决方案,因为你通常不想更改请求。毕竟,你相信开发人员,对吧?在这里你可能需要做的一件事情可能是通过添加一个头信息来返回一个新的请求。

requestIsCacheEquivalent(_:toRequest:)方法是你花时间来定义当两个自定义URL方案的请求相等时的把这事缓存依据。如果两个请求的内容相同,那么他们应当使用相同的缓存层数据。这是URL Loading System的关注点,内置缓存系统,本教程中暂时不详细说明。在本小节练习中,只需要依靠默认的父类方法来实现。

在加载系统中使用startLoading()和stopLoading() 来通知NSURLProtocol开始或者停止处理请求。在startLoading方法中,将NSURLConnection加载数据进行了实例化。stopLoading方法的存在是为了使URL加载过程可以被中断。上面操作处理了中断当前连接,并将它消毁。

哇!你已经实现了一个有效的NSURLProtocol实例所需要的接口。如果你想了解更多内容的话,查看官方文档这里描述了一个有效的NSURLProtocol的实例可以实现的所有方法。

但量现在你的代码尚未结束!你还需要做的重要工作就是在NSURLConnection中你创建的的回调函数来处理请求。

打开MyURLProtocol.swift然后添加如下方法:

func connection(connection: NSURLConnection!, didReceiveResponse response: NSURLResponse!) {

    self.client!.URLProtocol(self, didReceiveResponse: response, cacheStoragePolicy: .NotAllowed)

}



func connection(connection: NSURLConnection!, didReceiveData data: NSData!) {

    self.client!.URLProtocol(self, didLoadData: data)

}



func connectionDidFinishLoading(connection: NSURLConnection!) {

    self.client!.URLProtocolDidFinishLoading(self)

}



func connection(connection: NSURLConnection!, didFailWithError error: NSError!) {

    self.client!.URLProtocol(self, didFailWithError: error)

}

这些就是NSURLConnection的回调函数。当你使用的NSURLConnection实例进行数据加载,进行响应,有数据变化,完成加载,或者遇到错误的时候就会调用这些方法。在每一种情况下,你都需要在客户端对这些信息进行处理。

所以回顾一下,MyURLProtocol处理并创建自己的NSURLConnection通知处理连接请求。在上面的NSURLConnection回调方法中,协议处理器传送消息给连接URL加载系统。这些信息表示了加载进度,是否完成或者遇到错误。仔细观察你会发现NSURLConnectionDelegate与NSURLProtocolClient之前的消息特征非常相似,它们都是用来处理数据异步加载。当然,还需要注意一下MyURLProtocol 是如何使用它的client 属性来将消息传回到URL Loading System的。

编译并运行工程。当程序启动后,输入之前的网站地址,点击GO按钮。

哇!在浏览器中没有显示出任何内容!如果你在程序运行过程中注意到调试导航,你会发现内容使用已经不受控制。控制台日志应当滚动显示出大量相同的URL请求,是哪里出错了吗?

在控制台日志,你应当会看到下面这种内容消息在无限循环的输出:

Request #0: URL = http://raywenderlich.com/

Request #1: URL = http://raywenderlich.com/

Request #2: URL = http://raywenderlich.com/

Request #3: URL = http://raywenderlich.com/

Request #4: URL = http://raywenderlich.com/

Request #5: URL = http://raywenderlich.com/

Request #6: URL = http://raywenderlich.com/

Request #7: URL = http://raywenderlich.com/

Request #8: URL = http://raywenderlich.com/

Request #9: URL = http://raywenderlich.com/

Request #10: URL = http://raywenderlich.com/

...

Request #1000: URL = http://raywenderlich.com/

Request #1001: URL = http://raywenderlich.com/

你需要回到Xcode并在考虑这个问题前停止程序运行。

解决无限循环标签问题

再次思考一下关于URL LOADING SYSTEM和协议注册,你可能会想到为什么会发生这种现象。当UIWebView将要加载URL的时候,URL Loading System如何可以处理该特殊的请求它就会通知MyURLProtocol。而自定义类中返回了true,表明它可以处理该特殊请求。

所以URL Loading System将会调用startLoading方法创建一个自定义协议的实例。然后你实现的协议创建并连接了它的NSURLConnection。但是它也调用了URL Loading System.猜猜为什么?因为你在canInitWithRequest(_:) 总是返回true,它又创建了另一个新的MyURLProtocol实例。

这个新的实现将导致又创建一个新的实例,接着创建无限数量的实例。这就是你的程序没有加载出任何内容的原因!它占用了大量的内存,只是在控制台输出这个URL。这个欠佳的浏览器被死循环卡住了!你的用户会因为这个问题给予他们机器的损坏而受打击。

很显然在canInitWithRequest(:) 方法中,你不能一直返回true.你需要做一些事情让URL Loading System能够只去处理一次那个请求。解决的办法就是NSURLProtocol接口。找到类中叫做setProperty(:forKey:inRequest:) 的方法,它可以允许你添加给定URL请求的自定义属性。通过这种方法,你可以为该请求添加一个属性来“标记”它,然后浏览器在下次遇到它的时候就不在对它进行处理。

下面是如何打破浏览器死循环的实例。打开MyURLProtocol.swift.然后修改startLoading()和canInitWithRequest(_:)方法为如下代码:

override class func canInitWithRequest(request: NSURLRequest!) -> Bool {

    println("Request #\(requestCount++): URL = \(request.URL.absoluteString)")



    if NSURLProtocol.propertyForKey("MyURLProtocolHandledKey", inRequest: request) != nil {

      return false

    }



    return true

}



override func startLoading() {

    var newRequest = self.request.copy() as NSMutableURLRequest

    NSURLProtocol.setProperty(true, forKey: "MyURLProtocolHandledKey", inRequest: newRequest)



    self.connection = NSURLConnection(request: newRequest, delegate: self)

}

现在startLoading()设定如果给定的请求关联属性值为“MyURLProtocolHandledKey”,那么方法会返回true.也就是说下次再使用给定的NSURLRequest请求调用canInitWithRequest(_:)方法的时候,只有当关联属性被设置的时候才可被访问。

如果被标记过的话,会被设置为true,也就是说下次你就不用在去处理它了。URL Loading System将会从网站中加载数据。当你的MyURLProtocol被实例化后,它将会从NSURLConnectionDelegate接收到回调方法的信息。

编译并运行。当程序运行起来后,网站内容会成功的在网页视图中显示出来。伟大的胜利!控制台的输出内容应当像如下这样:

Request #0: URL = http://raywenderlich.com/

Request #1: URL = http://raywenderlich.com/

Request #2: URL = http://raywenderlich.com/

Request #3: URL = http://raywenderlich.com/

Request #4: URL = http://raywenderlich.com/

Request #5: URL = http://raywenderlich.com/

Request #6: URL = http://raywenderlich.com/

Request #7: URL = http://raywenderlich.com/

Request #8: URL = http://raywenderlich.com/

Request #9: URL = http://www.raywenderlich.com/

Request #10: URL = http://www.raywenderlich.com/

Request #11: URL = http://www.raywenderlich.com/

Request #12: URL = http://raywenderlich.com/

Request #13: URL = http://cdn3.raywenderlich.com/wp-content/themes/raywenderlich/style.min.css?ver=1402962842

Request #14: URL = http://cdn3.raywenderlich.com/wp-content/plugins/swiftype-search/assets/autocomplete.css?ver=3.9.1

Request #15: URL = http://cdn4.raywenderlich.com/wp-content/pluginRse/qvidueeosjts -#h1t6m:l URL = ht5t-pv:i/d/ecodn3.raywenderlich.com/-wppl-acyoenrtent/themes/raywenderlich/-sftoyrl-ew.omridnp.css?vreers=s1/4p0l2u9g6i2n8-4s2t

yles.css?ver=3.9.1

Request #17: URL = http://cdn3.raywenderlich.com/wp-content/themes/raywenderlich/style.min.css?ver=1402962842

Request #18: URL = http://vjs.zencdn.net/4.5/video-js.css?ver=3.9.1

Request #19: URL = http://www.raywenderlich.com/wp-content/plugins/wp-polls/polls-css.css?ver=2.63

Request #20: URL = http://cdn3.raywenderlich.com/wp-content/plugins/swiftype-search/assets/autocomplete.css?ver=3.9.1

Request #21: URL = http://cdn3.raywenderlich.com/wp-content/plugins/swiftype-search/assets/autocomplete.css?ver=3.9.1

Request #22: URL = http://cdn4.raywenderlich.com/wp-content/plugins/powerpress/player.min.js?ver=3.9.1

Request #23: URL = http://cdn4.raywenderlich.com/wp-content/plugins/videojs-html5-video-player-for-wordpress/plugin-styles.css?ver=3.9.1

Request #24: URL = http://cdn4.raywenderlich.com/wp-content/plugins/videojs-html5-video-player-for-wordpress/plugin-styles.css?ver=3.9.1

Request #25: URL = http://cdn3.raywenderlich.com/wp-content/themes/raywenderlich/style.min.css?ver=1402962842

Request #26: URL = http://cdn3.raywenderlich.com/wp-content/plugins/swiftype-search/assets/autocomplete.css?ver=3.9.1

...

你也许会感到惊讶,为何你所做的一切只是让程序回到了刚开始的那种效果。是的,因为你需要为接下来的部分做好充分的准备!现在你已经拥有了URL所有数据的控制权,可以任意进行处理修改。下面开始缓存程序的URL数据。

实现本地缓存

记住本程序的基本需求:给出一个地址请求,程序应当可以一次性的加载网站资源,然后可以对这些内容缓存下来。如果下次在有相同的地址请求,那么程序会直接使用已经缓存好的内容,而不必再次从网络上请求获得。

提示:工程的开始部分已经包括了基本的Core Data模版和堆栈。你需要了解Core Data的详细内容只需要把它当成一个不透明的数据存储;如果你对此感兴趣,请查看苹果官网的 Core Data Programming Guide(Core Data编程指导)

下面来存储程序从网站接收到的信息,无论啥时候只要检索到匹配的就缓存它。打开MyURLProtocol.swift 然后在文件的开始添加导入:

import CoreData

继续,在类的定义中添加下面两个变量:

var mutableData: NSMutableData!

var response: NSURLResponse!

当对服务器的响应进行保存的时候,response变量将会保存你需要的元数据。mutableData变量将会保存connection(_:didReceiveData:)回调方法返回的缓存数据。当连接结束后,你就会缓存完成(数据和元数据)。

然后在类中添加下面的就读:

func saveCachedResponse () {

    println("Saving cached response")



    // 1

    let delegate = UIApplication.sharedApplication().delegate as AppDelegate

    let context = delegate.managedObjectContext!



    // 2

    let cachedResponse = NSEntityDescription.insertNewObjectForEntityForName("CachedURLResponse", inManagedObjectContext: context) as NSManagedObject



    cachedResponse.setValue(self.mutableData, forKey: "data")

    cachedResponse.setValue(self.request.URL.absoluteString, forKey: "url")

    cachedResponse.setValue(NSDate(), forKey: "timestamp")

    cachedResponse.setValue(self.response.MIMEType, forKey: "mimeType")

    cachedResponse.setValue(self.response.textEncodingName, forKey: "encoding")



    // 3

    var error: NSError?

    let success = context.save(&error)

    if !success {

        println("Could not cache the response")

    }

}

下面是方法内容的详细解释:

1。在AppDelegate实例中获得Core Data的NSManagedObjectContext。NSManagedObjectContext是Core Data的接口。

2。创建NSManagedObject的实例,来匹配你在.xcdatamodeld 文件中所见到的数据模型。通过你现有的NSURLResponse和NSMutableData引用来设置它的属性。

3。保存NSManagedObjectContext对象的内容。

现在你已经有办法存储数据了,你需要在某个地方调用该方法。打开MyURLProtocol.swift方法,把NSURLConnection 回调方法中的内容修改如下:

func connection(connection: NSURLConnection!, didReceiveResponse response: NSURLResponse!) {

    self.client!.URLProtocol(self, didReceiveResponse: response, cacheStoragePolicy: .NotAllowed)



    self.response = response

    self.mutableData = NSMutableData()

}



func connection(connection: NSURLConnection!, didReceiveData data: NSData!) {

    self.client!.URLProtocol(self, didLoadData: data)

    self.mutableData.appendData(data)

}



func connectionDidFinishLoading(connection: NSURLConnection!) {

    self.client!.URLProtocolDidFinishLoading(self)

    self.saveCachedResponse()

}

现在程序所获得的数据不在由客户端程序存储,而是由你自己实现的协议类来存储了。编译并运行。程序的结果没有什么大的改变,但现在程序已经可以成功从网站的服务器中接收到数据请求并保存到程序本地的数据库中。

检索缓存请求

终于,是时候开始处理缓存请求并将他们发送给NSURLProtocol的客户端了。打开MyURLProtocol.swift。然后添加下面的方法:

func cachedResponseForCurrentRequest() -> NSManagedObject? {

    // 1

    let delegate = UIApplication.sharedApplication().delegate as AppDelegate

    let context = delegate.managedObjectContext!



    // 2

    let fetchRequest = NSFetchRequest()

    let entity = NSEntityDescription.entityForName("CachedURLResponse", inManagedObjectContext: context)

    fetchRequest.entity = entity



    // 3

    let predicate = NSPredicate(format:"url == %@", self.request.URL.absoluteString!)

    fetchRequest.predicate = predicate



    // 4

    var error: NSError?

    let possibleResult = context.executeFetchRequest(fetchRequest, error: &error) as Array?



    // 5

    if let result = possibleResult {

        if !result.isEmpty {

            return result[0]

        }

    }



    return nil

}

方法解析:

1。获得managedObjectContext,就像在saveCachedResponse()方法中那样。

2。创建一个NSFetchRequest,然后使用它得到叫做CachedURLResponse的实体。这是对象模型中你想检索到的实体。

3。predicate用来将需要获得的请求对象CachedURLResponse对象和URL将要加载的内容进行关联。

4。执行获取到的请求。

5。如果结果不为空,返回第一个结果。

现在来回头看一下startLoading()方法的实现。与其先去从网站上获取一切,不如先检索本地缓存。找到当前实现代码,并使用下面的代码替换:

override func startLoading() {

    // 1

    let possibleCachedResponse = self.cachedResponseForCurrentRequest()

    if let cachedResponse = possibleCachedResponse {

        println("Serving response from cache")



        // 2

        let data = cachedResponse.valueForKey("data") as NSData!

        let mimeType = cachedResponse.valueForKey("mimeType") as String!

        let encoding = cachedResponse.valueForKey("encoding") as String!



        // 3

        let response = NSURLResponse(URL: self.request.URL, MIMEType: mimeType, expectedContentLength: data.length, textEncodingName: encoding)



        // 4

        self.client!.URLProtocol(self, didReceiveResponse: response, cacheStoragePolicy: .NotAllowed)

        self.client!.URLProtocol(self, didLoadData: data)

        self.client!.URLProtocolDidFinishLoading(self)

    } else {

        // 5

        println("Serving response from NSURLConnection")



        var newRequest = self.request.copy() as NSMutableURLRequest

        NSURLProtocol.setProperty(true, forKey: "MyURLProtocolHandledKey", inRequest: newRequest)

        self.connection = NSURLConnection(request: newRequest, delegate: self)

    }

}

方法解析:

1。首先,判断当前请求是否为缓存响应。

2。如果它是缓存响应,那么在缓存对象中得到所有内容数据。

3。创建一个NSURLResponse 对象用来存储数据。

4。将数据返回到客户端。设置客户端的缓存存储策略.NotAllowed ,因为你不想让客户端做任何缓存的相关工作,因为这是你的工作。然后调用URLProtocolDidFinishLoading方法来结束加载。没有任何网络请求--这就是它!

5。如果请求为非缓存响应,那么照常从网络中获得。

再次编译并运行工程。打开一个网站,然后退出应用程序。将你的设置切换到飞行模式(或者,如果你使用的是IOS模拟器,就关闭掉电脑的无线网络连接/拔掉网络)然后再次打开之前的网站。尝试加载之前打开的网站。应当会从缓存中读取并显示出网站所有内容。哇!太棒了!你成功了!

你应当会在控制台中看到许多这样的条目:

Request #22: URL = http://vjs.zencdn.net/4.5/video-js.css?ver=3.9.1

Serving response from cache

这些日志说明请求响应来自你的缓存!

这就是使用本地缓存打开网站。现在你的程序已经能够在网站请求中加载缓存数据和元数据了。用户将会享受到更快的页面加载速度和优越的性能!

什么时候使用NSURLProtocol

你该如何使用NSURLProtocol来使你的程序更加的效率,安全让人瞠目结舌?下面是几个例子:

提供自定义的网络响应请求:

无论你使用什么来制作,比如UIWebView,NSURLConnection 甚至第三方库(比如AFNetworking, MKNetworkKit, 自定义的等,这些都是基于NSURLConnection的)。你可以弄个自定义的,既用于数据也用于元数据。你也许会愿意这样做,如果你是用来进行测试的话。

减少网络请求,使用本地数据:

有时候你可以会认为为程序提供某些需要的数据来进行网络连接是没必要的。NSURLProtocol可以使你的程序在本地缓存或者数据库中进行数据检索。

重定向网络请求:

你曾经望过可以将请求重定向到一个代理服务器——不经过用户来允许而让IOS程序定位?恩,这是可以的!NSURLProtocol 为你提供了你所想要的-控制请求。你可以设定你的程序拦截或者重定向它们到另一个服务器或者代理服务器,或者你任何想想连接的。这是绝对的控制!

改变用户请求代理:

在进行任何网络请求前,你可以决定是否改变它的元数据或者数据。举例来说,你可以改变用户的代理。这对于服务器根据用户代理而改变是很有用的。比如根据用户的使用环境或者客户端语言来返回不同的内容。

使用自定义网络协议:

你也行有自己的网络协议(比如,一些建立于UDP基础上的)。你可以在程序中实现它,或者你也可以选择使用一些其它你喜欢的网络协议库。

不用说,方法是有很多的。在本教程中把所有的都列出来是绝对不可能的(不是不太可能)。你可以在NSURLRequest请求响应前通过重定向NSURLResponse做任何你想要做的。更好的是,创建自定义NSURLResponse。怎么说,你也是个程序员。

NSURLProtocol是很强的,记住它不是一人网络库。它是一个你已经使用库中的附加工具。简而言之,你可以利用NSURLProtocol的优势来完善自己的库。

下一步做什么

这里可以下载到本教程的最终源码。

这个教程包括了一个NSURLProtocol的简单应用,但不要误认为这对缓存来说是一个完整的解决方法。还有许多其它需要实现的内容。实际上,加载系统已经内置了缓存配置,可以去了解一下。本教程的目的就是代你了解一下NSURLProtocol。因为NSURLProtocol 可以修改数据和并且有许多其它的组件,很强大!startLoading方法对你的实现几乎没有什么任何限制。

IETF’s RFC 3986 大多数时候定义URL都像这样“...紧凑的字符序列标识一个抽象的或物理资源...”事实是,URL是自己的迷你语言。它是特定领域语言(DSL)的命名和方向。它也许是世界上特定领域最普及的语言,比如电视机,广播和电视广告,印刷在杂志和在世界各地的商店的招牌上。

NSURLProtocol是一种可以使用各种各样方法的语言。当Twitter想要在苹果系统上实现SPDY protocol协议的时候,HTTP 1.1的优秀继任者,他们就使用了NSURLProtocol。你用它来做什么,决定权在你。NSURLProtocol给可能和灵活性,同时需要一个简单的实现来完成你的目标。

请在本教程的论坛的讨论区随意留下任何的问题或建议。就在右下!

如何使用Swift添加Table View搜索框


原文地址:http://www.raywenderlich.com/76519/add-table-view-search-swift
泰然翻译组:阳光。校对:glory。

Mou icon

关于此次更新的说明:这篇引导是Brad Johnson用原文使用iOS 8和Swift改写的,并在Xcode 6 beta 7上测试通过!原始文章是由Nicolas Martin引导教程团队撰写的。

在移动应用的世界,人们希望能够快捷的获取信息,甚至是立刻就能得到想要的内容!

iOS用户希望能够灵活的获取他们想要的信息,并且能以最快的速度将信息显示出来。此外,他们也希望能够以更简单和直观的方式来获取信息。当然,这是一个非常高的目标!

自从滑动屏幕变得自然和快速,许多使用UIKit为基础的应用使用UITableView来浏览数据。但是对于大量,巨量的数据如何进行筛选呢?对于大数据集,滚动有大量数据的列表十分缓慢,这也会使人厌烦-所以允许用户使用特定的分类是十分重要的。幸运的是,UIKit有一个UISearchBar 类,可以无缝的集成在table view中,可以让我们快速的过滤信息。

在这篇引导教程中,你将在一个基本的table view上创建一个可以搜索糖果的应用。你将给table view增加搜索的能力,包括动态过滤和预设范围。最后,你将学会如何使你的应用界面更友好,并且满足你用户急切的需求!

想查找一些糖果的搜索结果!继续读下去吧!

提示:在写这篇引导时,iOS 8还是个测试项目,所以我们不能截图。所有的截图均出自iOS 7而且我们在却定可以使用之前尽量不使用Xcode 6的截图。

让我们开始吧

在Xcode中,选择 File\New\Project… 创建一个新项目。选择 Single View ApplicationNext 。将项目命名为 CandySearch 确保 Language 设置为 Swift 并且 Devices 设置为 iPhone 。点击完成,选择你想储存的位置,然后点击 Create

首先将默认的文件清理掉,这样你就可以真正的从头开始。在 Project Navigator中,选择 ViewController.swift,右键点击,选择删除,然后选择 Move to Trash。然后打开 Main.storyboard,选择唯一的一个view controller然后删掉他。现在你有一个空的storyboard,可以在你的应用中添加主屏幕了。

Object Browser (边框控制条的右下部分)拖出 Navigation Controller 以将iOS内置的导航逻辑添加到项目中。这样可以在storyboard中创建两个view controllers - navigation controller和table view controller,这些将作为应用的初始视图。

当用户选择列表中的某一项时,你需要一个视图控件来显示详细的内容。从Table View Controller中拖拽,一直到那个新的view controller释放,并且在弹出的菜单中选择 show 来作为manual segue的选项。

设置糖果类

下一步你将创建一个用来显示糖果的数据结构,保存像策略和名称类的信息。在 CandySearch 文件夹上点击右键,然后选择 New File… 。选择 iOS \ Source \ Swift File 然后点击下一步。将此文件命名为 Candy.swift 。打开文件,然后添加如下内容:

struct Candy {

  let category : String

  let name : String

}

这个结构有两个属性:糖果的策略和名称。当你的用户在应用中搜索糖果时,会引用名称属性对用户搜索的字符串进行比较。你文章稍后将会发现策略字符串在你实现分类条时有多么重要。

你不需要在这里添加自己的初始化,你能得到一个自动生成的机制。默认的,初始化参数将对属性进行配置。在下一个部分中你将看到如何创建 candy 实例。

现在你已经准备好设置 UITableView 以使你的 UISearchBar 能够过滤信息!

连接Table View

下一步你将设置 UITableView 让他和 UISearchBar 一起工作。右键点击 CandySearch 文件夹并且选择 New File… 。选择 iOS \ Source \ Cocoa Touch Class 点击下一步。类命名为 CandyTableViewController ,父类设置为 UITableViewController 并且设置语言为 Swift

开始的时候我们需要添加一个数组。打开 CandyTableViewController.swift 并且增加如下代码:

var candies = [Candy]()

candies 数组将会管理所有不同的 Candy 对象,以便用户搜索。说到这里,是时候创建你的糖果了!在这篇引导中,你只需要创建一个数量区间,用来说明搜索条如何工作;在制作应用的过程中,你会发现你有成千上万的条目需要检索。但是不论你的应用程序有多少条目,搜索的方法都是相同的。具有可伸缩性是最好的!为了能够布置好你的 candies 数组,重写 viewDidLoad 如下:

override func viewDidLoad() {

  super.viewDidLoad()



  // 向candyArray中添加简单的数据

  self.candies = [Candy(category:"Chocolate", name:"chocolate Bar"),

    Candy(category:"Chocolate", name:"chocolate Chip"),

    Candy(category:"Chocolate", name:"dark chocolate"),

    Candy(category:"Hard", name:"lollipop"),

    Candy(category:"Hard", name:"candy cane"),

    Candy(category:"Hard", name:"jaw breaker"),

    Candy(category:"Other", name:"caramel"),

    Candy(category:"Other", name:"sour chew"),

    Candy(category:"Other", name:"gummi bear")]



  // 刷新table

  self.tableView.reloadData()

}

这段代码并没做太多的设置,但他做了一些重要设置。首先它填充了9个有不同名称和类别的糖果。你将在填充数组后使用它。然后你让tableView重新载入数据。你必须这么做,以确保所有的糖果信息都被显示。

下一步,你将添加控制表视图本身的功能。实现 tableView(_:numberOfRowsInSection:) 如下:

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

  return self.candies.count

}

这里简单的告诉 tableView ,应该包含许多行,而这个数量是你在 candies 数组中确定的。

现在 tableView 知道了需要准备多少行,你需要告诉他每行需要放什么内容。实现 tableView(_:cellForRowAtIndexPath:):

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

  // 在tableview中查询一个条目,如果没有创建一个。

  let cell = self.tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell



  // 从我们的糖果数组中获得相应的内容

  let candy = self.candies[indexPath.row]



  // 设置条目

  cell.textLabel!.text = candy.name

  cell.accessoryType = UITableViewCellAccessoryType.DisclosureIndicator



  return cell

}

这个方法主要分三个部分。首先,你使用 indexPath 选出条目。然后检索 candies 数组,根据 indexPath 决定哪个 Candy 需要提取,然后使用 Candy 对象实现 UITableViewCell 实例。

注意:如果你在之前已经使用过Table Views,你也许会怀疑为什么没有实现 heightForRowAtIndexPath 。在iOS8中,这个方法已经不再支持;操作系统在运行时自动决定元素的大小。这岂不是更好吗?:]当然,如果你打算让你的应用支持早期iOS版本,你还是需要实现这个方法。

现在你做的差不多了!还需要一个步骤你就可以看到糖果列表了。你需要实现故事板中的所有代码。首先,打开 Main.storyboard 并且选择 Root View Controller 。在 Identity Inspector 中(右边栏中顶部第三个选项卡),设置 ClassCandyTableViewController 。这将会告诉操作系统当你的控件呈现在屏幕时加载你指定的关联类。好了,现在,双击 CandySearch 根视图控件的标题,然后修改他,这可以给你的用户一些关于他们能在软件中做什么的提示。

最后,你需要让你的操作系统知道你的代码是用于做什么的。你需要注意当你修改故事板的之前,一个默认的元素已经存在于你的table view中。选择这个元素(通常被命名为"Cell")并且打开Attributes Inspector(位置在Identity Inspector的右边)。修改 IdentifierCell 。这将重新匹配你早期代码中元素的标示符。

保存你的修改并运行。现在你有了一个正在工作的列表视图!许多的糖果。。。呈现这些只花了极短的时间!接下来我们需要一个UISearchBar!

Mou icon

设置UISearchBar

如果你查看过 UISearchBar 的文档,你将发现作者非常的懒惰。作者没有提供任何具体的搜索接口!这个类只是提供了一些用户所期望的接口。他更像是一个中级的管理类;擅长将任务委托给他人。

UISearchBar 这个类使用一个代理接口进行通讯,使你的应用知道用户做了些什么。所有的字符串匹配和其他的操作都需要你自己编写完成。尽管这个看起来有点可怕(有点不公平!),但写自定义的搜索方法给了你的应用完善的控制返回结果的体验。你的用户将会欣赏你提供的搜索结果 - 并且也会更快速。

打开 Main.storyboard 并且拖放一个 Search Bar and Search Display Controller 对象到视图控制器。注意 - 这里和 Search Bar 对象有一些不同,他也是有效的。调整搜索条的位置。

不能明白搜索显示控制器的意思?根据苹果自己的文档,一个搜索显示控制器是“管理搜索条的显示,具体来说是一个显示另一个视图控制器搜索结果的table view”。

这意味着区别于你刚才设置的, 搜索控件将有他自己的table view用来显示结果。简单的来说,先在显示控件中增加上层操控,然后把一个搜索中生成的过滤数据放入一个单独的视图控制器,这一点你丝毫不用自己操心。:]

UISearchBar中Attributes inspector的选项

在故事板中,花些时间重新查看搜索条对象的可用属性。你也许不会全部使用到,但使用一个新的UIKit组件之前了解Attributes Inspector是值得的。

  • Text:这将改变呈现在搜索条上的字符串。如果你的应用需要使用默认值,你是不需要修改这一属性的。

  • Placeholder: 这正是你所期望的 - 他允许你放置一个浅灰色的文本以高速你的用户搜索条可以做什么。在这个糖果应用中,我们使用 "搜索你的糖果"。

  • Prompt: 这个文本将会直接显示在搜索条的上方。这有利于对复杂的搜索机制进行说明。(但在这个应用中,Placeholder应该更显而易懂!)

  • Search Style, Bar Style, Translucent, Tint, Background and Scope Bar Images: 这些选项允许你自定义搜索条的呈现。这些选项是为了是你的UISearchBar和UINavigationBar看起来更和谐。

  • Search Text and Background Positions: 这些选项允许你添加文本与搜索框的偏移量。

  • Show Search Results Button: 在搜索条的右边提供一个按钮为了执行像显示最近搜索或者显示最后搜索的方法。搜索栏与这个按钮的交互是通过代理方法来实现的。

  • Show Bookmarks Button: 在搜索栏右边显示标准的蓝色椭圆形书签。这将有一个用户希望保存的搜索书签。像结果按钮一样,也是通过代理来实现的。

  • Show Cancel Button: 这个按钮允许用户关闭搜索栏单独生成的视图控制器。如果这个选项没有选中,搜索栏会在隐藏取消按钮并在搜索模式中自动显示。

  • Shows Scope Bar & Scope Titles: 分类条允许用户用特定的策略进行搜索。比如在音乐应用中,这个控件条也许会显示艺术家,专辑或者流派。对于当前项目,保持这个选项没有被选中;你将实现自己的分类标签。

  • Capitalize, Correction, Keyboard, etc.: 这些选项都借用了UITextField的内容,可以让你改变搜索条的行为。例如,如果用户想搜索专业名词或者是人名,这是就可以关闭自动修正,以避免不必要的麻烦。在本教程中,糖果的名字都是普通的名称,所以保持默认选项就可以了。

注意:了解一些有用的选项可以为你的开发节省许多时间。所以对于一个iOS未来的开发者,是要经常花时间搜索可用的资源。

UISearchBarDelegate和过滤器

设置了故事板以后,你需要写一些代码来让搜索条工作。设置 CandyTableViewController 类响应搜索条,这需要实现一些接口。打开 CandyTableViewController.swift 并且将类的声明替换为下列代码:

class CandyTableViewController : UITableViewController, UISearchBarDelegate, UISearchDisplayDelegate {

UISearchBarDelegate 定义了搜索的行为和响应方式,然后 UISearchDisplayDelegate 定义了搜索条的外观。

下一步,在类中添加如下属性:

var filteredCandies = [Candy]()

这个数组将保存过滤后的数据。

下一步,添加如下辅助方法到类里面:

func filterContentForSearchText(searchText: String) {

  // 使用过滤方法过滤数组

  self.filteredCandies = self.candies.filter({( candy: Candy) -> Bool in

    let categoryMatch = (scope == "All") || (candy.category == scope)

    let stringMatch = candy.name.rangeOfString(searchText)

    return categoryMatch && (stringMatch != nil)

  })

}

这个方法将使用 searchText (也就是你的搜索字符串) 过滤 candies ,然后将结果放入 filteredCandies 。Swift的数组有一个叫做filter()的方法,他使用了一个闭合表达式作为他唯一的参数。

如果你有一些Objective-C的经验,仔细看一下闭合表达式在Swift中的表现形式。闭包是一个自包含的功能块。他们被称作闭包,是因为他们可以使用一段上下文捕获或储存任意变量或常量的引用,这被称为关闭。下面是一个闭合表达式的语法:

{(parameters) -> (return type) in expression statements}

在这个示例中,过滤方法使用闭合表达式来区分数组中的每一个元素。闭包表达式的参数用于对个别的元素进行排序。闭包表达式返回一个bool值,如果元素存在于过滤的数组中则返回true,如果返回false则说明没有包含在数组内。如果你仔细查看文档中的方法,你会注意到他使用了一个像 的类型。这是一个通用类型,这以为这他可以是任何类型。直到你使用闭包表达式定义自己的过滤规则,这个过滤方法可以使用在任何类型的过滤上。闭包表达式的参数也是一个 T 类型,其中的元素就是等待被过滤的。在你的代码中我们使用了 candy: Candy ,然后你了解了这个数组是被 Candy 装满的:

rangeOfString() 用于检查是否字符串包含索要查找的字符串。如果是,则返回true,表明当前的糖果包含在过滤的数组中;如果返回false则不包含。

下一步,在类中添加如下代码:

func searchDisplayController(controller: UISearchDisplayController!, shouldReloadTableForSearchString searchString: String!) -> Bool {

  self.filterContentForSearchText(searchString)

  return true

}



func searchDisplayController(controller: UISearchDisplayController!, shouldReloadTableForSearchScope searchOption: Int) -> Bool {

  self.filterContentForSearchText(self.searchDisplayController!.searchBar.text)

  return true

}

这两个方法是 UISearchDisplayControllerDelegate 的一部分。当用户输入一个搜索队列时中他们会调用过滤方法。当用户更改搜索的字符串时,第一种方法将被调用。第二种方法将用于操作分类条的输入。你还没有添加分类条在这篇引导中,但是你最好添加 UISearchBarDelegate 方法,稍后会使用到。

运行应用;你会注意到,使用搜索条还是没有任何的过滤效果!这到底是怎么回事?这很简单,因为你还没有添加代码,让 tableView 知道什么时候使用过滤过的数据。你需要修改 numberOfRowsInSectioncellForRowAtIndexPath 方法。使用如下方法替换方法中的内容:

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

  if tableView == self.searchDisplayController!.searchResultsTableView {

    return self.filteredCandies.count

  } else {

    return self.candies.count

  }

}



override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

  // 从table view中查找一个可修改的元素,如果没有就创建一个

  let cell = self.tableView.dequeueReusableCellWithIdentifier("Cell") as UITableViewCell



  var candy : Candy

  // 检查标准table和搜索结果table是否正常显示,然后从candy数据集查找需要的对象

  if tableView == self.searchDisplayController!.searchResultsTableView {

    candy = filteredCandies[indexPath.row]

  } else {

    candy = candies[indexPath.row]

  }



  // 设置元素

  cell.textLabel!.text = candy.name

  cell.accessoryType = UITableViewCellAccessoryType.DisclosureIndicator



  return cell

}

代码测试了当前的 tableView 是否为搜索搜索table或者标准table。如果他确实是搜索table,则数据从 filteredCandies 数组中获取。否则,数据从全部的项目中获取。回想一下之前的内容,搜索显示控制器自动处理结果table的显示和隐藏,所以你所有的代码不得不提供当前的数据(过滤或者没有过滤),这依赖于当前的视图。

运行应用。你现在已经得到了搜索条的方法,过滤主table的行!哇哈!使用以下,看看用户如何搜索各种糖果。

Mou icon

你可能已经注意到 tableView(numberOfRowsInSection:) 中的if/else逻辑重用了很多次。这在处理显示控制器时是很重要的,如果缺少可能会造成错误,这种错误很难被排查。记住过滤的结果不会出现在主table的相同table中。他们实际上是完全独立的表视图,但苹果公司使用了无缝方式设计了他们 - 这样做会让开发者们很困惑!

向详细内容视图发送数据

当向详细视图控件中发送消息时,你需要确定view controller知道哪个用户正在使用的是哪一个table view:完整的列表,或者搜索过的列表。这里的代码和 tableView(_:numberOfRowsInSection:)tableView(_:cellForRowAtIndexPath:) 的地方类似。还是在CandyTableViewController.swift,添加如下代码:

override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {

  self.performSegueWithIdentifier("candyDetail", sender: tableView)

}



override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {

  if segue.identifier == "candyDetail" {

    let candyDetailViewController = segue.destinationViewController as UIViewController

    if sender as UITableView == self.searchDisplayController!.searchResultsTableView {

      let indexPath = self.searchDisplayController!.searchResultsTableView.indexPathForSelectedRow()!

      let destinationTitle = self.filteredCandies[indexPath.row].name

      candyDetailViewController.title = destinationTitle

    } else {

      let indexPath = self.tableView.indexPathForSelectedRow()!

      let destinationTitle = self.candies[indexPath.row].name

      candyDetailViewController.title = destinationTitle

    }

  }

}

打开故事板并且确保从 Candy Table View ControllerDetail View 的segue拥有candyDetail标示符。运行代码,查看应用如何导航从主table或者搜索tabel过度到详细视图的。

创建一个过滤条件选项卡

如果你想给你的用户搜索的另一种方式,你可以添加一个分类条,以过按物品类别进行过滤。过滤的内容是你在 candyArray 创建之前在 Candy 中注册的:巧克力,硬巧克力,或其他。

首先,在故事板中设置分类条。切换到 CandySearch View Controller 然后选择搜索条。在In the attributes inspector,选择 Shows Scope Bar 。修改标题为:“全部”, “巧克力”, “硬巧克力” ,and “其他”。(你可以使用 + 按钮同时添加多个分类,双击可以修改分类)

下一步,修改CandyTableViewController.swift中的filterContentForSearchText,以为账户添加新的分类。用如下内容替换当前方法:

func filterContentForSearchText(searchText: String, scope: String = "All") {

  self.filteredCandies = self.candies.filter({( candy : Candy) -> Bool in

  var categoryMatch = (scope == "All") || (candy.category == scope)

  var stringMatch = candy.name.rangeOfString(searchText)

  return categoryMatch && (stringMatch != nil)

  })

}

这个方法现在获取一个 scope 变量(默认值为全部)。这个方法默认选取所有的分类,并且如果有策略和字符串都匹配的则返回true(或者手动设置分类为全部)。

现在你可以改变这个方法,你需要改变两个 searchDisplayController 方法以改变范围:

func searchDisplayController(controller: UISearchDisplayController!, shouldReloadTableForSearchString searchString: String!) -> Bool {

  let scopes = self.searchDisplayController.searchBar.scopeButtonTitles as [String]

  let selectedScope = scopes[self.searchDisplayController.searchBar.selectedScopeButtonIndex] as String

  self.filterContentForSearchText(searchString, scope: selectedScope)

  return true

}



func searchDisplayController(controller: UISearchDisplayController!, shouldReloadTableForSearchScope searchOption: Int) -> Bool {

  let scope = self.searchDisplayController.searchBar.scopeButtonTitles as [String]

  self.filterContentForSearchText(self.searchDisplayController.searchBar.text, scope: scope[searchOption])

  return true

}

所有的方法将分类从搜索条发送到过滤方法。运行。你将会看到如下内容:

Mou icon

接下来你应该关注哪些东西?

恭喜你 - 你现在已经完成了一个可以在主table view中直接搜索内容的应用了!这是一个实例,其中包含引导中所有的代码。

Table views在应用中被广泛使用,因为他为触摸控制搜索提供了良好的支持。对于UISearchBar和UISearchDisplayController,iOS提供了大量的创造性方法,所以没有理由不去使用。当人们发现控件没有搜索选项是,他们肯定不会乐于使用他们!

Mou icon

不要让这样的事情发生在你的用户身上。始终保持提供搜索方法。


我们希望你能在你的table view应用中添加搜索的能力。如果你有什么问题,可以尽情的在下面的讨论区留言!

如何在Swift中创建自定义控件


原文地址:http://www.raywenderlich.com/76433/how-to-make-a-custom-control-swift
泰然翻译组:阳光新鲜。校对:glory。

Mou icon

更新通知:这篇引导教程由Mikael Konutgan使用iOS 8和Swift语言重新制作,在Xcode6和7上测试通过。原始教程是由Colin Eberhardt团队制作的。

用户界面控件是许多应用的重要组成部分。使用这些控件,可以让用户查看应用的内容或与他们的应用进行交互。苹果提供了一个控件集,像UITextField, UIButtonUISwitch。灵活使用这些工具箱中已经存在的控件,可以让你创建各种各样的用户界面。

但是,有的时候你可能需要做一些与众不同的事情;库中的控件已经不够用了。

自定义控件只不过是你自己创建的控件;也就是一个不来自于UIKit框架的控件。自定义控件,就像标准控件一样,应该具有通用和可定制的特性。而且,你会找到一个积极且充满活力的开发者社区喜欢分享他们的自定义控件。

在这个教程里,你将实现你自己的RangeSlider控件。这个控件就像一个双头滑块,可以用来选择最小和最大值。你会接触到关于如何扩展已经存在控件的思想,然后设计和实现你的控件API,甚至在开发者社区中共享你的控件。

开始我们的自定义之旅吧!

注意:在我们制作教程时,iOS 8仍处於测试阶段,所以我们不能给他截图。所有的截图都取自之前的iOS版本,但你的实际运行结果是没有差别的。

开始的工作

话说你开发了一个可以搜索房地产销售信息的应用。这个应用允许用户过滤搜索结果,来将搜索结果控制在一定的价格范围内。

你可以提供一个呈现出一对UISlider控件的交互界面,一个设置最低价格,另一个设置最高价格。尽管如此,这个交互界面不能真正帮助使用者形象的了解价格范围。如果设计成一个滑动条和连个滑动钮,这样可以更好的表达出他们想要搜索的价格高低范围。

你可以子类化UIView来创建一个范围滑动器,并且创建一个定制的视图来显示价格区间。这对于你的app是好用的,但如果将他用在其他类型的app上那就是一种煎熬。

让这个新的自定义组件变的更通用将会是个好的想法,这样他就可以在任何需要他的环境内使用。这是自定义控件的精髓所在。

启动Xcode。选择File/New/Project,然后选择iOS/Application/Single View Application模板,点击Next。在接下来的屏幕里,在product name中输入CustomSliderExample,选择你想使用的Organization NameOrganization Identifier,确定Swift语言被选中,iPhone被选为DeviceUse Core Data没有被选中。

最后选择一个保存路径,然后点击Create

我们要做的第一个决策就是通过对一个已经存在的控件进行继承或扩展来实现新的控件。

为了在现有的UI上使用,你的控件必须继承于UIView

如果你查看苹果的UIKit参考手册,你会看到许多的控件像 UILabelUIWebView 是直接继承于UIView的。尽管如此,还是有一些棘手的事情,像 UIButtonUISwitch 是继承于 UIControl ,像如下的层级关系所示:

Mou icon

注意:如果你想查看一个完整的UI组件类层级示意图,请阅读UIKit Framework Reference

UIControl 使用的是target-action pattern机制,这是一种用于通知用户信息改变的机制。 UIControl 也具有很多的属性来表示当前的控制状态。在这个自定义控件中将使用target-action pattern,所以 UIControl 将担当重要的起始点。

在项目导航中右击CustomSliderExample组然后选择New File…,选择iOS/Source/Cocoa Touch Class模板点击Next。类取名为RangeSlider,在Subclass of中输入 UIControl 并且确保语言选择为Swift。点击下一步然后选择Create以使用默认位置来保存新类。

尽管写代码是件漂亮的事,你可能想看看你的控件在实际屏幕上显示的效果来了解项目的进展!在你写其他代码之前,你可以先将控件添加到view controller中以便我们随时查看控件制作的进展程度。

打开ViewController.swift替换如下:

import UIKit



class ViewController: UIViewController {

    let rangeSlider = RangeSlider(frame: CGRectZero)



    override func viewDidLoad() {

        super.viewDidLoad()



        rangeSlider.backgroundColor = UIColor.redColor()

        view.addSubview(rangeSlider)

    }



    override func viewDidLayoutSubviews() {

        let margin: CGFloat = 20.0

        let width = view.bounds.width - 2.0 * margin

        rangeSlider.frame = CGRect(x: margin, y: margin + topLayoutGuide.length,

            width: width, height: 31.0)

    }

}

上面的代码简单的创建了一个指定过大小的全新控件的实例并且将实例添加到了视图内。控件的背景颜色已经被设置成红色这样可以使其和应用的背景形成鲜明的对比。如果你不将背景设置成红色,你的控件将不容易被找到,你将怀疑你的控件去哪了!:]

运行app;你将看到和下图一样的画面:

Mou icon

在你向控件中添加可视元素之前,你需要几个属性,以便跟踪控件当前所设置的信息。这将形成你的控件的应用程序接口,简称API。

注意:你的控件的API定义了你打算开放给其他开发人员的方法和属性。你将在这篇文章的稍后学习一些API设计技巧 - 所以,现在就应该注意这些!

为控件添加默认的属性

打开RangeSlider.swift替换如下代码:

import UIKit



class RangeSlider: UIControl {

    var minimumValue = 0.0

    var maximumValue = 1.0

    var lowerValue = 0.2

    var upperValue = 0.8

}

这四个属性足够你描述控件的状态了,最大最小值用于表示范围,这些值由上限下限值进行调整。

设计好的控件需要一些默认的属性值,否则你的控件会看起来有一些怪!如果你照做的话会好很多。

现在我们可以设计控件上的交互元素了;也就是说,滑块以表示最大最小值,并且可以在滑条上滑动。

Images vs. CoreGraphics

你可以使用以下两种方式来在屏幕上绘制你的控件:

  1. Images – 为控件创建图片来表示多种多样的形式

  2. CoreGraphics – 使用层和CoreGraphics混合使用来渲染控件

每个技术都有优缺点,如下所示:

Images — 使用图片来创建控件可能是最容易做到的事 - 只要你知道如何绘制!:]如果你需要让你的开发队友能够修改控件的外观,你只需要将图片设为 UIImage 的开放属性就可以了。

使用图片可以使开发人员更灵活的使用控件。开发这可以修改每一个像素和没一个你想要表现的细节,但这需要很好的平面设计技能 - 因为在代码中修改图片很困难。

Core Graphics — 使用Core Graphics构建以为这你需要自己编写渲染代码,这需要花费更多的努力。但是,这种技术允许你创建更加灵活的API。

使用Core Graphics,你可以用参数表示控件的每一个特征,不如颜色,边框宽度,和弯曲度 - 几乎所有的视觉元素都可以直接绘制!这种方法允许开发人员对控件进行完整的定制以适应他们的需要。

这个教程将使用第二种技术 - 使用Core Graphics进行渲染。

注意:有趣的是,苹果公司倾向于在控件中直接使用图片。这可能是因为他们知道控件的实际大小,且不需要做太多的自定义修改。毕竟,他们希望的结果是所有的应用看起来都差不多。

打开RangeSlider.swiftimport UIKit 的下面加入以下代码:

import QuartzCore

在我们刚才添加代码的下部添加如下代码:

let trackLayer = CALayer()

let lowerThumbLayer = CALayer()

let upperThumbLayer = CALayer()



var thumbWidth: CGFloat {

    return CGFloat(bounds.height)

}

trackLayer, lowerThumbLayer, 和 upperThumbLayer 这三个层将用于滑条控件的渲染。 thumbWidth 将在布局属性中被使用。

接下来是添加一些控件自身的默认属性。

在RangeSlider类中,添加一个初始化方法和其他的辅助方法:

override init(frame: CGRect) {

    super.init(frame: frame)



    trackLayer.backgroundColor = UIColor.blueColor().CGColor

    layer.addSublayer(trackLayer)



    lowerThumbLayer.backgroundColor = UIColor.greenColor().CGColor

    layer.addSublayer(lowerThumbLayer)



    upperThumbLayer.backgroundColor = UIColor.greenColor().CGColor

    layer.addSublayer(upperThumbLayer)



    updateLayerFrames()

}



required init(coder: NSCoder) {

    super.init(coder: coder)

}



func updateLayerFrames() {

    trackLayer.frame = bounds.rectByInsetting(dx: 0.0, dy: bounds.height / 3)

    trackLayer.setNeedsDisplay()



    let lowerThumbCenter = CGFloat(positionForValue(lowerValue))



    lowerThumbLayer.frame = CGRect(x: lowerThumbCenter - thumbWidth / 2.0, y: 0.0,

      width: thumbWidth, height: thumbWidth)

    lowerThumbLayer.setNeedsDisplay()



    let upperThumbCenter = CGFloat(positionForValue(upperValue))

    upperThumbLayer.frame = CGRect(x: upperThumbCenter - thumbWidth / 2.0, y: 0.0,

        width: thumbWidth, height: thumbWidth)

    upperThumbLayer.setNeedsDisplay()

}



func positionForValue(value: Double) -> Double {

    let widthDouble = Double(thumbWidth)

    return Double(bounds.width - thumbWidth) * (value - minimumValue) /

        (maximumValue - minimumValue) + Double(thumbWidth / 2.0)

}

初始化方法简单的创建了三个层,然后作为子层添加到控件层中,然后 updateLayerFrames ,这样,层的布局就能自动选择合适的大小了!:]

最后,positionForValue 根据一个值计算出控件在屏幕中的位置,并且使用一个简单的比例缩放最大最小值,计算控件中的位置。

接下来,重写frame方法,实现一个观察者属性,添加到RangeSlider.swift后面:

override var frame: CGRect {

    didSet {

        updateLayerFrames()

    }

}

这个观察者会在布局发生改变时更新层的布局位置。这是必须的,但就像在ViewController.swift中一样,在初始化中被执行,但这并不是最终的布局。

运行应用;你的滑动条已经成形了!他看起来应该如下所示:

Mou icon

记住,红色的是整个控件的背景。蓝色是轨道,绿色是两个滑动的钮,对应最大最小值。

你的控件已经可以显示了,但几乎苹果上所有的控件都可以和用户进行交互。

对于你的控件,用户应该可以拖拽两个滑块来调解范围。你将控制他们进行交互,并且开放更新UI和更新属性的接口。

添加交互逻辑

交互必须储存滑块拖动的位置,并在UI中显示出来。控件层是最适合放这些逻辑的地方。

像以前一样,在Xcode中创建一个新Cocoa Touch Class,将他命名为 RangeSliderThumbLayer ,继承于 CALayer

将以下代码添加到RangeSliderThumbLayer.swift中:

import UIKit

import QuartzCore



class RangeSliderThumbLayer: CALayer {

    var highlighted = false

    weak var rangeSlider: RangeSlider?

}

这里添加了两个属性:一个用于表明滑块是否高亮显示,另一个是父控件的引用。自从RangeSlider有两个滑块层,他们需要持有两个背景控件的弱引用,以避免被内存管理系统回收。

打开RangeSlider.swift修改 lowerThumbLayerupperThumbLayer 属性的类型,替换成如下代码。

let lowerThumbLayer = RangeSliderThumbLayer()

let upperThumbLayer = RangeSliderThumbLayer()

还是在RangeSlider.swift,找到 init 添加如下代码:

lowerThumbLayer.rangeSlider = self

upperThumbLayer.rangeSlider = self

上面的代码设置了自己的父节点弱引用 rangeSlider 属性。

运行项目;检查应用是否按预想的方式执行。

现在你已经使用 RangeSliderThumbLayer 创建了滑块层,你需要添加让用户可以拖动滑块移动的功能。

添加触摸控制

打开RangeSlider.swift在属性区域添加如下属性:

var previousLocation = CGPoint()

这个属性将被用于追踪触摸位置。

如何跟踪变化的触摸点和释放事件?

UIControl 提供了跟踪触摸点的多种方法。继承于 UIControl 的类可以重写这些方法并在里面增加自己的交互代码。

在你的自定义控件中,你将重写三个方法 UIControl: beginTrackingWithTouch, continueTrackingWithTouchendTrackingWithTouch

添加如下方法在RangeSlider.swift

override func beginTrackingWithTouch(touch: UITouch, withEvent event: UIEvent) -> Bool {

    previousLocation = touch.locationInView(self)



    // Hit test the thumb layers

    if lowerThumbLayer.frame.contains(previousLocation) {

        lowerThumbLayer.highlighted = true

    } else if upperThumbLayer.frame.contains(previousLocation) {

        upperThumbLayer.highlighted = true

    }



    return lowerThumbLayer.highlighted || upperThumbLayer.highlighted

}

这个方法将在用户第一次触摸屏幕时被调用。

首先,他将触摸事件转换为控件的坐标系。接下来,他将检查两个滑块已查看时候触摸到了滑块上。上面方法的返回值通知 UIControl 父类哪个后来的触点需要被跟踪。

如果滑块是高亮的,则跟踪轨迹事件。

现在你有一个初始的触摸事件,你将需要在手指在屏幕上移动是做一些处理。

添加如下方法到RangeSlider.swift

func boundValue(value: Double, toLowerValue lowerValue: Double, upperValue: Double) -> Double {

    return min(max(value, lowerValue), upperValue)

}



override func continueTrackingWithTouch(touch: UITouch, withEvent event: UIEvent) -> Bool {

    let location = touch.locationInView(self)



    // 1. Determine by how much the user has dragged

    let deltaLocation = Double(location.x - previousLocation.x)

    let deltaValue = (maximumValue - minimumValue) * deltaLocation / Double(bounds.width - bounds.height)



    previousLocation = location



    // 2. Update the values

    if lowerThumbLayer.highlighted {

        lowerValue += deltaValue

        lowerValue = boundValue(lowerValue, toLowerValue: minimumValue, upperValue: upperValue)

    } else if upperThumbLayer.highlighted {

        upperValue += deltaValue

        upperValue = boundValue(upperValue, toLowerValue: lowerValue, upperValue: maximumValue)

    }



    // 3. Update the UI

    CATransaction.begin()

    CATransaction.setDisableActions(true)



    updateLayerFrames()



    CATransaction.commit()



    return true

}

boundValue 会将值限制在一定范围内。使用这个助手方法比读一大堆 min / max 方便多了。

这里是 continueTrackingWithTouch 的几个关键点,这里是详细注释:

  1. 首先你计算一个位置的变化量,计算用户的手指移动的像素。然后你依据控件的最大最小值来缩放你的移动量。

  2. 这里通过手指一动哪个滑块确定改变最大值还是最小值。

  3. 这个部分设置了 CATransaction 类的 disabledActions 标志。这将确保改变立刻生效,而不使用动画。最后调用 updateLayerFrames 设置滑钮到当前位置。

你已经编写了移动滑钮的代码 - 但你还需要编写触摸和拖拽的处理事件。

添加如下方法到RangeSlider.swift

override func endTrackingWithTouch(touch: UITouch, withEvent event: UIEvent) {

    lowerThumbLayer.highlighted = false

    upperThumbLayer.highlighted = false

}

上面的代码将恢复滑钮到非高亮显示的状态。

运行项目,然后试用你闪亮的滑动条!你应该可以随意的拽动绿色的按钮。

Mou icon

请注意,在你拖动滑条时,你可以将手指拖动到空间范围以外的地方,然后返回控件位置依然不会失去对按钮的控制。这是一个对小屏低分辨率设备尤为重要的特性 - 或者说对于广大手指们来说!:]

改变通知消息

现在你已经有了一个用户可以控制上界和下界的用户交互控件。但你如何将这些更改通知给应用,以让应用知道新设置的值?

有很多模式可以实现发出修改消息:NSNotification, Key-Value-Observing (KVO),委托模式,目标操作模式和其他。有很多的选择!

怎么做?

如果你看过UIKit的控件,你将发现他们都不使用 NSNotification 也不鼓励使用KVO,所以为了能和UIKit达到一致性你应该避免使用这两个方法。另外两种方法委托和目标则在UIKit中广泛使用。

以下是对委托和目标的详细描述:

委托模式 - 委托者模式中你将提供一个包含多个方法的协议用于一定范围内的消息通知。控件有一个属性,通常命名为 delegate ,用于接受使用这种协议的类。一个经典的例子是UITableView使用了 UITableViewDelegate 协议。注意这些控件都值接受一种协议实例。一个委托方法有许多参数,所以你可以使用方法传递尽可能多的信息。

目标操作模式 - 目标操作模式是 UIControl 的基类。当一个控件的状态发生改变时,将用 UIControlEvents 的枚举值提醒目标发生了什么动作。你可以给一个控件提供多个目,可以创建自定义事件(具体请查看 UIControlEventApplicationReserved ),自定义事件的数量限定为4个。控件多做不能在事件中传递更多的信息。所以他不能处理包含巨量信息的事件。

下面是关于两个模式的不同点:

  • 多路广播 - 目标操作模式可以实现多路通知,而委托模式只限制在一个委托实例上。

  • 灵活性 - 你在委托模式中定义了自己的协议,以为着你可以通过它控制巨量的信息。目标操作不能提供额外的信息,客户端只能在接受到事件后自查。

你的范围滑条中提供的消息中是没有大量的状态信息的。消息中其实仅有最大和最小值。

在这种情况下,目标操作模式完美的做到了这一点。这就是为什么在教程的一开始要求你继承UIControl的原因之一!

啊哈!现在感觉不错!:]

滑条值的更新在 continueTrackingWithTouch:withEvent: ,所以这里也是你添加通知代码的地方。

打开RangeSlider.swift,定位到 continueTrackingWithTouch ,在“ return true ”前面添加:

sendActionsForControlEvents(.ValueChanged)

这样做你就完全可以响应定制目标的改变!

现在你已经有你的消息处理了,你现在应该将他挂载到你的应用中。

打开ViewController.swiftviewDidLoad 尾部添加如下代码:

rangeSlider.addTarget(self, action: "rangeSliderValueChanged:", forControlEvents: .ValueChanged)

上面的代码将在滑条每次发送 UIControlEventValueChanged 动作时调用 rangeSliderValueChanged 方法。

现在在ViewController.swift中添加如下代码:

func rangeSliderValueChanged(rangeSlider: RangeSlider) {

    println("Range slider value changed: (\(rangeSlider.lowerValue) \(rangeSlider.upperValue))")

}

这段代码简单的将滑块数值的改变发送到控制台输出上。

运行应用,左右移动滑条。你将在控制台上看到空间输出的值,截图如下:

Range slider value changed: (0.117670682730924 0.390361445783134)

Range slider value changed: (0.117670682730924 0.38835341365462)

Range slider value changed: (0.117670682730924 0.382329317269078)

Range slider value changed: (0.117670682730924 0.380321285140564)

Range slider value changed: (0.119678714859438 0.380321285140564)

Range slider value changed: (0.121686746987952 0.380321285140564)

估计你已经看够了由多种颜色拼成的滑条的UI。他看起来就像一个愤怒的水果沙拉!

是时候给控件整整容了!

使用Core Graphics修改你的控件

首先,首先你需要更新滑钮滑动的轨道。

增加另一个以CALayer为父类的类像之前一样,这次起名为 RangeSliderTrackLayer

打开这个类,将内容替换为如下内容:

import UIKit

import QuartzCore



class RangeSliderTrackLayer: CALayer {

    weak var rangeSlider: RangeSlider?

}

代码添加了一个指向背景的弱引用,就像之前的滑钮层一样。

打开 RangeSlider.swift,定位到 trackLayer 属性并修改为一个新的类,如下:

let trackLayer = RangeSliderTrackLayer()

现在寻找init方法,并替换如下:

init(frame: CGRect) {

    super.init(frame: frame)



    trackLayer.rangeSlider = self

    trackLayer.contentsScale = UIScreen.mainScreen().scale

    layer.addSublayer(trackLayer)



    lowerThumbLayer.rangeSlider = self

    lowerThumbLayer.contentsScale = UIScreen.mainScreen().scale

    layer.addSublayer(lowerThumbLayer)



    upperThumbLayer.rangeSlider = self

    upperThumbLayer.contentsScale = UIScreen.mainScreen().scale

    layer.addSublayer(upperThumbLayer)

}

代码确保了滑钮从滑道的引用添加正确 - 之前可怕的背景颜色不再被使用了。:]设置 contentsScale 工厂匹配设备将确保在视网膜屏幕上也能按正确的比例显示。

这项的内容有点多一些 - 在控件中删除红色的背景。

打开ViewController.swift,定位到viewDidLoad方法中的如下位置然后删除它:

rangeSlider.backgroundColor = UIColor.redColor()

运行。。。你会看到什么呢?

Mou icon

你什么都没看到?那就对了!

很好?这有什么好的?之前所做的一切都白费了?!?!

别着急 - 你刚才删除了层上用于测试的颜色。你的控件一直都在这里 - 但是你的控件使用的是一个空白的画布!

因为大多数开发者喜欢可以定制控件的显示风格,所以你需要在滑条里添加一些属性以实现可以改变视觉效果的控件。

打开RangeSlider.swift,将下面的内容添加到你之前添加代码的位置:

var trackTintColor = UIColor(white: 0.9, alpha: 1.0)

var trackHighlightTintColor = UIColor(red: 0.0, green: 0.45, blue: 0.94, alpha: 1.0)

var thumbTintColor = UIColor.whiteColor()



var curvaceousness : CGFloat = 1.0

使用可变颜色的目的是非常简单的。还有曲线?好的,这一点就有趣了 - 你很快就会了解他!:]

下一步,打开RangeSliderTrackLayer.swift

这个层是用于渲染两个滑钮下面的滑道的。他继承于 CALayer ,他只是渲染一个固定的颜色。

为了绘制这条轨迹,你需要实现 drawInContext :并且使用Core Graphics APIs来执行渲染。

注意:想深入学习Core Graphics的只是,强烈推荐学习Core Graphics 101系列引导教程的内容,这里如果讨论关于Core Graphics的话题就超出了本教程的范围了。

RangeSliderTrackLayer 添加如下方法:

override func drawInContext(ctx: CGContext!) {

    if let slider = rangeSlider {

        // Clip

        let cornerRadius = bounds.height * slider.curvaceousness / 2.0

        let path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)

        CGContextAddPath(ctx, path.CGPath)



        // Fill the track

        CGContextSetFillColorWithColor(ctx, slider.trackTintColor.CGColor)

        CGContextAddPath(ctx, path.CGPath)

        CGContextFillPath(ctx)



        // Fill the highlighted range

        CGContextSetFillColorWithColor(ctx, slider.trackHighlightTintColor.CGColor)

        let lowerValuePosition = CGFloat(slider.positionForValue(slider.lowerValue))

        let upperValuePosition = CGFloat(slider.positionForValue(slider.upperValue))

        let rect = CGRect(x: lowerValuePosition, y: 0.0, width: upperValuePosition - lowerValuePosition, height: bounds.height)

        CGContextFillRect(ctx, rect)

    }

}

一旦轨道的轮廓被裁减,背景就会填充上。之后高亮的部分也被填充。

运行一下,看看你的新的轨道,很自豪吧!他看起来应该如下所示:

Mou icon

尝试着改变可以变化的值看看会对渲染产生什么影响。

如果你一直怀疑 curvaceousness 是做什么的,尝试改变他试一下!

你将使用相同的方式绘制按钮层。

打开RangeSliderThumbLayer.swift在属性声明部分的下部添加如下内容:

override func drawInContext(ctx: CGContext!) {

    if let slider = rangeSlider {

        let thumbFrame = bounds.rectByInsetting(dx: 2.0, dy: 2.0)

        let cornerRadius = thumbFrame.height * slider.curvaceousness / 2.0

        let thumbPath = UIBezierPath(roundedRect: thumbFrame, cornerRadius: cornerRadius)



        // Fill - with a subtle shadow

        let shadowColor = UIColor.grayColor()

        CGContextSetShadowWithColor(ctx, CGSize(width: 0.0, height: 1.0), 1.0, shadowColor.CGColor)

        CGContextSetFillColorWithColor(ctx, slider.thumbTintColor.CGColor)

        CGContextAddPath(ctx, thumbPath.CGPath)

        CGContextFillPath(ctx)



        // Outline

        CGContextSetStrokeColorWithColor(ctx, shadowColor.CGColor)

        CGContextSetLineWidth(ctx, 0.5)

        CGContextAddPath(ctx, thumbPath.CGPath)

        CGContextStrokePath(ctx)



        if highlighted {

            CGContextSetFillColorWithColor(ctx, UIColor(white: 0.0, alpha: 0.1).CGColor)

            CGContextAddPath(ctx, thumbPath.CGPath)

            CGContextFillPath(ctx)

        }

    }

}

一旦路径定义成按钮的轮廓,轮廓将被填充。注意看那细微的影子,这给人一种按钮在轨道上徘徊的立体感。边界之后被渲染。最后,如果按钮高亮 - 正打算移动 - 在他的附近就会出现一个微妙的阴影。

最后的事情是在运行前。将 highlighted 属性改为如下所示:

var highlighted: Bool = false {

    didSet {

        setNeedsDisplay()

    }

}

这里,你定义了一个属性观察者,这样在高亮这个属性改变时层会被重绘。这样在触摸事件激活时颜色会发生些微的改变。

再次运行;你将会看到漂亮的轮廓,和下面的截图类似:

Mou icon

你可以轻易的发现使用Core Graphics渲染你的控件产生了很多额外的效果,这是非常值得的。使用Core Graphics可以比用图片渲染更轻易的进行控制。

操控控件属性的改变

现在还剩什么? 现在的控件看起来华丽而俗气,视觉效果可以改变,并且支持目标操作模式的消息通知。

看起来已经完成了 - 或者没完成?

想一下在渲染完成后,滑条属性会被设置成什么。举例来说,如果你想通过设置属性来改变滑条的显示范围,或者通过设置轨迹高亮来确定一个可用的范围。

目前没有进行属性的观察者设置。你需要在控件中添加如下功能。这里你需要实现观察属性,以实现更新控件的轮廓或重新绘制。打开RangeSlider.swift并且用下面的代码改变属性描述:

var minimumValue: Double = 0.0 {

    didSet {

        updateLayerFrames()

}

}



var maximumValue: Double = 1.0 {

    didSet {

        updateLayerFrames()

    }

}



var lowerValue: Double = 0.2 {

    didSet {

        updateLayerFrames()

    }

}



var upperValue: Double = 0.8 {

    didSet {

        updateLayerFrames()

    }

}



var trackTintColor: UIColor = UIColor(white: 0.9, alpha: 1.0) {

    didSet {

        trackLayer.setNeedsDisplay()

    }

}



var trackHighlightTintColor: UIColor = UIColor(red: 0.0, green: 0.45, blue: 0.94, alpha: 1.0) {

    didSet {

        trackLayer.setNeedsDisplay()

    }

}



var thumbTintColor: UIColor = UIColor.whiteColor() {

    didSet {

        lowerThumbLayer.setNeedsDisplay()

        upperThumbLayer.setNeedsDisplay()

    }

}



var curvaceousness: CGFloat = 1.0 {

    didSet {

        trackLayer.setNeedsDisplay()

        lowerThumbLayer.setNeedsDisplay()

        upperThumbLayer.setNeedsDisplay()

    }

}

基本上,你需要调用 setNeedsDisplay 在属性值被改变时。 setLayerFrames 调用时可以改变控件的布局。

现在,寻找 updateLayerFrames 并且将如下内容添加到这个方法的顶端:

CATransaction.begin()

CATransaction.setDisableActions(true)

在最下面添加如下方法:

CATransaction.commit()

代码会使用一些动画效果,这样可以使整个过程更平滑。这种效果会在层上被禁用,像之前我们做的一样,以使层内容立即更新。

自从你现在可以自动更新画面,每次最大最小值改变,在 continueTrackingWithTouch 中寻找如下代码,并删除它:

// 3. Update the UI

CATransaction.begin()

CATransaction.setDisableActions(true)



updateLayerFrames()



CATransaction.commit()

这是你需要做的全部事情,确保滑条改变属性。

尽管如此,你还是需要一些代码,来测试你的大量代码,并且确保所有的元素都关联起来,如期运行。

打开ViewController.swift并且在 viewDidLoad 方法的结尾添加如下代码:

let time = dispatch_time(DISPATCH_TIME_NOW, Int64(NSEC_PER_SEC))

dispatch_after(time, dispatch_get_main_queue()) {

    self.rangeSlider.trackHighlightTintColor = UIColor.redColor()

    self.rangeSlider.curvaceousness = 0.0

}

这断代码会更新控件的属性,实际使用中会有一些延迟。你改变轨迹高亮的颜色为红色,改变滑条的轮廓和滑钮的外观。

运行项目。几秒后,你应该看到滑条从这样:

Mou icon

变成这样:

Mou icon

多简单,不是吗

刚才添加的代码演示了一个有趣的部分,而且是经常被忽视的部分,同时也是开发自定义控件最关键的部分 - 测试。当你开发你的自定义控件时,你就有责任测试他的所有属性并验证所有视觉效果。一个好的办法是创建一个视觉测试框架,有各种不同的按钮和滑条,每个控件都和控件的不同属性关联。这样你就可以同时改变你控件的各种属性 - 并查看会有什么效果。

接下来要做的事情?

你的数值范围滑条已经完成了所有功能,并且可以在你自己的应用中使用!你可以在这里下载完整版本。

尽管如此,创建通用性控件的好处之一是你可以将他用在其他工程上 - 并和其他开发者分享。

在这个苹果开发的黄金时期,你的控件准备好了吗?

现在还不是时候。在共享你的自定义控件之前这里还有其他的几点需要考虑:

文档 - 所有的开发者都喜欢编码!:]你肯定认为你的代码是漂亮的精美的,自文档的,可其他的开发者可不这么想。一个好的练习就是提供一个公共的API文档,最精简的,包括所有的开放共享代码。这意味着你将写出所有的开放类和属性。

举例说明,你的 RangeSlider 需要文档来解释 - 一个滑条有四个成员属性: minimumValue, maximumValue, lowerValue, 和 upperValue - 这可以做什么 - 可以让用户直接设置范围值。

健壮性 - 如果你设置最小值比最大值大会发生什么?你自己当然不会这么干 - 那会很愚蠢,不是吗?但你不能保证别人不这么做!你需要保证控制状态始终是可用的 - 尽管有些人会做傻事。

API设计 - 前面的健壮性延伸出了其他话题 - API设计。创建一个灵活,直观,健壮的API将是你的控件使用的更广泛,也会更流行。

API设计是一个更深层次的主题,超出了我们本次教程的讨论范围。如果有兴趣,强烈推荐Matt Gemmell’s 25 rules of API design

共享你的控件有如下几种方式。这里有如下几条建议:

  • GitHub - GitHub已经成为最流行的开源项目社区。这里已经有大量的自定义控件。GitHub的好处就在于他允许人们非常容易的使用你的代码并且通过为你的控件创建分支方法来实现潜在的合作开发,或者你也可以为已有的控件提交错误报告。

  • CocoaPods – 允许人们方便的添加控件到自己的工程,你也可以在CocoaPods发布,这里主要是支持iOS和OSX工程。

  • Cocoa Controls – 这个站点提供了商业和开源控件。这里的许多开源控件引用于GitHub上。这里也是一个推广你新鲜创意的好地方。

希望你能在创建滑条控件时得到乐趣,也希望你已经用自己的灵感创造了一个自定义控件。如果是这样,那么请在评论中发布链接 - 让我们看到您惊人的创造力!

在Swift中用Sprite Kit制作的卡牌游戏机制


原文地址:http://www.raywenderlich.com/76718/card-game-mechanics-sprite-kit-swift
泰然翻译组:柳比。校对:glory。

Example card image

学习如何实现基本的卡牌游戏机制和动画。

在过去的20年里,人们玩过收集类卡牌游戏(CCGs)。维基百科提供了比较详尽的这些游戏演变的过程,它似乎启发了角色扮演游戏(RPG)像龙与地下城。万智牌是一个现代化的CCG的一个例子。

其核心,CCGs是一组自定义卡片代表了人物,位置,能力,事件等。玩游戏前,玩家首先必须构建他们自己的牌组,然后他们才能使用他们独特的牌组来玩游戏。大多数玩家的牌组都会突出某种派别,生物或能力。

在本教程中,你将在CCG应用程序中使用Sprite Kit操纵图片卡。你会在屏幕上移动卡牌,移动它们来看哪些卡牌是有效的,翻转它们并放大它们来看卡牌上的文字 — 或欣赏艺术作品。

如果你还不熟悉SpriteKit,你可以阅读初学者的教程iOS游戏教程。如果你还不熟悉Swift,确认你已经了解了Swift快速入门系列.

开始

由于这是一个卡牌游戏,最好的开始就是看实际的卡牌。下载启动项目,它提供了一个iPad上横向模式下的SpriteKit项目,以及所有的图像,字体和声音文件,和你需要创建的一个功能性的示例游戏。

花一点时间来熟悉项目的文件结构和内容。你应该能看到如下的项目目录:

  1. System: 包含设置一个SpriteKit项目的基础文件。它包含了AppDelegate.swiftGameViewController.swiftMain.storyboard

  2. Scenes: 包含管理游戏内容的一个空主场景文件GameScene.swift

  3. Card: 包含管理卡牌的一个空的Card.swift文件。

  4. Supporting Files: 包含你将在本教程中使用的所有图像、字体和声音文件。

这个游戏没有美工不会显得很酷,所以我想特别感谢gameartguppy.com的Vicki的美丽的卡牌插图!

一个优雅的开始

既然我们需要使用卡牌玩卡牌游戏,我们将创建一个卡牌类来代表它们。目前Card.swift是一个空的Swift文件,所以找到它并添加:

import Foundation

import SpriteKit



class Card : SKSpriteNode {



  required init(coder aDecoder: NSCoder!) {

    fatalError("NSCoding not supported")

  }



  init(imageNamed: String) {

    let cardTexture = SKTexture(imageNamed: imageNamed)

    super.init(texture: cardTexture, color: nil, size: cardTexture.size())

  }

}

你声明了CardSKSpriteNode的子类。

要根据图片创建一个简单的精灵,你可以使用SKSpriteNode(imageNamed:)。为了保持这种行为,你需要使用继承的初始化函数,它将调用父类的指定的初始化函数init(texture:color:size:)。在本游戏中你不支持NSCoding

要把精灵放到屏幕上,打开GameScene.swift然后添加下列代码到didMoveToView()中:

let wolf = Card(imageNamed: "card_creature_wolf.png")

wolf.position = CGPointMake(100,200)

addChild(wolf)



let bear = Card(imageNamed: "card_creature_bear.png")

bear.position = CGPointMake(300, 200)

addChild(bear)

构建和运行项目,并花一点时间来欣赏儿狼和熊。

Card Images on iPad Screen

一个好的开始…

规则 #1 创建卡牌游戏:先有创意和富有想象力的美工。看起来你的应用程序塑造的不错!

注: 根据屏幕的大小,你可能需要缩放模拟器的窗口,使用Window\Scale\50%来适应屏幕。我也推荐使用iPad 2模拟器。

看这一对卡牌非常有趣,但如果你能真正的移动卡片UI将会更酷。你将在下面做到这些!

我想移动它,移动它…

无论美工的质量如何,卡牌静坐在屏幕上不会让你的应用程序获得任何的好评,因为你需要像你在玩真实的纸质卡牌一样能拖动它们。实现它的最简单的办法是在场景中处理触摸事件。

依旧在GameScene.swift中,添加这个新的函数到类中:

override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {

  for touch in touches {

    let location = touch.locationInNode(self)

    let touchedNode = nodeAtPoint(location)

    touchedNode.position = location

  }

}

构建和运行项目,然后试着拖动这两个卡牌。

Cards move, but sometimes slide under other cards

卡牌现在可以移动了,但有时卡牌会被遮住。继续下面的阅读来解决这个问题。

你玩过之后,将注意到两个主要的问题:

  1. 首先,由于精灵都在相同的zPosition,所以它们在被添加到场景中时被布置成相同的顺序。这意味着熊卡牌会在狼的卡牌“上面”。如果你拖动狼,它似乎被熊遮住了。

  2. 第二,nodeAtPoint()返回这个点上的最上面的精灵。所以当你拖动熊下面的狼时,nodeAtPoint()返回熊精灵然后开始改变它的位置,所以你可能会发现你拖动狼时熊会被拖动。

这个效果虽然很神奇,但它并不是你在最终的应用程序中想要的!

为了解决这个问题,你将需要在拖动时修改卡牌的zPosition。你的第一反应可能是在touchesMoved中修改精灵的zPosition,但如果你想在后面把它改回来,这不是一种好的办法。

使用开始和结束函数是一个更好的策略。依旧在GameScene.swift中,添加下列函数:

override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {

  for touch in touches {

    let location = touch.locationInNode(self)

    let touchedNode = nodeAtPoint(location)

    touchedNode.zPosition = 15

  }

}



override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {

  for touch in touches {

    let location = touch.locationInNode(self)

    let touchedNode = nodeAtPoint(location)

    touchedNode.zPosition = 0

  }

}

再次构建和运行项目,你将看到卡牌像我们期望的那样滑动。

Cards now correctly move over each other

卡片现在正确的移动了,但看起来有些平淡。你将在后面解决这个问题。

确保你挑选了大于其他卡牌的一个zPosition值。在本教程的示例游戏中,有一些重叠的元素的zPosition为20。数字19确保了重叠的元素显示在卡牌之上。

现在卡牌已经正确的移动了,但你需要添加一些令人满意的内容 — 一个可视化的指示表明卡牌被举起了。

是时候让你的卡牌跳舞了!

卡牌动画

依旧在GameScene.swift中,添加下面的代码到touchesBegan()函数中for循环的结尾

let liftUp = SKAction.scaleTo(1.2, duration: 0.2)

touchedNode.runAction(liftUp, withKey: "pickup")

touchesEnded()中添加类似的内容

let dropDown = SKAction.scaleTo(1.0, duration: 0.2)

touchedNode.runAction(dropDown, withKey: "drop")

在这里当你点击卡牌时,使用了SKActionscaleTo(scale:duration:)函数来增加卡牌的宽度和高度为它原来的1.2倍,当你松开时变回原来的大小。

构建和运行项目来查看效果。

Moving cards with pickup and drop down animation.

这个简单的动画像是把卡牌拿起来和放下去。有时候最简单的动画也是最有效的。

调整你scale和duration的值来找到最适合你的。如果你设置拿起和放下的durations为不同的值,你可以让它看起来像拿起的时候比较慢,放下的时候比较快。

摆动,摆动,摆动

拖动卡牌现在工作得已经很好了,但你应该做得更好。让卡牌围绕y轴拍翅膀当然是很棒的。

由于SpriteKit是一个纯2D框架,似乎没有办法做到精灵的部分转动效果。然而你可以这么做,修改xScale属性来制造转动的假象。

你将添加代码到touchesBegan()touchesEnded()函数中。在touchesBegan()中添加如下代码到for循环的最后:

let wiggleIn = SKAction.scaleXTo(1.0, duration: 0.2)

let wiggleOut = SKAction.scaleXTo(1.2, duration: 0.2)

let wiggle = SKAction.sequence([wiggleIn, wiggleOut])

let wiggleRepeat = SKAction.repeatActionForever(wiggle)



touchedNode.runAction(wiggleRepeat, withKey: "wiggle")

touchesEnded()中添加类似的代码:

touchedNode.removeActionForKey("wiggle")

这些代码让卡片回来转动 — 只是一点点 — 当它来回移动时。这个效果利用了reaction(action:, withKey:)函数添加了一个字符串名称到这个动作,这样你可以在后面取消它。

对于这种做法有一个小小的警告:当你删除动画时,无论它在不在动画的生命周期中,它会离开精灵。

你已经有一个动作使卡牌返回它的初始缩放值1.0。由于缩放同时设置x和y缩放,所以这部分不用担心,但如果你使用其他的属性,记得在touchesEnded函数中返回到初始值。

构建和运行项目,你会看到当你拖动他们时,卡牌会拍翅膀。

Card with scaling animation to fake 3d rotation.

一个简单的动画来显示这个卡牌是当前活动的。

挑战:在本教程最后的额外的示例游戏中,你将了解如何使用zRotation来让卡牌来回摇晃。

试着用rotateBy更换scaleXTo动作,以“摇晃”动画取代“摆动”动画。记得使其循环,这意味着它需要在重复之前返回它的出发点。

Card rotates slightly back and forth.

尝试重现这个摇摆动画的效果。

解决方法

touchesBegan中替换下列摆动代码:

let rotR = SKAction.rotateByAngle(0.15, duration: 0.2)

let rotL = SKAction.rotateByAngle(-0.15, duration: 0.2)

let cycle = SKAction.sequence([rotR, rotL, rotL, rotR])

let wiggle = SKAction.repeatActionForever(cycle)

touchedNode.runAction(wiggle, withKey: "wiggle")

这给你的卡牌添加了令人满意的小摆动,但是仍然有一个问题。试着拖动卡牌完成半个摆动周期。它的旋转是不是错误的?是的,这就是你下一步需要解决的问题,添加下面这行代码到touchesEndedfor循环的结尾:

runAction(SKAction.rotateToAngle(0, duration: 0.2), withKey:"rotate")

现在当你释放卡牌时有了一个正确旋转的漂亮的摇摆动画!

追踪伤害

在很多卡牌收集游戏中,像这些怪物会有关联的伤害值,可以与其他卡牌进行战斗。

要实现这一点,你需要在卡牌的顶端添加一个标签,这样用户可以追踪每个生物造成的伤害。依旧在GameScene.swift中,添加如下新方法:

func newDamageLabel() -> SKLabelNode {

  let damageLabel = SKLabelNode(fontNamed: "OpenSans-Bold")

  damageLabel.name = "damageLabel"

  damageLabel.fontSize = 12

  damageLabel.fontColor = UIColor(red: 0.47, green: 0.0, blue: 0.0, alpha: 1.0)

  damageLabel.text = "0"

  damageLabel.position = CGPointMake(25, 40)



  return damageLabel

}

这个辅助方法创建了一个新的SKLabelNode,它将显示每个卡牌造成的伤害。它使用了启动项目中正确的info.plist设置中包含的自定义字体。

注:关于安装自定义字体的更多信息,请查看iOS游戏教程中的第7章, “标签”.

你想知道示例中的位置是怎么工作的吗?

由于标签是卡牌精灵的子节点,位置默认是相对于精灵中心的锚点。通常经过一些尝试就可以得到你想要的标签位置。

添加如下代码到didMoveToView()结尾,来给每个卡牌添加一个伤害标签:

wolf.addChild(newDamageLabel())

bear.addChild(newDamageLabel())

构建和运行项目。你现在应该在每个卡牌中看到一个红色的“0”。

Card with a label for damage taken.

卡牌现在有一个显示受到多少伤害的标签了。

试着拖动卡牌,但是点击该标签拖动时拖动的是标签而不是卡牌本身。注意标签飞到了什么地方 — 也许是一个神奇的国度在那里可以肆无忌惮的移动?

不,其他它不是那么神秘。;]

这里的问题是当你调用nodeAtPoint时,它返回SKNode最上层的任何类型的节点,在这种情况下是SKLabelNode。当你改变节点的位置,移动的是标签而不是卡牌。呃。。。是的,这是合乎逻辑的解释。

Dragging on top of the damage label causes problems.

触摸顶部伤害标签的结果。哎呀。(改变背景为白色,使标签更明显)

场景触摸处理的利弊

在继续之前,让我们考虑考虑场景级别的触摸处理的优点和缺点。

在项目中,场景级别的触摸处理是一个好的出发点,因为它是最简单,最直接的办法。事实上,如果你的精灵有透明区域需要被忽略,比如六角格,这可能是唯一合理的解决方案。

但是,当你有复合的精灵时它就开始显出缺点了。例如,这些可能包含多个图像、标签或甚至血条。如果你有不同的规则对应不同的精灵,它可以是很笨重和复杂的。

一个好的办法是你使用nodeAtPoint,它总是返回一个节点。

如果你拖动到一个卡牌精灵之外会发生什么?因为SKSceneSKNode的一个子类,如果触摸位置相交没有其他节点,则场景本身会返回一个SKNode

当你修改了位置并在之前做了动画,你可能不知道但你实际上应该检查touchedNode是不是场景本身,但因为现在是一个学习阶段,所以也没有关系。

你会很高兴的知道这里有一个更好的解决方案…

处理那些触摸!处理它们!

你能做什么替代它呢?好吧,你可以使卡牌类负责处理它自己的触摸事件。这种方法的逻辑是相当明确的。打开Card.swift然后添加下列内容到init(imageNamed:)中:

userInteractionEnabled = true

这使得卡牌类拦截了触摸事件而不是传递它们到场景里。SpriteKit将根据这个属性设置发送触摸事件到最上层的实例。

接下来,你需要删除这三个触摸处理函数,GameScene.swift中的touchesBegan()touchesMoved()touchesEnded()然后把它们添加到Card.swift

原来的代码不能像拿过来就直接用,所以它需要一些变化来与节点工作。

作为一个挑战,让我们看看你能不能不看答案做出适当的改变!

提示:由于触摸事件直接发送到了正确的精灵,所以你不需要指出需要修改的精灵。

解决方法

override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {

  for touch in touches {

    // note: removed references to touchedNode

    // 'self' in most cases is not required in Swift

    zPosition = 15

    let liftUp = SKAction.scaleTo(1.2, duration: 0.2)

    runAction(liftUp, withKey: "pickup")



    let wiggleIn = SKAction.scaleXTo(1.0, duration: 0.2)

    let wiggleOut = SKAction.scaleXTo(1.2, duration: 0.2)

    let wiggle = SKAction.sequence([wiggleIn, wiggleOut])

    let wiggleRepeat = SKAction.repeatActionForever(wiggle)



    // again, since this is the touched sprite

    // run the action on self (implied)

    runAction(wiggleRepeat, withKey: "wiggle")

  }

}



override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {

  for touch in touches {

    let location = touch.locationInNode(scene) // make sure this is scene, not self

    let touchedNode = nodeAtPoint(location)

    touchedNode.position = location

  }

}



override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {

  for touch in touches {

    zPosition = 0

    let dropDown = SKAction.scaleTo(1.0, duration: 0.2)

    runAction(dropDown, withKey: "drop")

    removeActionForKey("wiggle")

  }

}

从本质上讲,这个复制了在他们当前的状态的触摸的处理函数到卡牌的实现里。主要的区别是你不再需要搜索节点树来找对应的节点。

SpriteKit调用了正确实例的函数,所以你只需要直接修改属性。

构建和运行项目,你会注意到它解决了之前乱飞的标签的问题。

Card can be moved the same as before

卡牌可以像以前一样移动。

两面性

现在花一点时间来研究卡牌节点是如何初始化的。目前,我们简单的使用了字符串名称来创建纹理,然后发送它到父类的初始化函数。

为了添加属性,如攻击和防御值,或神秘的魔法效果,你需要基于指定的卡牌数据设置属性和配置它们。你应该用枚举代替使用字符串来定义卡牌,因为字符串定义卡牌容易出现错别字。打开Card.swift 然后添加如下内容到import行和类定义中间:

enum CardName: Int {

    case CreatureWolf = 0,

    CreatureBear,       // 1

    CreatureDragon,     // 2

    Energy,             // 3

    SpellDeathRay,      // 4

    SpellRabid,         // 5

    SpellSleep,         // 6

    SpellStoneskin      // 7

}

这定义了CardName为一个新的类型,你可以使用它来识别卡牌。整数值作为引用在一套牌里将会很有帮助。

接下来,你需要为去Card类定义一些自定义属性。添加下列代码到类声明和init中间:

let frontTexture: SKTexture

let backTexture: SKTexture

var largeTexture: SKTexture?

let largeTextureFilename: String

替换Card.swift中的init(imageNamed:)

init(cardNamed: CardName) {



  // initialize properties

  backTexture = SKTexture(imageNamed: "card_back.png")



  switch cardNamed {

  case .CreatureWolf:

    frontTexture = SKTexture(imageNamed: "card_creature_wolf.png")

    largeTextureFilename = "card_creature_wolf_large.png"



  case .CreatureBear:

    frontTexture = SKTexture(imageNamed: "card_creature_bear.png")

    largeTextureFilename = "Card_creature_bear_large.png"



  default:

    frontTexture = SKTexture(imageNamed: "card_back.png")

    largeTextureFilename = "card_back_large.png"

  }



  // call designated initializer on super

  super.init(texture: frontTexture, color: nil, size: frontTexture.size())





  // set properties defined in super

  userInteractionEnabled = true

}

最后,打开GameScene.swift然后修改didMoveToView()来使用新的枚举替换字符串文件名称:

let wolf = Card(cardNamed: .CreatureWolf)

wolf.position = CGPointMake(100,200)

addChild(wolf)



let bear = Card(cardNamed: .CreatureBear)

bear.position = CGPointMake(300, 200)

addChild(bear)

下面是修改的内容:

  • 首先,你添加了一个新的叫做CardName的类型,这种枚举类型的优点是编译器知道所有可能的值并会在你输入错误的时候警告你。另外,Xcode的自动完成功能可以在你输入名称的开始几个字符时提示你。

  • 接下来,你在Card.swift中创建了四个新的属性来存储每个SKTexture的值,它将基于卡牌的状态使用。每个卡牌需要一个字体图片,背景图片和大的前面图片。largeTextureFilename使图片需要使用时再加载以节省内存,防止载入大图片时内存溢出。

  • 接下来你更新了init方法来接受一个CardName而不是一个字符串,然后根据Card的类型设置了每个新创建的属性。这利用了Swift新的switch语句。这里switch的cases不会自动失败。另外,你需要提供一个default case,或者覆盖所有可能的值。一旦你有自定义的属性,比如攻击或防御,你可以分配这些值到switch语句中。

    当初始化swift对象时有一个特定的顺序你必须遵循。



    • 首先,确保类定义的所有属性有默认的值。

    • 其次,调用父类的designated initializer

    • 第三,设置所有父类中定义的属性,然后调用你需要的对象的任意函数。

  • 最后,你更新了GameScene.swift中的代码来使用新的Card的init函数。

构建和运行项目,确保一切像之前一样工作。

注:因为你只操作了七个卡牌,所以不需要复杂的初始化卡片。当你有几十或几百个卡牌时这种特殊的策略可能不太好用。到那时,你需要系统的存储所有卡牌的属性到一个配置文件里,比如一个.json文件。你还需要设置初始化系统从配置文件中抽出数据做为字典来构建卡牌。

挑战:

通过为其他卡牌添加正确的图片来完成Card,比如凶猛的龙。你将会在Supporting Files中的cards文件夹中找到图片。

Image of Dragon creature card

Dun Dun Dun

触发翻转

最后,添加一些卡牌类动作来使游戏更逼真。由于基本前提是两个玩家共享一个iPad,所以卡牌需要正面朝下来使其他玩家无法看到它们。

一种实现的简单方法是当双击它时翻转卡牌。但是,你需要一个属性来追踪卡牌的状态。

打开Card.swift然后添加下列属性到其他属性的下面:

var faceUp = true

接下来,添加一个交换的纹理使卡牌出现翻转效果:

func flip() {

  if faceUp {

    self.texture = self.backTexture

    if let damageLabel = self.childNodeWithName("damageLabel") {

      damageLabel.hidden = true

    }

    self.faceUp = false

  } else {

    self.texture = self.frontTexture

    if let damageLabel = self.childNodeWithName("damageLabel") {

      damageLabel.hidden = false

    }

    self.faceUp = true

  }

}

最后,添加如下代码到touchesBegan的开始,在for-in循环中:

if touch.tapCount > 1 {

  flip()

}

现在你应该明白了为什么之前我们保存了卡牌的正面和背面的图片为纹理 — 它让翻转卡牌如此简单。你还需要隐藏damageLabel,这样卡牌朝下时数字才不会显示。

构建和运行项目然后翻转那些卡牌。

Card flip

通过交换纹理来实现卡牌的翻转。第一次点击卡牌时会触发一个小的弹跳效果。

注:在这里,伤害标签在卡牌初始化时作为属性初始化是完美的。作为保持本教程简洁的目的,它在这里还是一个子节点。试着把它从GameScene放到Card中。

效果还不错,但你还可以做得更好。一个技巧是使用scaleToX动画来使它看下来像真的翻转了。

用如下代码替换flip

func flip() {

  let firstHalfFlip = SKAction.scaleXTo(0.0, duration: 0.4)

  let secondHalfFlip = SKAction.scaleXTo(1.0, duration: 0.4)



  setScale(1.0)



  if faceUp {

    runAction(firstHalfFlip) {

      self.texture = self.backTexture

      if let damageLabel = self.childNodeWithName("damageLabel") {

        damageLabel.hidden = true

      }

      self.faceUp = false

      self.runAction(secondHalfFlip)

    }

  } else {

    runAction(firstHalfFlip) {

      self.texture = self.frontTexture

      if let damageLabel = self.childNodeWithName("damageLabel") {

        damageLabel.hidden = false

      }

      self.faceUp = true

      self.runAction(secondHalfFlip)

    }

  }

}

scaleXTo只收缩了水平方向,制造了一个非常酷的2D翻转动画。动画分成两半,这样你可以分别互换它们的纹理。setScale函数确保其他缩放动画不会影响到翻转。

构建和运行项目来看看新的“翻转”效果。

Card flip with animation

现在你有了看起来很不错的翻转动画。

看起来还不错,但卡牌这么小,你还不能完全理解熊的傻笑。所以你需要能放大选中的卡牌来看它的细节…

放大时刻

你将在本教程学习的最后一个效果是修改双击动作来放大卡牌。添加这两个属性到Card.swift的最开始的其他属性的前面:

var enlarged = false

var savedPosition = CGPointZero

添加如下方法来执行放大动作:

func enlarge() {

  if enlarged {

    enlarged = false

    zPosition = 0

    position = savedPosition

    setScale(1.0)

  } else {

    enlarged = true

    savedPosition = position

    zPosition = 20

    position = CGPointMake(CGRectGetMidX(parent.frame), CGRectGetMidY(parent.frame))

    removeAllActions()

    setScale(5.0)

  }

}

记得更新touchesBegan()来调用新的函数替换flip()

if touch.tapCount > 1 {

  enlarge()

}



if enlarged { return }

最后,给touchesMoved()touchesEnded做一点小修改,添加如下代码到每个for-in循环前面:

if enlarged { return }

你需要添加额外的属性savedPosition,这样卡牌才能移回它原来的位置。正如前面提到的,这是触摸处理逻辑比较棘手的问题。

在函数开始处的tapCount检查防止了卡牌已经被放大了再次双击的错误。没有提前退出,大图片会收缩并开始摆动动画。

移动放大的图片也是没有意义的,所以触摸结束后没有什么可做的,所以当卡牌放大时所有函数都提前退出了。

构建和运行应用程序,查看卡牌放大到填充屏幕。

Basic card enlarging.

基础卡牌放大效果。动画看起来好多了,放大的图片是模糊的。

但是为什么它是像素化的?Vicki的美工在这种情况下可应该是很漂亮的。你放大成这样是因为你没有使用Supporting Filescards_large文件夹里的放大版本的图片。

因为一开始就加载所有卡牌的大图片会浪费内存,所以最好在用户不需要它们的时候不加载它们。

放大函数的最终版本如下:

func enlarge() {

  if enlarged {

    let slide = SKAction.moveTo(savedPosition, duration:0.3)

    let scaleDown = SKAction.scaleTo(1.0, duration:0.3)

    runAction(SKAction.group([slide, scaleDown])) {

      self.enlarged = false

      self.zPosition = 0

    }

  } else {

    enlarged = true

    savedPosition = position



    if largeTexture != nil {

      texture = largeTexture

    } else {

      largeTexture = SKTexture(imageNamed: largeTextureFilename)

      texture = largeTexture

    }



    zPosition = 20



    let newPosition = CGPointMake(CGRectGetMidX(parent.frame), CGRectGetMidY(parent.frame))

    removeAllActions()



    let slide = SKAction.moveTo(newPosition, duration:0.3)

    let scaleUp = SKAction.scaleTo(5.0, duration:0.3)

    runAction(SKAction.group([slide, scaleUp]))

  }

}

动画现在非常明确了。

卡牌的位置在运行动画之前保存了,所以它返回了它原来的位置。为了防止它放大时被拿起和放下动画打断,你添加了removeAllActions()函数。

当缩小动画运行时,enlarged和zPosition属性没有设置直到动画完成。如果这些值在完成前被修改了,被放大的卡牌后面的卡牌将会显示出来,它也会返回它之前的位置。

由于largeTexture被定义为optional,它的值可以为nill,或“没有值”。if语句测试了它,来看它是否有值,如果它没有则加载纹理。

注: Optional是学习Swift的一个核心部分。特别是它不同于Objective-C中的nil值。

再次构建和运行应用程序。你现在应该看到一个从初始位置到最终的放大的位置漂亮的,平滑的动画。你也会看到卡牌是非像素化的、干净的、填充的。

Card enlargement with animation.

把卡牌变大,然后换成大图片让它看起来更好。

最终挑战:音效是任何游戏的一个重要部分,启动项目中包含了很多声音文件。看看你能不能使用SKAction.playSoundFileNamed(soundFile:, waitForCompletion:)来添加音效到卡牌翻转中和放大的动作中。

下一步?

你可以在这里找到本教程的最终项目。

到了这里,你已经理解了一些可以在你自己的卡牌游戏中使用的基础的 — 和一些不那么基础的 — 卡牌机制。

这个示例项目中有很多微秒的动画你可以调整,所以确保你已经试过不同的值来找到你喜欢的和适合你的。

一旦你对动画满意了,这里还有板区域,牌组,攻击和很多其他特性,内容很多所以不能简单像这篇一样的一篇文章中全部讲完。你可以查看用Objective-CSwift完成的示例游戏,了解更多有关游戏开发的其他元素。

请使用论坛在下面评论、问问题或分享你对Swift的卡牌动画的想法。感谢你抽出宝贵的时间学习本教程!

?>