第十八章——觸摸事件 和 UIResponder【譯】

在接下來的兩章中茶鹃,您將創(chuàng)建 TouchTracker实夹,該應(yīng)用程序中用戶可以通過觸摸屏幕來畫畫禽笑。 在本章中蝗茁,您將創(chuàng)建一個(gè)視圖來使用戶能通過拖動(dòng)來畫線(圖18.1)墙基。 使用多點(diǎn)觸控的話栈拖,用戶一次可以繪制多條線趾盐。

圖18.1 TouchTracker

觸摸事件

作為 UIResponder 的子類搬泥,UIView 可以覆蓋四種方法來處理四個(gè)不同的觸摸事件:

  • 一個(gè)或多個(gè)手指觸摸屏幕
    func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)

  • 一個(gè)或多個(gè)手指在屏幕上移動(dòng)(該信息隨著手指的移動(dòng)而重復(fù)發(fā)送)
    func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)

  • 一個(gè)或多個(gè)手指從屏幕上移除
    func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)

  • 系統(tǒng)事件(如來電)在觸摸結(jié)束之前中斷它
    func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?)

讓我們看一下典型的觸摸生命周期辅搬。 當(dāng)用戶的手指觸摸屏幕時(shí)唯沮,會(huì)創(chuàng)建一個(gè) UITouch 的實(shí)例。 在手指觸摸的 UIView 上調(diào)用 touchesBegan(_:with :) 方法堪遂,并通過一個(gè) touches Set 傳入 UITouch介蛉。

當(dāng)手指圍繞屏幕移動(dòng)時(shí),觸摸對(duì)象被更新以包含手指在屏幕上的當(dāng)前位置溶褪。 然后币旧,最初觸摸的 UIView 將被發(fā)送消息 touchesMoved(_:with :)。 作為參數(shù)傳遞給此方法的 Set 中包含的 UITouch 就是之前手指觸摸屏幕時(shí)創(chuàng)建的猿妈。

當(dāng)手指從屏幕上移除時(shí)吹菱,最后一次更新觸摸對(duì)象以包含手指的最終位置,并且最初觸摸的視圖被發(fā)送消息 touchesEnded(_:with :)彭则。 該方法完成執(zhí)行后鳍刷,UITouch 對(duì)象將被銷毀。

從這些信息俯抖,您可以得出關(guān)于觸摸對(duì)象如何工作的一些結(jié)論:

  • 一個(gè) UITouch 對(duì)應(yīng)于屏幕上的一根手指输瓜。 只要手指在屏幕上,該觸摸對(duì)象就可以生存芬萍,并且始終包含手指在屏幕上的當(dāng)前位置尤揣。
  • 手指最初觸摸的視圖將接收該手指的每個(gè)觸摸事件消息。 即使手指移動(dòng)超出最初觸摸的 UIView 的邊界担忧,在該視圖上仍然會(huì)調(diào)用 touchesMoved(_:with :)touchesEnded(_:with :) 方法芹缔。 因此,如果觸摸從一個(gè)視圖開始瓶盛,則該視圖擁有觸摸的生命周期最欠。
  • 您不必——也不應(yīng)該——永遠(yuǎn)不要——引用 UITouch 對(duì)象示罗。 該應(yīng)用程序?qū)⒃试S您通過觸摸生命周期中不同點(diǎn)調(diào)用的 UIResponder 方法訪問觸摸對(duì)象。

每次觸摸都會(huì)像開始芝硬,移動(dòng)或結(jié)束一樣——觸摸事件(touch event) 被添加到 UIApplication 對(duì)象管理的事件的隊(duì)列中蚜点。 在實(shí)踐中,隊(duì)列很少會(huì)填滿拌阴,而且事件會(huì)立即發(fā)送绍绘。 這些觸摸事件的傳遞包括將一個(gè) UIResponder 消息發(fā)送到擁有觸摸的視圖。

多點(diǎn)觸摸呢迟赃? 如果多個(gè)手指在同一視圖的同一時(shí)間完成相同的事情陪拘,則所有這些觸摸事件都將一次發(fā)送。 每個(gè)觸摸對(duì)象——每個(gè)手指一個(gè)——包含在作為 UIResponder 消息中的參數(shù)傳遞的 Set 中纤壁。 然而左刽,同一時(shí)間內(nèi)觸摸的位置都不相同。 因此酌媒,通常不是一個(gè)響應(yīng)者消息對(duì)應(yīng)所有觸摸欠痴,而是多個(gè) 響應(yīng)者消息對(duì)應(yīng)一個(gè)或多個(gè)觸摸。 本章稍后將介紹如何處理多點(diǎn)觸摸秒咨。

