第十九章——UIGestureRecognizer 和 UIMenuController【譯】

在第十八章中,您通過實現 UIResponder 的方法來處理原始觸摸。 有時你想檢測一個特定的觸摸模式——手勢,如捏或滑動。 您可以使用 UIGestureRecognizer 的實例而不用自己寫代碼來檢測常用手勢。

UIGestureRecognizer 攔截由視圖處理的觸摸。 當它識別出一個特定的手勢時,它就會根據你選擇的對象調用一個方法。 SDK 中內置了多種類型的手勢識別器。 在本章中,您將使用其中三個來允許 TouchTracker 用戶選擇,移動和刪除線(圖19.1)。 您還將看到如何使用另一個有趣的 iOS 類,UIMenuController

圖19.1 本章末尾的TouchTracker

UIGestureRecognizer子類

您不需要自己去實例化 UIGestureRecognizer。 相反, UIGestureRecognizer 有很多子類,每個子類負責識別一個特定的手勢。

要使用 UIGestureRecognizer 子類的實例,請給它一個 目標動作對 并將其與視圖相關聯。 每當手勢識別器在視圖上識別其手勢時,它將向其目標發送動作消息。 所有 UIGestureRecognizer 動作消息具有相同的形式:

func action(_ gestureRecognizer: UIGestureRecognizer) { }

當識別手勢時,手勢識別器截取視圖內指定的觸摸(圖19.2)。 因此,在使用了手勢識別器的視圖上可能不會調用像 touchesBegan(_:with :) 這樣的典型的 UIResponder 方法。

圖19.2 手勢識別器攔截

用 UITapGestureRecognizer 檢測點擊

您將使用的第一個 UIGestureRecognizer 子類是 UITapGestureRecognizer。 當用戶點擊屏幕兩次時,屏幕上的所有線將被清除。

打開 TouchTracker.xcodeprojDrawView.swift。 添加一個 init?(coder :) 方法并實例化一個 UITapGestureRecognizer,需要兩次點擊來觸發并調用其目標上的動作方法。

required init?(coder aDecoder: NSCoder) {
??super.init(coder: aDecoder)

??let doubleTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(DrawView.doubleTap(_:)))
??doubleTapRecognizer.numberOfTapsRequired = 2
??addGestureRecognizer(doubleTapRecognizer)
}

現在當在 DrawView 的一個實例上發生雙擊時,將在該實例上調用 doubleTap(_ :) 方法。 在 DrawView.swift 中實現此方法。

func doubleTap(_ gestureRecognizer: UIGestureRecognizer) {
??print("Recognized a double tap")

??currentLines.removeAll()
??finishedLines.removeAll()
??setNeedsDisplay()
}

請注意,手勢識別器的動作方法的參數是調用該方法的 UIGestureRecognizer 的實例。 在雙擊的情況下,您不需要識別器的任何信息,但在本章后面您將需要用到其他識別器的信息。

構建并運行應用程序,繪制幾條線,然后雙擊屏幕以清除它們。

您可能已經注意到(特別是在模擬器上),第一次點擊雙擊會導致繪制一個小紅點。 之所以出現這個點,是因為在第一次點擊時,我們在 DrawView 上調用了 touchesBegan(_:with :),創建了一條很短的線。 檢查控制臺,您將看到以下事件序列:

touchesBegan(_:with:)
Recognized a double tap
touchesCancelled(_:with:)

手勢識別器通過檢查觸摸事件來確定他們的特定手勢是否發生。 在識別手勢之前,手勢識別器攔截所有的 UIResponder 方法調用。 如果它沒有識別出是某個手勢,那么每個調用都將轉發回到視圖中去。

識別點擊需要觸摸的開始和結束。 這意味著當最初調用 touchesBegan(_:with :) 時,UITapGestureRecognizer 不知道該觸摸是否是點擊,因此還是會在視圖上調用該方法。 當觸摸結束時,識別出為點擊,手勢識別器會將該觸摸聲明為自己所有。 它通過在視圖上調用 touchesCancelled(_:with :) 來實現。 之后,其它的 UIResponder 方法將無法在該視圖中調用。

