在接下來的兩章中茶鹃,您將創(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网持,UIViewController 和 UIView 的子類。
Line 將是一個(gè) 結(jié)構(gòu)體(struct)长踊。 您在本書中使用了結(jié)構(gòu)體-- CGRect功舀,CGSize 和 CGPoint 都是結(jié)構(gòu)體。 String身弊,Int辟汰,Array 和 Dictionary 也是如此列敲。 現(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ǔ)為名為 begin
和 end
的屬性靶庙。 觸摸開始時(shí)问畅,您將創(chuàng)建一個(gè) Line,并將其屬性設(shè)置為觸摸開始點(diǎn)六荒。 觸摸移動(dòng)時(shí)护姆,您將更新 Line 的 end
。 當(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 :)族阅,以便更新 currentLine
的 end
屬性篓跛。
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)建更多的屬性,如 currentLine1
和 currentLine2
只搁,但是管理哪個(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ù)字拢锹,CGPoint,CGSize萄喳,CGRect卒稳,UIColor,UIImage 等等他巨。
現(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)者并且接收觸摸事件囚霸。 UIView 是 UIResponder 子類的一個(gè)例子,但還有很多其他的激才,包括 UIViewController拓型,UIApplication 和 UIWindow额嘿。 你可能在想,“你不能觸摸 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 中很多類的父類鼓拧,包括 UIButton 和 UISlider半火。 您已經(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)用該方法轨淌。