創(chuàng)建 TouchTracker 應(yīng)用程序

現(xiàn)在讓我們開始你的應(yīng)用程序喇辽。 在 Xcode 中,創(chuàng)建一個(gè)新的 single view universal project雨席,并將其命名為 TouchTracker (圖18.2)菩咨。

圖18.2 創(chuàng)建TouchTracker

在構(gòu)建 TouchTracker 時(shí),您將使用模板創(chuàng)建的默認(rèn)視圖控制器和 storyboard文件陡厘。 對(duì)于其視圖和模型圖層旦委,您將要?jiǎng)?chuàng)建自定義視圖類和自定義結(jié)構(gòu)體。 TouchTracker 的主要部分如圖18.3所示雏亚。

圖18.3 TouchTracker的對(duì)象圖

我們從你的自定義結(jié)構(gòu)體開始。

創(chuàng)建 Line 結(jié)構(gòu)體

您將創(chuàng)建自定義 Line 類型摩钙。 到目前為止罢低,您創(chuàng)建的所有類型都是類。 其實(shí)他們一直是 Cocoa Touch 子類; 例如胖笛,您已經(jīng)創(chuàng)建過 NSObject网持,UIViewControllerUIView 的子類。

Line 將是一個(gè) 結(jié)構(gòu)體(struct)长踊。 您在本書中使用了結(jié)構(gòu)體-- CGRect功舀,CGSizeCGPoint 都是結(jié)構(gòu)體。 String身弊,Int辟汰,ArrayDictionary 也是如此列敲。 現(xiàn)在你要?jiǎng)?chuàng)建一個(gè)你自定義的結(jié)構(gòu)體。

創(chuàng)建一個(gè)名為 Line 的新的 Swift 文件帖汞。

Line.swift 中戴而,導(dǎo)入 CoreGraphics 并聲明 Line 結(jié)構(gòu)體。 聲明兩個(gè) CGPoint 屬性翩蘸,用于確定該線的起點(diǎn)和終點(diǎn)所意。

import Foundation
import CoreGraphics

struct Line {
??var begin = CGPoint.zero
??var end = CGPoint.zero
}

結(jié)構(gòu)體

結(jié)構(gòu)體與類有很多不同:

  • 結(jié)構(gòu)體不支持繼承。
  • 如果沒有聲明其他構(gòu)造器催首,Struct 將獲得 成員構(gòu)造器(member-wise initializer)扶踊。 成員構(gòu)造器接受類型中每個(gè)屬性的參數(shù)。 例如郎任,Line struct 具有成員構(gòu)造器 init(begin:CGPoint秧耗,end:CGPoint)
  • 如果所有屬性都具有默認(rèn)值涝滴,并且沒有聲明其他初始值設(shè)置绣版,那么結(jié)構(gòu)體也將獲得一個(gè)空的構(gòu)造器(init()),它創(chuàng)建一個(gè)實(shí)例并將所有屬性設(shè)置為其默認(rèn)值歼疮。
  • 也許最重要的是杂抽,結(jié)構(gòu)體(和枚舉)是 值類型(value type) —— 而不是類(它們是引用類型(reference type)) 。

值類型與引用類型

值類型是將值分配給另一個(gè)實(shí)例或傳遞給函數(shù)的參數(shù)時(shí)將其值復(fù)制的類型韩脏。 這意味著將值類型的實(shí)例分配給另一個(gè)實(shí)例會(huì)將第一個(gè)實(shí)例的副本分配給第二個(gè)實(shí)例缩麸。 值類型在 Swift 中起著重要作用。 例如赡矢,數(shù)組和字典都是值類型杭朱。 你寫的所有枚舉和結(jié)構(gòu)體也是值類型。

當(dāng)引用類型被分配給一個(gè)實(shí)例或傳遞給一個(gè)函數(shù)的參數(shù)時(shí)吹散,它們不被復(fù)制弧械。 而是傳遞對(duì)同一個(gè)實(shí)例的引用。 類和閉包是引用類型空民。

那么你該用哪個(gè)呢刃唐? 一般來說,我們建議您使用值類型(如結(jié)構(gòu)體)界轩,除非您完全知道您使用引用類型會(huì)更有益画饥。 值類型更容易理解,因?yàn)槟恍枰獡?dān)心在更改副本上的值時(shí)實(shí)例會(huì)發(fā)生什么浊猾。 如果您想對(duì)此主題進(jìn)行更深入的討論抖甘,請(qǐng)查看 Swift Programming:The Big Nerd Ranch Guide