要防止這個紅點暫時出現,您必須防止在視圖中調用 touchesBegan(_:with :)。 您可以告訴 UIGestureRecognizer 在其視圖上延遲調用 touchesBegan(_:with :),因為該觸摸有可能會被識別為手勢。

DrawView.swift 中,修改 init?(coder :) 來實現。

required init?(coder aDecoder: NSCoder) {
??super.init(coder: aDecoder)

??let doubleTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(DrawView.doubleTap(_:)))
??doubleTapRecognizer.numberOfTapsRequired = 2
??doubleTapRecognizer.delaysTouchesBegan = true
??addGestureRecognizer(doubleTapRecognizer)
}

構建并運行應用程序,繪制一些線,然后雙擊以清除它們。 雙擊時,您將不會再看到紅點。

多種手勢識別器

下一步是添加另一個允許用戶選擇一條線的手勢識別器。 (稍后,用戶將能夠刪除所選行。)您將在 DrawView 上添加只需要一次點擊就觸發的另一個 UITapGestureRecognizer

DrawView.swift 中,修改 init?(coder :) 來添加這個手勢識別器。

required init?(coder aDecoder: NSCoder) {
??super.init(coder: aDecoder)

??let doubleTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(DrawView.doubleTap(_:)))
??doubleTapRecognizer.numberOfTapsRequired = 2
??doubleTapRecognizer.delaysTouchesBegan = true
??addGestureRecognizer(doubleTapRecognizer)

??let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(DrawView.tap(_:)))
??tapRecognizer.delaysTouchesBegan = true
??addGestureRecognizer(tapRecognizer)
}

現在,在 DrawView.swift 中實現 tap(_ :),將點擊記錄到控制臺。

func tap(_ gestureRecognizer: UIGestureRecognizer) {
??print("Recognized a tap")
}

構建并運行應用程序。 嘗試點擊和雙擊。 點擊一次將適當的消息記錄到控制臺。 然而,雙擊可以觸發 tap(_:)doubleTap(_ :) 兩個方法。

在具有多個手勢識別器的情況下,一個手勢識別器識別到該觸摸并處理,但其實你想要讓另一個手勢識別器去處理這個手勢,這是很常見的。 在這些情況下,您可以在識別器之間設置依賴關系,這些依賴關系就像:“等等你先別急,這個觸摸可能是我的!”

init?(coder:) 中,使 tapDecognizer 等待直到 doubleTapRecognizer 無法識別雙擊才能聲明為其自身的點擊。

required init?(coder aDecoder: NSCoder) {
??super.init(coder: aDecoder)

??let doubleTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(DrawView.doubleTap(_:)))
??doubleTapRecognizer.numberOfTapsRequired = 2
??doubleTapRecognizer.delaysTouchesBegan = true
??addGestureRecognizer(doubleTapRecognizer)

??let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(DrawView.tap(_:)))
??tapRecognizer.delaysTouchesBegan = true
??tapRecognizer.require(toFail: doubleTapRecognizer)
??addGestureRecognizer(tapRecognizer)
}

再次構建并運行應用程序,并嘗試一些點擊。 點擊發生后,單擊現在需要少量時間去觸發,但雙擊不再觸發 tap(_:) 消息。

接下來,讓我們在 DrawView 上構建,以便用戶可以在點擊時選擇一條線。 首先,在 DrawView.swift 的頂部添加一個屬性來保存所選線的索引。

