CS193P 6. Protocols and Delegation, Gestures

网站添加 Tags,方便分类查找文章。 Medium 非常简洁优雅,可以借鉴。网站改版和文章排版比较耗费时间,以后要以内容输出优先。

关于课程中 Swift 语言的知识我只是简单浏览一遍,毕竟他说的也不详细,真正搞懂还是要系统学习。复习一下《疯狂 Swift 讲义》或者 Google 都是可行的。重要是 Demo 实践。今天微博看到的玩笑:Computer Programming To Be Officially Renamed “Googling Stackoverflow”

1. Interface Builder

Demo: 自定义 UIViews in your storyboard (FaceView)

如何在iOS 8中使用Swift和Xcode 6制作精美的UI组件 有个例子:苹果在Xcode 6中加入了两个新的Interface Builder(下文用IB简称)属性声明:IBInspectable和IBDesignable。IBInspectable在IB的Attribute Inspector(属性检查器)中查看类的属性,而IBDesignable能实时更新视图,很厉害吧!

2. The Happiness MVC’s Model

是寄于 HappinessViewController 里的 happiness,不同于 smiliness。

3. Protocols and Delegation

扩展

协议也是一种数据类型

代理是一种非常重要的协议的应用。
代理是我们完成视图和控制器盲沟通的方式。

Delegation

通过代理模式 UIViews 可以控制 Model,虽然并不是真的拥有 Model。

如何实现:

  1. 视图中创建一个代理协议 (定义具体视图想要控制器关心的内容)
  2. 视图中创建一个代理的属性,其类型是代理协议
  3. 视图中使用代理的属性去得到并不用的事,或做并不能控制的事
  4. 控制器声明并完成代理协议
  5. 控制器把自己作为视图的代理,通过设置设置代理的属性(上面2提到的)
  6. 控制器实施协议

现在视图就勾住了控制器。不过视图仍不晓得控制器的内容,所以视图仍是通用/可重复使用的。

Demo: Happiness MVC’s Model using generic FaceView in its View

Cmd + Shift + O 快速打开文档,有种 Xcode 内置 spotlight 的即视感。

FaceView.swift 为自定义 UIView 的类,是 View。
HappinessViewController.swift 是继承自 UIViewController,是 Controller。

View 中 dataSource 是 protocol FaceViewDataSource 的属性(代理或数据源),作为 dataSource?.smilinessForFaceView(self) 的形参。

Controller 实现了该协议,并将自己作为代理的对象,并通过 happiness 变量来改变协议中

Controller 中的 faceView 通过 Outlet 监控 dataSource 值的改变(didSet),来控制 View 笑脸的变化(smiliness)。

Happiness MVC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

// FaceView.swift 部分代码


import UIKit

protocol FaceViewDataSource: class {
func smilinessForFaceView(sender: FaceView) -> Double?
}

@IBDesignable
class FaceView: UIView
{
@IBInspectable
var lineWidth: CGFloat = 3 { didSet { setNeedsDisplay() } }
@IBInspectable
var color: UIColor = UIColor.blueColor() { didSet { setNeedsDisplay() } }
@IBInspectable
var scale: CGFloat = 0.90 { didSet { setNeedsDisplay() } }

var faceCenter: CGPoint {
return convertPoint(center, fromView: superview)
}
var faceRadius: CGFloat {
return min(bounds.size.width, bounds.size.height) / 2 * scale
}

weak var dataSource: FaceViewDataSource?


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//  HappinessViewController.swift


import UIKit

class HappinessViewController: UIViewController, FaceViewDataSource
{

@IBOutlet weak var faceView: FaceView! {
didSet {
faceView.dataSource = self
}
}
var happiness: Int = 99 { // 0 = very sad, 100 = ecstatic
didSet {
happiness = min(max(happiness, 0), 100)
print("happiness = \(happiness)")
updateUI()
}
}

func updateUI() {
faceView.setNeedsDisplay()
}

func smilinessForFaceView(sender: FaceView) -> Double? {
return Double(happiness - 50) / 50
}
}

4. Gestures

手势识别分两步走:

  1. Adding a gesture recognizer to a UIView (asking the UIView to “recognize” that gesture)
  2. Providing a method to “handle” that gesture (not necessarily handled by the UIView)

Usually the first is done by a Controller
Though occasionally a UIView will do this itself if the gesture is integral to its existence

The second is provided either by the UIView or a Controller Depending on the situation. We’ll see an example of both in our demo.

第一步:Adding a gesture recognizer to a UIView

Imagine we wanted a UIView in our Controller’s View to recognize a “pan” gesture …

pannableView

上面的代码造成排版错误,估计代码高亮产生的,先换成图片。

This is just a normal outlet to the UIView we want to recognize the gesture

We use its property observer to get involved when the outlet gets hooked up by iOS

Here we are creating an instance of a concrete subclass of UIGestureRecognizer (for pans)

The target gets notified when the gesture is recognized (in this case, the Controller itself)

The action is the method invoked on recognition (the : means it has an argument)

Here we ask the UIView to actually start trying to recognize this gesture in its bounds

Let’s talk about how we implement the handler …

第二步:A handler for a gesture needs gesture-specific information

So each concrete subclass provides special methods for handling that type of gesture

For example, UIPanGestureRecognizer provides 3 methods

func translationInView(view: UIView) -> CGPoint // cumulative since start of recognition

func velocityInView(view: UIView) -> CGPoint // how fast the finger is moving (points/s)

func setTranslation(translation: CGPoint, inView: UIView)

This last one is interesting because it allows you to reset the translation so far

By resetting the translation to zero all the time, you end up getting “incremental” translation

The abstract superclass also provides state information

var state: UIGestureRecognizerState { get }
This sits around in .Possible until recognition starts
For a discrete gesture (e.g. a Swipe), it changes to .Recognized (Tap is not a normal discrete) For a continues gesture (e.g. a Pan), it moves from .Began thru repeated .Changed to .Ended
It can go to .Failed or .Cancelled too, so watch out for those!

So, given this information, what would the pan handler look like?

  func pan(gesture: UIPanGestureRecognizer) {
      switch gesture.state {
          case .Changed: fallthrough
          case .Ended:
              let translation = gesture.translationInView(pannableView)
// update anything that depends on the pan gesture using translation.x and .y
            gesture.setTranslation(CGPointZero, inView: pannableView)
        default: break
     }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

Remember that the action was “pan:” (if no colon, we would not get the gesture argument)

We are only going to do anything when the finger moves or lifts up off the device’s surface

fallthrough means “execute the code for the next case down” Here we get the location of the pan in the pannableView’s coordinate system Now we do whatever we want with that information By resetting the translation, the next one we get will be how much it moved since this one ### 常用手势:
- UIPinchGestureRecognizer
var scale: CGFloat // not read-only (can reset)

var velocity: CGFloat { get } // scale factor per second
- UIRotationGestureRecognizer
var rotation: CGFloat // not read-only (can reset); in radians

var velocity: CGFloat { get } // radians per second
- UISwipeGestureRecognizer Set up the direction and number of fingers you want, then look for .Recognized

var direction: UISwipeGestureRecoginzerDirection // which swipes you want

var numberOfTouchesRequired: Int // finger count
- UITapGestureRecognizer
Set up the number of taps and fingers you want, then look for .Ended

var numberOfTapsRequired: Int // single tap, double tap, etc.

var numberOfTouchesRequired: Int // finger count

 ### Demo: Happiness pinch and pan

**FaceView Gestures**
Add a gesture recognizer (pinch) to the FaceView to zoom in and out (control its own scale)

Add a gesture recognizer (pan) to the FaceView to control happiness (Model) in the Controller ```

// FaceView.swift 部分代码


import UIKit

protocol FaceViewDataSource: class {
func smilinessForFaceView(sender: FaceView) -> Double?
}

// 2. handler
func scale(gestrue: UIPinchGestureRecognizer) {

if gestrue.state == .Changed {
scale *= gestrue.scale
gestrue.scale = 1
}
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
//  HappinessViewController.swift 部分代码


import UIKit

class HappinessViewController: UIViewController, FaceViewDataSource
{

@IBOutlet weak var faceView: FaceView! {
didSet {
faceView.dataSource = self

// 1. Adding a gesture recognizer
faceView.addGestureRecognizer(UIPinchGestureRecognizer(target: faceView, action: "scale:"))
// try in storyboard
// faceView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: "changeHapponess:"))
}
}


// 通过 storyboard 添加 Pan Gesture Recognizer,然后 storyboard 添加 IBAction 到 Controller 中
private struct Constants {
static let HappinessGestureScale: CGFloat = 4
}

@IBAction func changeHappiness(gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .Ended: fallthrough
case .Changed:
let translatrion = gesture.translationInView(faceView)
let happinessChange = -Int(translatrion.y / Constants.HappinessGestureScale)
if happinessChange != 0 {
happiness -= happinessChange
gesture.setTranslation(CGPointZero, inView: faceView)
}
default: break
}
}

var happiness: Int = 3 { // 0 = very sad, 100 = ecstatic
didSet {
happiness = min(max(happiness, 0), 100)
print("happiness = \(happiness)")
updateUI()
}
}

func updateUI() {
faceView.setNeedsDisplay()
}

func smilinessForFaceView(sender: FaceView) -> Double? {
return Double(happiness - 50) / 50
}
}

>有待翻译 ## 5. 拓展阅读 [UIGestureRecognizer Tutorial: Getting Started](http://www.raywenderlich.com/76020/using-uigesturerecognizer-with-swift-tutorial) ## 项目代码保存在我的 GitHub: [CS193P-2015](https://github.com/gewill/CS193P-2015)