創(chuàng)建 DrawView

除了自定義結(jié)構(gòu)體之外葫慎,TouchTracker 還需要自定義視圖衔彻。

創(chuàng)建一個(gè)名為 DrawView 的新 Swift 文件薇宠。 在 DrawView.swift 中,定義 DrawView 類米奸。 添加兩個(gè)屬性:一個(gè)可選的 Line 來跟蹤可能被繪制的線昼接,以及一個(gè) Line 數(shù)組來跟蹤已經(jīng)繪制的線。

import Foundation
import UIKit

class DrawView: UIView {

??var currentLine: Line?
??var finishedLines = [Line]()

}

DrawView 的一個(gè)實(shí)例將是應(yīng)用程序的 rootViewController 的視圖悴晰,項(xiàng)目中已經(jīng)存在一個(gè) ViewController慢睡。 視圖控制器需要知道它的視圖將是 DrawView 的一個(gè)實(shí)例。

打開 Main.storyboard铡溪。 選擇 View 并打開身份檢查器(Command-Option-3)漂辐。 在 Custom Class 下,將 Class 更改為 DrawView(圖18.4)棕硫。

圖18.4 更改視圖類

使用 DrawView繪制

DrawView 的實(shí)例需要能夠繪制線髓涯。 您將編寫一種使用 UIBezierPath 根據(jù)給定 Line 的屬性創(chuàng)建和描繪路徑的方法。 然后哈扮,您將覆蓋 draw(_ :) 來繪制完成線數(shù)組中的線以及當(dāng)前的線(如果有)纬纪。

DrawView.swift 中,實(shí)現(xiàn)用于畫線的方法滑肉,并覆蓋 draw(_ :)包各。

var currentLine: Line?
var finishedLines = [Line]()

func stroke(_ line: Line) {
??let path = UIBezierPath()
??path.lineWidth = 10
??path.lineCapStyle = .round

??path.move(to: line.begin)
??path.addLine(to: line.end)
??path.stroke()
}

override func draw(_ rect: CGRect) {
??// Draw finished lines in black
??UIColor.black.setStroke()
??for line in finishedLines {
????stroke(line)
??}

??if let line = currentLine {
????// If there is a line currently being drawn, do it in red
????UIColor.red.setStroke()
????stroke(line)
??}

}

觸屏畫線

一條線由兩點(diǎn)定義。 您的 Line 將這些點(diǎn)存儲(chǔ)為名為 beginend 的屬性靶庙。 觸摸開始時(shí)问畅,您將創(chuàng)建一個(gè) Line,并將其屬性設(shè)置為觸摸開始點(diǎn)六荒。 觸摸移動(dòng)時(shí)护姆,您將更新 Lineend。 當(dāng)觸摸結(jié)束時(shí)掏击,您將擁有完整的一條線卵皂。

DrawView.swift 中,實(shí)現(xiàn) touchesBegan(_:with :) 創(chuàng)建一條新的線砚亭。

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
??let touch = touches.first!

??// Get location of the touch in view's coordinate system
??let location = touch.location(in: self)

??currentLine = Line(begin: location, end: location)

??setNeedsDisplay()
}

該代碼首先在視圖的坐標(biāo)系統(tǒng)中找出觸摸的位置渐裂。 然后它調(diào)用 setNeedsDisplay() 方法,該方法標(biāo)記在運(yùn)行循環(huán)結(jié)束時(shí)重繪的視圖钠惩。

接下來,還可以在 DrawView.swift 中實(shí)現(xiàn) touchesMoved(_:with :)族阅,以便更新 currentLineend 屬性篓跛。

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
??let touch = touches.first!
??let location = touch.location(in: self)

??currentLine?.end = location

??setNeedsDisplay()
}

最后,仍然在 DrawView.swift 中坦刀,更新 currentLine 的結(jié)束位置愧沟,并在觸摸結(jié)束時(shí)將其添加到 finishedLines 數(shù)組蔬咬。

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
??if var line = currentLine {
????let touch = touches.first!
????let location = touch.location(in: self)
????line.end = location

????finishedLines.append(line)
??}
??currentLine = nil

??setNeedsDisplay()
}

構(gòu)建并運(yùn)行應(yīng)用程序并在屏幕上繪制一些線。 在繪制時(shí)沐寺,線將以紅色顯示林艘。 一旦完成,它們將以黑色顯示混坞。

處理多點(diǎn)觸控