class DrawView: UIView {
??var currentLines = [NSValue:Line]()
??var finishedLines = [Line]()
??var selectedLineIndex: Int?

現在修改 draw(_ :) 將所選線畫成綠色。

override func draw(_ rect: CGRect) {
??finishedLineColor.setStroke()
??for line in finishedLines {
????stroke(line)
??}

??currentLineColor.setStroke()
??for (_,line) in currentLines {
????stroke(line)
??}

??if let index = selectedLineIndex {
????UIColor.green.setStroke()
????let selectedLine = finishedLines[index]
????stroke(selectedLine)
??}
}

仍然在 DrawView.swift 中,添加一個 indexOfLine(at :) 方法返回離給定點最接近的 Line 的索引。

func indexOfLine(at point: CGPoint) -> Int? {
??// Find a line close to point
??for (index, line) in finishedLines.enumerated() {
????let begin = line.begin
????let end = line.end

????// Check a few points on the line
????for t in stride(from: CGFloat(0), to: 1.0, by: 0.05) {
??????let x = begin.x + ((end.x - begin.x) * t)
??????let y = begin.y + ((end.y - begin.y) * t)

??????// If the tapped point is within 20 points, let's return this line
??????if hypot(x - point.x, y - point.y) < 20.0 {
????????return index
??????}
????}
??}

??// If nothing is close enough to the tapped point, then we did not select a line
??return nil
}

stride(from:to:by:) 方法將允許 t 從 from 值開始,并且增加到(但不達到) to 值,每次增量為 by

還有其他更好的方法可以確定最接近某個點的線條,但是這個簡單的實現已經達到目的了。

要傳遞的 point 是手指點擊的點。 您可以輕松獲取此信息。 每個 UIGestureRecognizer 都有一個 location(in :) 方法。 在手勢識別器上調用此方法將給出在作為參數傳遞的視圖的坐標系中觸發手勢的坐標。

DrawView.swift 中,更新 tap(_ :) 來調用手勢識別器的 location(in:),將結果傳遞給 indexOfLine(in:),并將返回的索引賦給 selectedLineIndex

func tap(_ gestureRecognizer: UIGestureRecognizer) {
??print("Recognized a tap")

??let point = gestureRecognizer.location(in: self)
??selectedLineIndex = indexOfLine(at: point)

setNeedsDisplay()
}

如果用戶在選擇一條線時使用了雙擊來清除所有線,應用程序將崩潰。 要解決這個問題,請更新 doubleTap(_ :)selectedLineIndex 設置為 nil

func doubleTap(_ gestureRecognizer: UIGestureRecognizer) {
??print("Recognized a double tap")

??selectedLineIndex = nil
??currentLines.removeAll()
??finishedLines.removeAll()
??setNeedsDisplay()
}

構建并運行應用程序。 畫幾條線,然后點擊其中一條。 點擊的線應顯示為綠色(請記住,在點擊而不是雙擊被識別出來之前需要一些時間)。

UIMenuController

接下來,您需要做到當用戶選擇一條線時,具有刪除該行的選項的菜單將顯示在用戶點擊的位置。 有一個內置的類用于提供這種菜單,稱為 UIMenuController(圖19.3)。 菜單控制器具有 UIMenuItem 對象的列表,并且在現有視圖中呈現。 每個 item 都有一個標題(菜單中顯示的內容)和一個動作(它發送到窗口的第一響應者的消息)。

圖19.3 UIMenuController

每個應用程序只有一個 UIMenuController。 當你想呈現這個實例時,你可以用菜單項來填充它,給它一個矩形來呈現,并將其設置為可見。

DrawView.swifttap(_ :) 方法中執行如下操作,如果用戶已經點擊了一條線。 如果用戶點擊不在該線附近的某處,則當前選中的行將被取消選擇,并且菜單控制器將被隱藏。

func tap(_ gestureRecognizer: UIGestureRecognizer) {
??print("Recognized a tap")

??let point = gestureRecognizer.location(in: self)
??selectedLineIndex = indexOfLine(at: point)

??// Grab the menu controller
??let menu = UIMenuController.shared

??if selectedLineIndex != nil {

????// Make DrawView the target of menu item action messages
????becomeFirstResponder()

????// Create a new "Delete" UIMenuItem
????let deleteItem = UIMenuItem(title: "Delete",action: #selector(DrawView.deleteLine(_:)))
????menu.menuItems = [deleteItem]

????// Tell the menu where it should come from and show it
????let targetRect = CGRect(x: point.x, y: point.y, width: 2, height: 2)
????menu.setTargetRect(targetRect, in: self)
????menu.setMenuVisible(true, animated: true)
??} else {
????// Hide the menu if no line is selected
????menu.setMenuVisible(false, animated: true)
??}

??setNeedsDisplay()
}

要顯示菜單控制器,在 UIMenuController 的菜單項中響應至少一個動作消息的視圖必須是窗口的第一響應者——這就是為什么在設置菜單控制器之前,要在 DrawView 上調用方法 becomeFirstResponder()

如果您的自定義視圖類要成為第一響應者,則還必須覆蓋 canBecomeFirstResponder。 在 DrawView.swift 中,覆蓋此屬性以返回 true

override var canBecomeFirstResponder: Bool {
??return true
}

最后,在 DrawView.swift 中實現 deleteLine(_ :)

func deleteLine(_ sender: UIMenuController) {
??// Remove the selected line from the list of finishedLines
??if let index = selectedLineIndex {
????finishedLines.remove(at: index)
????selectedLineIndex = nil

????// Redraw everything
????setNeedsDisplay()
??}
}

當被呈現時,菜單控制器通過每個菜單項并且詢問第一響應者是否實現該項的動作方法。 如果第一響應者沒有實現該方法,則菜單控制器將不會顯示相關的菜單項。 如果菜單項沒有由第一響應者實現的動作方法,則完全不顯示該菜單。

構建并運行應用程序。 畫一條線,點擊它,然后從菜單項中選擇 Delete

選擇一條線,然后雙擊以清除所有行,而菜單控制器仍然可見。 如果 selectedLineIndexnil,菜單控制器應該為不可見才對。

DrawView.swift 中向 propertiesLineIndex 添加屬性觀察器,如果索引設置為 nil,則將菜單控制器設置為不可見。

var selectedLineIndex: Int? {
??didSet {
????if selectedLineIndex == nil {
??????let menu = UIMenuController.shared
??????menu.setMenuVisible(false, animated: true)
????}
??}
}

構建并運行應用程序。 畫一條線,選擇它,然后雙擊背景。 線和菜單控制器將不再可見。

更多手勢識別器

在本節中,您將添加用戶通過長按來選擇線的功能,然后通過平移移動所選線。 這將需要用到另外兩個 UIGestureRecognizer 子類:

UILongPressGestureRecognizerUIPanGestureRecognizer

UILongPressGestureRecognizer

DrawView.swift 中,在 init?(coder:) 中實例化一個 UILongPressGestureRecognizer 并將其添加到 DrawView

...
??addGestureRecognizer(tapRecognizer)

??let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(DrawView.longPress(_:)))
??addGestureRecognizer(longPressRecognizer)
}