繪制線時(shí)狐援,您可能已經(jīng)注意到,屏幕上有多個(gè)手指畫線并沒有任何作用——也就是說究孕,您一次只能畫一條線啥酱。 讓我們更新 DrawView,以便您可以用多個(gè)手指來繪制盡可能多的線條厨诸。

默認(rèn)情況下镶殷,視圖一次只能接受一個(gè)觸摸。 如果一個(gè)手指已經(jīng)觸發(fā)了 touchesBegan(_:with :) 但尚未完成微酬, 也就是沒有觸發(fā) touchesEnded(_:with :)绘趋, 所有后續(xù)的觸摸被忽略。 在這種情況下颗管,“忽略” 意味著在 DrawView 上將不會(huì)調(diào)用 touchesBegan(_:with :) 和任何其他 UIResponder 方法與額外的觸摸相關(guān)的方法陷遮。

Main.storyboard 中,選擇 Draw View 并打開屬性檢查器忙上。 選中標(biāo)有 Multiple Touch 的框(圖18.5)拷呆,以使 DrawView 實(shí)例的 multiTouchesEnabled 屬性設(shè)置為 true

圖18.5 多點(diǎn)觸控啟用

現(xiàn)在疫粥,DrawView 將接受多點(diǎn)觸控茬斧,每次手指觸摸屏幕,移動(dòng)或從屏幕上移除時(shí)梗逮,相應(yīng)的 UIResponder 將在視圖中被調(diào)用项秉。 但是,您現(xiàn)在有一個(gè)問題:您的 UIResponder 代碼原本假定一次只會(huì)觸發(fā)一個(gè)觸摸和繪制一條線慷彤。

例如娄蔼,每個(gè)觸摸處理方法要求它接收的一組 touches 集合中的 first 元素。 在單觸摸視圖中底哗,集合中只會(huì)有一個(gè)對(duì)象岁诉,所以要請(qǐng)求何對(duì)象都總是返回一個(gè)觸發(fā)事件的對(duì)象。 在多點(diǎn)觸摸視圖中跋选,該集可以包含多個(gè)觸摸涕癣。 此外,DrawView 只有一個(gè)屬性(currentLine)關(guān)聯(lián)到正在進(jìn)行的線前标。 顯然坠韩,您將需要保留當(dāng)前屏幕上觸摸的線的數(shù)量距潘。 雖然您可以創(chuàng)建更多的屬性,如 currentLine1currentLine2只搁,但是管理哪個(gè)屬性對(duì)應(yīng)于哪個(gè)觸摸將是一件麻煩事音比。

您只需要用包含一個(gè)字典的實(shí)例的 Line 來替換單個(gè) Line 而不用添加更多的屬性。 在 DrawView.swift 中氢惋,添加一個(gè)新屬性來替換 currentLine洞翩。

class DrawView: UIView {

??var currentLine: Line?
??var currentLines = [NSValue:Line]()

該線存儲(chǔ)在字典中的 key 將來自該線對(duì)應(yīng)的 UITouch 對(duì)象。 隨著更多的觸摸事件發(fā)生明肮,您可以使用相同的算法從觸發(fā)事件的 UITouch 導(dǎo)出 key菱农,并使用它在字典中查找適當(dāng)?shù)?Line

現(xiàn)在柿估,您需要更新 UIResponder 方法來添加當(dāng)前正在繪制到此字典的行循未。 在 DrawView.swift 中,更新 touchesBegan(_:with :) 中的代碼秫舌。

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
??let touch = touches.first!

??// Get location of the touch in view's coordinate system
??let location = touch.location(in: self)

??currentLine = Line(begin: location, end: location)

??// Log statement to see the order of events
??print(#function)

??for touch in touches {
????let location = touch.location(in: self)

????let newLine = Line(begin: location, end: location)

????let key = NSValue(nonretainedObject: touch)
????currentLines[key] = newLine
??}

??setNeedsDisplay()
}

在此代碼中的妖,您首先使用 #function 表達(dá)式打印出方法的名稱。 其次足陨,您列舉了所有觸摸的開始嫂粟,因?yàn)榭梢酝瑫r(shí)開始多個(gè)觸摸。 (通常墨缘,觸摸在不同的時(shí)間開始星虹,并且觸摸在每個(gè)觸摸的 DrawView 上多次調(diào)用 Bone(_:with :)。但你必須做好準(zhǔn)備镊讼,即使它不太可能發(fā)生)

接下來宽涌,請(qǐng)注意使用 NSValue(nonretainedObject :) 來導(dǎo)出存儲(chǔ) Line 的 key。 此方法創(chuàng)建一個(gè) NSValue 實(shí)例蝶棋,該實(shí)例將持有與該線關(guān)聯(lián)的 UITouch 對(duì)象的地址卸亮。 因?yàn)?UITouch 在觸摸開始時(shí)被創(chuàng)建,在其整個(gè)生命周期內(nèi)被更新玩裙,并且在觸摸結(jié)束時(shí)被破壞兼贸,所以通過每個(gè)觸摸事件處理方法該對(duì)象的地址將是不變的。 圖18.6顯示了新的事態(tài)吃溅。

圖18.6 多點(diǎn)觸控的 TouchTracker 的對(duì)象圖

你可能會(huì)想:為什么不使用 UITouch 本身作為 key溶诞? 為什么要通過創(chuàng)建 NSValue 來導(dǎo)出? UITouch 的文檔說决侈,你不應(yīng)該對(duì) UITouch 對(duì)象有很強(qiáng)的引用很澄。 內(nèi)存管理的細(xì)節(jié)超出了本書的范圍,但是為了避免創(chuàng)建對(duì)觸控對(duì)象的強(qiáng)大引用,您可以使用其 init(nonretainedObject :) 構(gòu)造器將 UITouch 的內(nèi)存地址包裝在 NSValue 的實(shí)例中甩苛。 該方法的文檔說明:“如果要將一個(gè)對(duì)象添加到集合中但是不希望集合創(chuàng)建一個(gè)強(qiáng)大的引用,這個(gè)方法很有用俏站,” 這正是你想要的讯蒲。 因?yàn)橄嗤?UITouch 對(duì)象被重復(fù)用于整個(gè)觸控的生命周期(因此具有相同的內(nèi)存地址),您可以使用相同的 UITouch 重新創(chuàng)建相同的 NSValue肄扎。

接下來墨林,在 DrawView.swift 中更新 touchesMoved(_:with :),以便它可以查找正確的線犯祠。

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
??let touch = touches.first!
??let location = touch.location(in: self)

??currentLine?.end = location

??// Log statement to see the order of events
??print(#function)
??for touch in touches {
????let key = NSValue(nonretainedObject: touch)
????currentLines[key]?.end = touch.location(in: self)
??}

??setNeedsDisplay()
}

現(xiàn)在旭等,更新 touchesEnded(_:with :) 將任何完成的線添加到 finishedLines 數(shù)組。

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
??if var line = currentLine {
????let touch = touches.first!
????let location = touch.location(in: self)
????line.end = location

????finishedLines.append(line)
??}
??currentLine = nil

??// Log statement to see the order of events
??print(#function)

??for touch in touches {
????let key = NSValue(nonretainedObject: touch)
????if var line = currentLines[key] {
??????line.end = touch.location(in: self)
??????finishedLines.append(line)
??????currentLines.removeValue(forKey: key)
????}
??}

??setNeedsDisplay()
}

最后衡载,更新 draw(_ :) 來繪制 currentLines 中的每一條線搔耕。

override func draw(_ rect: CGRect) {
??// Draw finished lines in black
??UIColor.black.setStroke()
??for line in finishedLines {
????stroke(line)
??}

??if let line = currentLine {
????If there is a line currently being drawn, do it in red
????UIColor.red.setStroke()
????stroke(line)
??}

??// Draw current lines in red
??UIColor.red.setStroke()
??for (_, line) in currentLines {
????stroke(line)
??}
}

構(gòu)建并運(yùn)行應(yīng)用程序,并用多個(gè)手指開始繪制線痰娱。 (在模擬器上弃榨,按住 Option(alt) 鍵的同時(shí)拖動(dòng)以模擬多根手指。)請(qǐng)注意控制臺(tái)中日志消息的順序梨睁。

您應(yīng)該知道鲸睛,當(dāng)在視圖上調(diào)用諸如 touchesMoved(_:with :) 之類的 UIResponder 方法時(shí),只有已移動(dòng)的觸摸將位于 touches 中坡贺。 因此官辈,三個(gè)觸摸可以在一個(gè)視圖上,但是在該組觸摸中只有一個(gè)觸摸被傳遞到這些方法之一中遍坟。 另外拳亿,一旦 UITouch 從視圖開始,即使該觸摸從其開始的視圖移開政鼠,所有觸摸事件方法都會(huì)在觸摸的生命周期中同一視圖中被調(diào)用风瘦。