當用戶按住 DrawView 時,將會調用 longPress(_ :) 方法。 默認情況下,觸摸必須持續 0.5 秒才被視為長按,但如果你想修改的話,可以更改手勢識別器的 minimumPressDuration

到目前為止,您已經使用過手勢了。 點擊 是一個 獨立(discrete) 的手勢。 當被識別時,手勢已經結束,并且已經傳送了動作消息。 另一方面,長按是一個 持續(continuous) 的手勢。 持續的手勢隨著時間推移而發生,為了跟蹤持續的手勢發生了什么,您可以檢查識別器的state` 屬性。

例如,考慮一個典型的長按:

  • 當用戶觸摸視圖時,長按識別器會注意到 可能(possible) 是一個長按,但是它必須等待觀看觸摸是否持續足夠長以成為長按手勢。 識別器的狀態是 UIGestureRecognizerState.possible
  • 一旦用戶持續足夠長的時間,長按可以被識別并且手勢已經 開始(began)。 識別器的狀態是 UIGestureRecognizerState.began
  • 當用戶移除手指時,手勢已經 結束(ended)。 識別器的狀態是 UIGestureRecognizerState.ended

當長按手勢識別器從 possible 轉移到 began 和從 began 到 ended 時,它將其動作消息發送到其目標。 要確定在哪個轉換中觸發動作,可以查看手勢識別器的 state

請記住,長按是較大功能的一部分。 在下一節中,您將使用戶可以通過用長按開始的同一個手指拖動所選直線來移動所選直線線。 所以這里是實現 longPress(_ :) 動作方法的計劃:當識別器處于 began 狀態時,您將選擇距離手勢發生的最近的直線。 當識別器處于 ended 狀態時,您將取消選擇該線。

DrawView.swift 中,實現 longPress(_ :)

func longPress(_ gestureRecognizer: UIGestureRecognizer) {
??print("Recognized a long press")

??if gestureRecognizer.state == .began {
????let point = gestureRecognizer.location(in: self)
????selectedLineIndex = indexOfLine(at: point)

????if selectedLineIndex != nil {
??????currentLines.removeAll()
????}
??} else if gestureRecognizer.state == .ended {
????selectedLineIndex = nil
??}

??setNeedsDisplay()
}

構建并運行應用程序。 畫一條直線,然后按住它; 該行將變為綠色并成為所選行。 當您放開時,該行將恢復為其之前的顏色,并且將不再是所選行。

UIPanGestureRecognizer 和 同時識別器

DrawView.swift 中,聲明一個 UIPanGestureRecognizer 作為一個屬性,以便您可以在所有方法中訪問它。

class DrawView: UIView {

??var currentLines = [NSValue:Line]()
??var finishedLines = [Line]()
??var selectedLineIndex: Int? {
????...
??}
??var moveRecognizer: UIPanGestureRecognizer!

接下來,在 DrawView.swift 中,添加代碼到 init?(coder :) 來實例化一個 UIPanGestureRecognizer,設置其中的一個屬性,并將其添加到 DrawView

let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(DrawView.longPress(_:)))
??addGestureRecognizer(longPressRecognizer)

??moveRecognizer = UIPanGestureRecognizer(target: self, action: #selector(DrawView.moveLine(_:)))
??moveRecognizer.cancelsTouchesInView = false
??addGestureRecognizer(moveRecognizer)
}

cancelsTouchesInView 是什么? 每個 UIGestureRecognizer 都具有此屬性,默認為 true。 當 cancelsTouchesInViewtrue 時,手勢識別器將 “吃” 掉任何可識別的觸摸,并且視圖將無法通過傳統的 UIResponder 方法,例如 touchesBegan(_:with :) 來處理觸摸。

通常,這是你想要的,但不總是。 在這種情況下,如果平移手勢識別器要觸摸它,則用戶將無法繪制線條。 當您將 cancelsTouchesInView 設置為 false 時,您確保手勢識別器識別的任何觸摸也將通過 UIResponder 方法傳遞到視圖。

DrawView.swift 中,為動作方法添加一個簡單的實現:

func moveLine(_ gestureRecognizer: UIPanGestureRecognizer) {
??print("Recognized a pan")
}

構建并運行應用程序并繪制一些直線。 因為 cancelsTouchesInViewfalse,所以可以識別平移手勢,也可以繪制線條。 您可以注釋掉設置 cancelsTouchesInView 的代碼行,再次運行以查看差異。

接下來,當用戶的手指移動到屏幕上時,您將更新 moveLine(_ :) 來重繪所選線。 但首先,您需要兩個手勢識別器才能處理相同的觸摸。 通常,當手勢識別器識別出其手勢時,它會吃掉它,而其他識別器沒有機會處理該觸摸。 嘗試一下:運行應用程序,畫一條線,按住選擇該線,然后移動你的手指。 控制臺報告是長按而不是平移。

在這種情況下,默認行為是有問題的:為了按住并選擇一條線,然后平移以移動線,在這過程中您的用戶是不會將手指抬起的。 因此,兩個手勢應該同時發生,并且即使長按手勢已經識別長按,也必須允許平移手勢識別器識別平移。

為了允許手勢識別器與其他手勢識別器同時識別其手勢,您可以從 UIGestureRecognizerDelegate 協議實現一種方法:

optional func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool

第一個參數是要求引導的手勢識別器。 它對其 委托 說:“如果在我和另一個的識別器之中的某一個剛剛識別出了一個手勢。 那那個不識別的應該保持在 possible 的狀態,并繼續追蹤這個觸摸嗎?”

請注意,調用本身并沒有告訴您兩個識別器中哪一個已經識別出它的手勢,因此,有的可能被剝奪了識別其手勢的機會。

默認情況下,該方法返回 false,并且手勢識別器仍處于 possible 狀態,使手勢中的觸摸處于 recognized 狀態。 您可以實現返回 true 的方法,以允許兩個識別器在相同的觸摸中識別各自的手勢。 (如果您需要確定兩個識別器中的哪一個識別其手勢,則可以檢查它們的 state 屬性。)

要長時間啟用平移功能,您將要給平移手勢識別器一個委托(DrawView)。 然后,當長按識別器識別其手勢時,平移手勢識別器將在其委托上調用同時識別方法。 您將在 DrawView 中實現此方法返回 true。 這將允許平移手勢識別器識別在長按進行過程中發生的任何平移。

首先,在 DrawView.swift 中,聲明 DrawView 符合 UIGestureRecognizerDelegate 協議。

class DrawView: UIView, UIGestureRecognizerDelegate {

??var currentLines = [NSValue:Line]()
??var finishedLines = [Line]()
??var selectedLineIndex: Int? {
??...
}
var moveRecognizer: UIPanGestureRecognizer!

接下來,在 init?(coder :) 中,將 DrawView 設置為 UIPanGestureRecognizer 的委托。

let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(DrawView.longPress(_:)))
??addGestureRecognizer(longPressRecognizer)

??moveRecognizer = UIPanGestureRecognizer(target: self, action: #selector(DrawView.moveLine(_:)))
??moveRecognizer.delegate = self
??moveRecognizer.cancelsTouchesInView = false
??addGestureRecognizer(moveRecognizer)
}

最后,在 DrawView.swift 中,實現委托方法并返回 true

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
??return true
}

對于這種情況,只有您的平移手勢識別器有一個委托,除了返回 true 則不需要做更多的事情。 在更復雜的情況下,您可以使用傳入的手勢識別器來更仔細地控制同時識別。

現在,當長按開始時,UIPanGestureRecognizer 將繼續跟蹤觸摸,如果用戶的手指開始移動,則識別器將識別平移。 要看到差異,運行應用程序,畫一條線,選擇它,然后平移。 控制臺將報告兩種手勢。(UIGestureRecognizerDelegate 協議包括其他方法來幫助您調整手勢識別器的行為。訪問協議參考頁面了解更多信息。)

除了您已經看到的狀態之外,平移手勢識別器支持 changed 狀態。 當手指開始移動時,識別器進入 began 狀態并調用其目標上的方法。 當手指圍繞屏幕移動時,識別器轉換到 changed 的狀態,并重復地在其目標上調用動作方法。 當手指離開屏幕時,識別器的狀態被設置為 ended,并且最終在目標上調用該方法。

下一步是實現平移識別器在其目標上調用的 moveLine(_ :) 方法。 在這個實現中,您將在平移識別器上調用 translationInView(_ :) 方法。 該 UIPanGestureRecognizer 方法返回在作為參數傳遞的視圖的坐標系中,平移點作為 CGPoint 移動的距離。 當平移手勢開始時,該屬性設置為零點(其中 xy 為0)。 隨著不斷的平移,這個值被更新——如果向右平移,它具有高 x 值; 如果平移返回到它開始的位置,它的 translation 將回到零點。

DrawView.swift 中,實現 moveLine(_ :)。 請注意,因為您將從 UIPanGestureRecognizer 類發送手勢識別器方法,所以該方法的參數必須是 UIPanGestureRecognizer 的實例的引用,而不是 UIGestureRecognizer

func moveLine(_ gestureRecognizer: UIPanGestureRecognizer) {
??print("Recognized a pan")

??// If a line is selected...
??if let index = selectedLineIndex {
????// When the pan recognizer changes its position...
????if gestureRecognizer.state == .changed {
??????// How far has the pan moved?
??????let translation = gestureRecognizer.translation(in: self)

??????// Add the translation to the current beginning and end points of the line
??????// Make sure there are no copy and paste typos!
??????finishedLines[index].begin.x += translation.x
??????finishedLines[index].begin.y += translation.y
??????finishedLines[index].end.x += translation.x
??????finishedLines[index].end.y += translation.y

??????// Redraw the screen
??????setNeedsDisplay()
????}
??} else {
????// If no line is selected, do not do anything

return
??}
}

構建并運行應用程序。 觸摸并按住一條線并開始拖動——您會發現該線和您的手指不同步。 這到底是怎么回事?

您正在將該當前的 translation 重復添加到該線的原始終點。 當您最后一次調用此方法時,您實際上需要讓手勢識別器來報告 translation 中的更改。 幸運的是,你可以做到這一點。 每次報告更改時,您可以將平移手勢識別器的 translation 設置回零點。 然后,下一次報告更改時,它將獲得自上次發生以來的 translation。

DrawView.swift 中的 moveLine(_ :) 底部附近添加以下代碼行。

finishedLines[index].end.x += translation.x
finishedLines[index].end.y += translation.y

gestureRecognizer.setTranslation(CGPoint.zero, in: self)

// Redraw the screen
setNeedsDisplay()

構建并運行應用程序并移動一條線。 效果還不錯

關于 UIGestureRecognizer 的更多信息##

你只是用到了 UIGestureRecognizer 的一些基礎知識。 它還有更多的子類,更多的屬性和更多的委托方法——你甚至可以創建自己的識別器。 本節將為講解關于 UIGestureRecognizer 其它的信息。 您可以查看文檔以了解更多信息。

當手勢識別器在視圖中時,它確實會為您處理所有的 UIResponder 方法,如 touchesBegan(_:with :)。 手勢識別器是非常貪心的,所以他們通常不會讓視圖接觸到觸摸事件,或者他們至少延遲交付這些事件。 您可以在識別器上設置屬性,如 delayedTouchesBegandelaysTouchesEndedcancelsTouchesInView 以更改此行為。 如果您需要比這種全無變化的方法更好的控制,您可以為識別器實現委托方法。

有時,您可能會有兩個手勢識別器尋找非常相似的手勢。 您可以將識別器鏈接在一起,以便一個失敗后下一個才能開始使用 require(toFail :) 方法。 你在 init?(coder :) 中使用了這個方法,使得點擊識別器等待雙擊識別器失敗。

掌握手勢識別器必須了解的一件事是他們如何解釋他們的狀態。 總體來說,識別器可以輸入七個狀態:

  • UIGestureRecognizerState.possible
  • UIGestureRecognizerState.failed
  • UIGestureRecognizerState.began
  • UIGestureRecognizerState.cancelled
  • UIGestureRecognizerState.changed
  • UIGestureRecognizerState.recognized
  • UIGestureRecognizerState.ended

識別器花費大部分時間在 possible 狀態。 當手勢轉換到 possible 狀態或 failed 狀態之外的任何狀態時,識別器的動作消息被發送,并且可以查看其 state 屬性以了解原因。

failed 狀態用于識別器等待多點觸控手勢。 在某些時候,用戶的手指可能會達到一個位置,從這個位置他們再也不能夠識別識別器的手勢。 那么手勢識別器識別失敗。 識別器在中斷時進入 canceled 的狀態,例如通過來電。

如果手勢是連續的如平移,那手勢識別器將進入 began 狀態,然后進入 changed 狀態,直到手勢結束。 當手勢結束(或 canceled 狀態 )時,識別器進入 ended(或 canceled)狀態,并在返回到 possible 狀態之前最后發送其動作消息。

對于識別離散手勢(如點擊)手勢識別器,您只能看到 recognized 狀態(與 ended 狀態具有相同的值)。

您在本章中未實現的四個內置識別器是 UIPinchGestureRecognizerUISwipeGestureRecognizerUIScreenEdgePanGestureRecognizerUIRotationGestureRecognizer。 每個都具有允許您微調其行為的屬性。 文檔將向您展示怎么做。

最后,如果您想要識別的手勢不是由 UIGestureRecognizer 的內置子類實現的,那么您可以自己對 UIGestureRecognizer 進行子類化。 這不在本書的范圍之內。 您可以閱讀 UIGestureRecognizer 文檔的 Methods for Subclassing 部分,了解需要的內容。

白銀挑戰:神秘線

應用程序中有一個 bug。 如果您點擊一條線,然后在菜單可見時開始繪制新的,您將拖動所選行并同時繪制新線。 修復這個bug。

黃金挑戰:速度和大小

當你在畫一條線的時候,你可以用手勢識別器來記錄下平移的速度。根據這個速度調整線的厚度。不要只靠猜測 pan 識別器的速度值可以設為多大或能有多大。(換句話說,將各種速度記錄到控制臺。)

白金挑戰:顏色

用三指向上滑動可以彈出一個顯示顏色的面板。 選擇這些顏色之一應該使您以后繪制的任何線條以該顏色顯示。 通過放置該面板不應該畫出額外的線,或者當應用程序意識到處理三指滑動時,應立即刪除任何繪制的線。

更多:UIMenuController 和 UIResponderStandardEditActions

UIMenuController 通常負責在顯示時向用戶展示一個“編輯”菜單。 (當您按住文本字段或文本視圖時。)因此,未修改的菜單控制器(您未設置菜單項的控件)已經具有其所呈現的默認菜單項,如 剪切(Cut),復制(Copy) 和其他熟悉的選項。 每項都有一個相關聯的動作消息。 例如,當 Cut 菜單項被點擊時,cut: 被發送到呈現菜單控制器的視圖。

UIResponder 的所有實例都會實現這些方法,但是默認情況下,這些方法不會執行任何操作。 像 UITextField 這樣的子類會覆蓋這些方法,以便對其上下文進行適當的操作,例如剪切當前選擇的文本。 這些方法都在 UIResponderStandardEditActions 協議中聲明。

如果您在視圖中覆蓋了 UIResponderStandardEditActions 的方法,則其菜單項將自動顯示在您為該視圖顯示的任何菜單中。 這是因為菜單控制器在其視圖上調用 canPerformAction(_:withSender :) 方法,它根據視圖是否實現此方法返回 truefalse

如果要實現這些方法之一,但不希望它出現在菜單中,可以覆蓋 canPerformAction(_:withSender :) 返回 false

override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {

??if action == #selector(copy(_:)) {
????return false
??} else {
????// Else return the default behavior
????return super.canPerformAction(action, withSender: sender)
??}
}

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,983評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,772評論 3 422
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,947評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,201評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,960評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,350評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,406評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,549評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,104評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,914評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,089評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,647評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,340評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,753評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,007評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,834評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,106評論 2 375

推薦閱讀更多精彩內容

  • 手勢識別器是附加到視圖的對象,將低級別事件處理代碼轉換為更高級別的操作,它允許視圖以控件執行的方式響應操作。 手勢...
    坤坤同學閱讀 4,114評論 0 9
  • 手勢識別器(Gesture Recognizer)用于識別觸摸序列并觸發響應事件。當手勢識別器識別到一個手勢或手勢...
    pro648閱讀 6,074評論 0 13
  • 0、緣起 之所以要寫這篇文章,是因為發現在實際編程處理點擊事件的過程中,知道響應鏈和探測鏈根本沒有一點用處。 即使...
    吳佩在天涯閱讀 44,159評論 33 127
  • block對象簡介及語法 什么是block? block對象是一組指令,可以像調用函數指令那樣調用block對象。...
    dreamCatcher閱讀 1,191評論 0 14
  • 1.今天早上學習了時間成本 2.今天早上七點出門去地鐵蘇州街B口發資料(只是發資料),有2個人主動加我,有兩個人意...
    鵬鵬YH閱讀 230評論 1 2