TouchTracker 的基礎(chǔ)知識(shí)的最后一件事是處理取消觸摸時(shí)會(huì)發(fā)生什么。 當(dāng)應(yīng)用程序被操作系統(tǒng)中斷時(shí)(例如公般,當(dāng)電話進(jìn)入時(shí))万搔,當(dāng)觸摸當(dāng)前在屏幕上時(shí),可以取消觸摸官帘。 當(dāng)觸摸被取消時(shí)瞬雹,其設(shè)置的任何狀態(tài)都應(yīng)該被恢復(fù)。 在這種情況下刽虹,您應(yīng)該刪除正在繪制的任何線酗捌。

DrawView.swift 中,實(shí)現(xiàn) touchesCancelled(_:with :)

override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
??// Log statement to see the order of events
??print(#function)

??currentLines.removeAll()

??setNeedsDisplay()
}

@IBInspectable

Interface Builder 中工作時(shí)胖缤,您可以修改添加到畫布的視圖的屬性尚镰。 例如,您可以在視圖上設(shè)置背景顏色哪廓,標(biāo)簽上的文本以及滑塊上的當(dāng)前進(jìn)度狗唉。 您可以將相同的行為添加到您自己的某些類型的 UIView 子類。 我們可以通過 Interface Builder 增加 DrawView 當(dāng)前線條顏色涡真,最終線條顏色和線條厚度的功能分俯。

DrawView.swift 中,聲明三個(gè)屬性來引用這些值哆料。 給它們默認(rèn)值缸剪,并且只要這些屬性改變,都有自己的 view 標(biāo)志來重新繪制东亦。

var currentLines = [NSValue:Line]()
var finishedLines = [Line]()

@IBInspectable var finishedLineColor: UIColor = UIColor.black {
??didSet {
????setNeedsDisplay()
??}
}

@IBInspectable var currentLineColor: UIColor = UIColor.red {
??didSet {
????setNeedsDisplay()
??}
}

@IBInspectable var lineThickness: CGFloat = 10 {
??didSet {
????setNeedsDisplay()
??}
}

@IBInspectable 關(guān)鍵字讓 Interface Builder 知道這是您要通過屬性檢查器進(jìn)行自定義的屬性杏节。 許多常見類型由 @IBInspectable 支持:布爾值,字符串讥此,數(shù)字拢锹,CGPointCGSize萄喳,CGRect卒稳,UIColorUIImage 等等他巨。

現(xiàn)在更新 stroke(_ :)drawView(_ :) 來使用這些新屬性充坑。

func stroke(_ line: Line) {
??let path = UIBezierPath()
??path.lineWidth = 10
??path.lineWidth = lineThickness
??path.lineCapStyle = .round

??path.move(to: line.begin)
??path.addLine(to: line.end)
??path.stroke()
}

override func draw(_ rect: CGRect) {
??// Draw finished lines in black
??UIColor.black.setStroke()
??finishedLineColor.setStroke()
??for line in finishedLines {
????stroke(line)
??}

??// Draw current lines in red
??UIColor.red.setStroke()
??currentLineColor.setStroke()
??for (_, line) in currentLines {
????stroke(line)
??}
}

當(dāng)您在 Interface Builder 中將 DrawView 添加到畫布中時(shí),可以在屬性檢查器中自定義這三個(gè)屬性來改變實(shí)例(圖18.7)染突。

圖18.7 自定義DrawView

白銀挑戰(zhàn):顏色

一旦線被添加到 currentLines捻爷,按畫線的角度來決定它的顏色。

黃金挑戰(zhàn):圓圈

用兩根手指畫圓份企。 嘗試讓每個(gè)手指代表圍繞圓的邊框的一個(gè)角也榄。 回想一下,您可以通過按住 Option 鍵在模擬器上模擬兩根手指司志。 (提示:如果您在單獨(dú)的字典中跟蹤在一個(gè)圓圈上工作的觸摸甜紫,這會(huì)更容易)

更多:響應(yīng)者鏈

在第14章中,我們簡要介紹了第一響應(yīng)者骂远。 UIResponder 可以是第一響應(yīng)者并且接收觸摸事件囚霸。 UIViewUIResponder 子類的一個(gè)例子,但還有很多其他的激才,包括 UIViewController拓型,UIApplicationUIWindow额嘿。 你可能在想,“你不能觸摸 UIViewController劣挫。 它不是一個(gè)屏幕上的對(duì)象册养。” 你是對(duì)的——你不能直接向 UIViewController 發(fā)送觸摸事件压固,但視圖控制器可以通過 響應(yīng)者鏈(responder chain) 接收事件捕儒。

每個(gè) UIResponder 可以通過其 next 屬性來引用另一個(gè) UIResponder,并且這些對(duì)象組成響應(yīng)者鏈(圖18.8)邓夕。 觸摸事件從被觸動(dòng)的視圖開始。 視圖的 next 響應(yīng)者通常是它的 UIViewController(如果它有)或它的父視圖(如果沒有)阎毅。 視圖控制器的 next 響應(yīng)者通常是其視圖的父視圖焚刚。 最頂級(jí)的父視圖是窗口。 窗口的 next 響應(yīng)者是 UIApplication 的單例實(shí)例扇调。

圖18.8 響應(yīng)者鏈

UIResponder 如何處理一個(gè)事件矿咕? 它在 next 響應(yīng)者上調(diào)用相同的方法。 那就像 touchesBegan(_:with :) 這樣的方法的默認(rèn)實(shí)現(xiàn)狼钮。 所以如果一個(gè)方法不被覆蓋碳柱,其 next 響應(yīng)者將嘗試處理觸摸事件。 如果應(yīng)用程序(響應(yīng)者鏈中的最后一個(gè)對(duì)象)不處理該事件熬芜,則它將被丟棄莲镣。

您也可以在 next 響應(yīng)者上顯式調(diào)用方法。 假如在一個(gè)視圖中涎拉,發(fā)生雙擊事件瑞侮,它的 next 響應(yīng)者應(yīng)該處理它。 代碼將會(huì)如下所示:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
??let touch = touches.first!
??if touch.tapCount == 2 {
????next?.touchesBegan(touches, with: event)
??} else {
????// Go on to handle touches that are not double-taps
??}
}

更多:UIControl

UIControl 類是 Cocoa Touch 中很多類的父類鼓拧,包括 UIButtonUISlider半火。 您已經(jīng)了解了如何設(shè)置這些控件的 目標(biāo) 和 動(dòng)作。 現(xiàn)在我們可以仔細(xì)看看 UIControl 如何覆蓋在本章中實(shí)現(xiàn)的相同的 UIResponder 方法季俩。

UIControl 中钮糖,每個(gè)可能的 控制事件(control event) 與一個(gè)常數(shù)相關(guān)聯(lián)。 例如酌住,按鈕通常會(huì)在 UIControlEvents.touchUpInside 控件事件上發(fā)送動(dòng)作消息店归。 注冊(cè)該控制事件的目標(biāo)只有在用戶觸摸控制然后將手指從控件邊框內(nèi)的屏幕提起時(shí)才會(huì)收到其動(dòng)作消息。

但是赂韵,對(duì)于一個(gè)按鈕娱节,您還可以對(duì)其他事件類型執(zhí)行動(dòng)作。 例如祭示,如果用戶在邊框內(nèi)部或外部移除手指肄满,也可以觸發(fā)方法谴古。 以編程方式分配目標(biāo)和動(dòng)作將如下所示:

button.addTarget(self, action: #selector(Thermostat.resetTemperature(_:)), for: [.touchUpInside, .touchUpOutside])

現(xiàn)在考慮 UIControl 如何處理 UIControlEvents.touchUpInside

// Not the exact code. There is a bit more going on!
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {

??// Reference to the touch that is ending
??let touch = touches.first!

??// Location of that point in this control's coordinate system
??let touchLocation = touch.location(in: self)

??// Is that point still in my viewing bounds?
??if bounds.contains(touchLocation) {
????// Send out action messages to all targets registered for this event!
????sendActions(for: .touchUpInside)
??}
??else {
????// The touch ended outside the bounds: different control event
????sendActions(for: .touchUpOutside)
??}
}

那么這些動(dòng)作如何發(fā)送到正確的目標(biāo)呢稠歉? 在 UIResponder 方法實(shí)現(xiàn)的末尾掰担,控件調(diào)用了 sendActions(for :) 方法。 該方法查看控件具有的所有目標(biāo)動(dòng)作對(duì)怒炸。 如果其中任何一個(gè)注冊(cè)為作為參數(shù)傳遞的控制事件带饱,則對(duì)這些目標(biāo)調(diào)用相應(yīng)的動(dòng)作方法。

然而阅羹,控件不直接在其目標(biāo)上調(diào)用方法勺疼。 而是通過 UIApplication 對(duì)象來引導(dǎo)這些方法調(diào)用。 為什么控件不直接在目標(biāo)上調(diào)用動(dòng)作方法呢捏鱼? 控件也可以具有 空目標(biāo)(nil-targeted) 的動(dòng)作执庐。 如果 UIControl 的目標(biāo)為 nil,則 UIApplication 會(huì)找到其 UIWindow 的 第一響應(yīng)者导梆,并調(diào)用該方法轨淌。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市看尼,隨后出現(xiàn)的幾起案子递鹉,更是在濱河造成了極大的恐慌,老刑警劉巖藏斩,帶你破解...
    沈念sama閱讀 221,695評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件躏结,死亡現(xiàn)場離奇詭異,居然都是意外死亡灾茁,警方通過查閱死者的電腦和手機(jī)窜觉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來北专,“玉大人禀挫,你說我怎么就攤上這事⊥赝牵” “怎么了语婴?”我有些...
    開封第一講書人閱讀 168,130評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長驶睦。 經(jīng)常有香客問我砰左,道長,這世上最難降的妖魔是什么场航? 我笑而不...
    開封第一講書人閱讀 59,648評(píng)論 1 297
  • 正文 為了忘掉前任缠导,我火速辦了婚禮,結(jié)果婚禮上溉痢,老公的妹妹穿的比我還像新娘僻造。我一直安慰自己憋他,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,655評(píng)論 6 397
  • 文/花漫 我一把揭開白布髓削。 她就那樣靜靜地躺著竹挡,像睡著了一般。 火紅的嫁衣襯著肌膚如雪立膛。 梳的紋絲不亂的頭發(fā)上揪罕,一...
    開封第一講書人閱讀 52,268評(píng)論 1 309
  • 那天,我揣著相機(jī)與錄音宝泵,去河邊找鬼好啰。 笑死,一個(gè)胖子當(dāng)著我的面吹牛儿奶,可吹牛的內(nèi)容都是我干的坎怪。 我是一名探鬼主播,決...
    沈念sama閱讀 40,835評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼廓握,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了嘁酿?” 一聲冷哼從身側(cè)響起隙券,我...
    開封第一講書人閱讀 39,740評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎闹司,沒想到半個(gè)月后娱仔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,286評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡游桩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,375評(píng)論 3 340
  • 正文 我和宋清朗相戀三年牲迫,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片借卧。...
    茶點(diǎn)故事閱讀 40,505評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡盹憎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出铐刘,到底是詐尸還是另有隱情陪每,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布镰吵,位于F島的核電站檩禾,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏疤祭。R本人自食惡果不足惜盼产,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,873評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望勺馆。 院中可真熱鬧戏售,春花似錦侨核、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至紧卒,卻和暖如春侥衬,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背跑芳。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評(píng)論 1 272
  • 我被黑心中介騙來泰國打工轴总, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人博个。 一個(gè)月前我還...
    沈念sama閱讀 48,921評(píng)論 3 376
  • 正文 我出身青樓怀樟,卻偏偏與公主長得像,于是被迫代替她去往敵國和親盆佣。 傳聞我的和親對(duì)象是個(gè)殘疾皇子往堡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,515評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容

  • 這篇文章是我在閱讀相關(guān)蘋果官方文檔后總結(jié)整理出來的一些平陈腔遥可能不太注意到,但是又比較有用的知識(shí)點(diǎn)痹兜。如有錯(cuò)誤穆咐,歡迎指...
    我們是斗士閱讀 1,156評(píng)論 0 0
  • 本文來自:http://ios.jobbole.com/84081/ 前言: 按照時(shí)間順序,事件的生命周期是這樣的...
    HackerOnce閱讀 2,841評(píng)論 1 10
  • 好奇觸摸事件是如何從屏幕轉(zhuǎn)移到APP內(nèi)的字旭?困惑于Cell怎么突然不能點(diǎn)擊了对湃?糾結(jié)于如何實(shí)現(xiàn)這個(gè)奇葩響應(yīng)需求?亦或是...
    Lotheve閱讀 57,303評(píng)論 51 599
  • 在iOS開發(fā)中經(jīng)常會(huì)涉及到觸摸事件遗淳。本想自己總結(jié)一下拍柒,但是遇到了這篇文章,感覺總結(jié)的已經(jīng)很到位屈暗,特此轉(zhuǎn)載斤儿。作者:L...
    WQ_UESTC閱讀 6,024評(píng)論 4 26
  • 響應(yīng)者對(duì)象 在iOS中不是任何對(duì)象都能處理事件凌受,只有繼承了UIResponder的對(duì)象才能接收并處理事件逆害。我們稱之...
    JonesCxy閱讀 702評(píng)論 0 0