ประเด็นสำคัญ
UIGestureRecognizer คือคลาสพื้นฐานนามธรรมใน UIKit ที่แยกกระบวนการรับรู้ท่าทางออกเป็นออบเจ็กต์แยกกัน แต่ละคลาสย่อยจัดการท่าทางหนึ่งประเภท: UITapGestureRecognizer จัดการการแตะด้วยจำนวนสัมผัสที่ระบุ UISwipeGestureRecognizer จัดการการปัดในทิศทางที่กำหนด นักพัฒนาเพิ่มตัวรับรู้ไปยัง UIView ผ่านเมธอด addGestureRecognizer(:) และ UIKit จะติดตามการสัมผัสโดยอัตโนมัติ อัปเดตสถานะ และเรียกใช้การดำเนินการเมื่อรับรู้
ก่อน UIGestureRecognizer (iOS 3.2, 2010) นักพัฒนาเขียนทับเมธอด UIResponder — touchesBegan, touchesMoved, touchesEnded — และวิเคราะห์เส้นทางการสัมผัสด้วยตนเอง สิ่งนี้นำไปสู่การทำซ้ำโค้ดและข้อผิดพลาดเมื่อจัดการกับการสัมผัสพร้อมกัน Apple ห่อหุ้มตรรกะนี้ใน UIGestureRecognizer เพิ่มการรองรับการสัมผัสหลายจุด การยกเลิกท่าทาง และการทำงานพร้อมกันของตัวรับรู้หลายตัวบนวิวเดียวกัน
ตามเซสชัน WWDC Gesture Recognizer จัดการการสัมผัสพร้อมกันสูงสุด 11 จุด บน iPad และ 5 จุดบน iPhone ที่ IT Sectr เราใช้ UIGestureRecognizer เป็นวิธีมาตรฐานในการจัดการอินพุตของผู้ใช้ในทุกโปรเจกต์ UIKit — ซึ่งช่วยกำจัดบักที่เกี่ยวข้องกับการติดตาม touchesBegan ด้วยตนเอง
UIGestureRecognizer แต่ละตัวผ่าน 7 สถานะที่เป็นไปได้ ซึ่งกำหนดใน enum UIGestureRecognizer.State สถานะเหล่านี้สะท้อนวงจรชีวิตของการรับรู้: จากการตรวจจับการสัมผัสไปจนถึงการเสร็จสมบูรณ์หรือการยกเลิก การทำความเข้าใจสถานะเป็นสิ่งสำคัญสำหรับการใช้งานตัวรับรู้แบบกำหนดเองและการดีบักข้อขัดแย้ง
| สถานะ | ความหมาย | เมื่อเกิดขึ้น |
|---|---|---|
| .possible | สถานะเริ่มต้น ยังไม่รับรู้ท่าทาง | ทันทีหลังจากเพิ่มไปยังวิว |
| .began | รับรู้ท่าทางและเริ่มดำเนินการ | เมื่อนิ้วเคลื่อนที่ครั้งแรกสำหรับ pan/longPress |
| .changed | พารามิเตอร์ของท่าทางเปลี่ยนไป (พิกัด, มุม) | ทุกครั้งที่นิ้วเคลื่อนที่ |
| .ended | ผู้ใช้ยกนิ้วขึ้น ท่าทางเสร็จสมบูรณ์ | ที่ touchesEnded |
| .cancelled | ท่าทางถูกขัดจังหวะโดยระบบ (สายเรียกเข้า, การเปลี่ยนทิศทาง) | ที่ touchesCancelled |
| .failed | ไม่รับรู้ท่าทางตามเงื่อนไข | ที่ touchesCancelled โดยไม่มีการรับรู้ |
| .recognized | คำพ้องของ .ended; รับรู้ท่าทางสำเร็จ | เหมือน .ended |
ท่าทางไม่ต่อเนื่อง (tap, swipe) เปลี่ยนจาก .possible ไปยัง .ended หรือ .failed โดยตรง ท่าทางต่อเนื่อง (pan, pinch, rotation, longPress) ผ่าน .possible → .began → .changed (ซ้ำ) → .ended ในเมธอดการดำเนินการ ให้ตรวจสอบ gestureRecognizer.state — ซึ่งช่วยแยกแยะจุดเริ่มต้น การเปลี่ยนแปลง และจุดสิ้นสุดของท่าทาง
UIKit มีคลาสย่อย UIGestureRecognizer ในตัว 7 คลาส ครอบคลุมสถานการณ์การโต้ตอบส่วนใหญ่ แต่ละคลาสย่อยมีการตั้งค่าเฉพาะ: numberOfTapsRequired สำหรับการแตะ direction สำหรับการปัด minimumPressDuration สำหรับการกดค้าง
สำหรับท่าทางแบบกำหนดเอง (เช่น การวาดซิกแซก) ให้สร้างคลาสย่อยของ UIGestureRecognizer โดยเขียนทับ touchesBegan, touchesMoved, touchesEnded และอัปเดตสถานะ Apple แนะนำให้ใช้คลาสในตัวเมื่อเป็นไปได้ — คลาสเหล่านี้ได้รับการปรับให้เหมาะสมและโต้ตอบกันอย่างถูกต้อง
เมื่อใช้ UIGestureRecognizer หลายตัวบน UIView เดียวกัน (เช่น การแตะและการแตะสองครั้ง) จะเกิดข้อขัดแย้งในการรับรู้: เมื่อแตะสองครั้ง การแตะเดี่ยวจะทำงานก่อน Apple มีเมธอด require(toFail:) เพื่อหน่วงเวลาการรับรู้ท่าทางหนึ่งจนกว่าอีกท่าทางจะล้มเหลว
กลไกทำงานดังนี้: โดยการเรียก tapRecognizer.require(toFail: doubleTapRecognizer) คุณระบุว่า tapRecognizer จะเปลี่ยนเป็นสถานะ .recognized ต่อเมื่อ doubleTapRecognizer สิ้นสุดใน .failed เท่านั้น สิ่งนี้เพิ่มความล่าช้า ~0.3 วินาทีก่อนดำเนินการแตะเดี่ยว — ผู้ใช้แตะสองครั้งและการแตะครั้งแรกจะถูกข้ามไป วิธีทางเลือกคือตัวแทน UIGestureRecognizerDelegate ที่มีเมธอด gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) ซึ่งอนุญาตให้รับรู้พร้อมกัน (เช่น pan + pinch สำหรับแผนที่)
ตาม WWDC 2020 ประมาณ 15% ของบัก ในแอปพลิเคชัน UIKit เกี่ยวข้องกับการกำหนดค่าขัดแย้งของท่าทางที่ไม่ถูกต้อง ที่ IT Sectr เรามาตรฐานวิธีการ: แต่ละหน้าจอมีแผนผังท่าทางที่มีลำดับความสำคัญ require(toFail:) — ซึ่งกำจัดบักการทำงานซ้ำของการแตะโดยสมบูรณ์
เพิ่มตัวจัดการการแตะเดี่ยวไปยัง UIImageView เมื่อแตะ ความโปร่งใสของรูปภาพจะเปลี่ยนไป — ตัวอย่างง่าย ๆ ที่แสดงการผูกท่าทางกับวิว
import UIKit
class ImageViewController: UIViewController {
@IBOutlet private var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
let tap = UITapGestureRecognizer(
target: self,
action: #selector(handleTap(_:))
)
tap.numberOfTapsRequired = 1
imageView.addGestureRecognizer(tap)
imageView.isUserInteractionEnabled = true
}
@objc private func handleTap(_: UITapGestureRecognizer) {
UIView.animate(withDuration: 0.2) {
self.imageView.alpha = self.imageView.alpha == 1.0 ? 0.5 : 1.0
}
}
}
จุดสำคัญ: isUserInteractionEnabled บน UIImageView มีค่าเป็น false โดยค่าเริ่มต้น — หากไม่มีแฟล็กนี้ Gesture Recognizer จะไม่ได้รับการสัมผัส สำหรับ UIView และ UIButton แฟล็กนี้เปิดใช้งานโดยค่าเริ่มต้น เมธอดการดำเนินการรับพารามิเตอร์ UITapGestureRecognizer ซึ่งสามารถใช้ location(in:) เพื่อกำหนดพิกัดของการแตะ
ใช้การปัดซ้ายเพื่อกลับไปยังหน้าจอก่อนหน้า สาธิตการกำหนดทิศทางและการผูกท่าทางกับวิวรากของคอนโทรลเลอร์
import UIKit
class DetailViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let swipeLeft = UISwipeGestureRecognizer(
target: self,
action: #selector(handleSwipe(_:))
)
swipeLeft.direction = .left
view.addGestureRecognizer(swipeLeft)
}
@objc private func handleSwipe(_: UISwipeGestureRecognizer) {
navigationController?.popViewController(animated: true)
}
}
UISwipeGestureRecognizer เป็นท่าทางไม่ต่อเนื่อง: มันเปลี่ยนเป็น .recognized ทันทีหลังจากการรับรู้ โดยไม่มีสถานะ .changed ระหว่างกลาง ดังนั้นจึงไม่จำเป็นต้องตรวจสอบสถานะในการดำเนินการ — ท่าทางจะรับรู้ (การดำเนินการทำงาน) หรือไม่ก็ตาม คุณสมบัติ direction รับหนึ่งในสี่ค่า: .left, .right, .up, .down เพื่อรองรับหลายทิศทาง ให้สร้างตัวรับรู้แยกกันสำหรับแต่ละทิศทาง
แสดงวิธีกำหนดค่าการแตะเดี่ยวและการแตะสองครั้งบนวิวเดียวกันโดยไม่ขัดแย้ง ตัวรับรู้การแตะสองครั้งมีลำดับความสำคัญ — การแตะเดี่ยวจะทำงานก็ต่อเมื่อไม่รับรู้การแตะสองครั้ง
import UIKit
class TapViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let singleTap = UITapGestureRecognizer(
target: self,
action: #selector(handleSingleTap)
)
singleTap.numberOfTapsRequired = 1
let doubleTap = UITapGestureRecognizer(
target: self,
action: #selector(handleDoubleTap)
)
doubleTap.numberOfTapsRequired = 2
singleTap.require(toFail: doubleTap)
view.addGestureRecognizer(singleTap)
view.addGestureRecognizer(doubleTap)
}
@objc private func handleSingleTap() {
print("Single tap — after 0.3s delay")
}
@objc private func handleDoubleTap() {
print("Double tap — instant")
}
}
หากไม่มี require(toFail:) เมื่อแตะสองครั้ง handleSingleTap จะทำงานก่อน แล้วตามด้วย handleDoubleTap — ซึ่งทำให้ UX เสียหาย เมื่อมี require(toFail:) การแตะเดี่ยวจะรอ ~0.3 วินาทีเพื่อให้แน่ใจว่าไม่มีการแตะครั้งที่สองตามมา ที่ IT Sectr รูปแบบนี้ใช้ในโปรแกรมแก้ไขรูปภาพและแกลเลอรี โดยการแตะสองครั้งจะซูมและการแตะเดี่ยวจะเลือกองค์ประกอบ
คำถามที่พบบ่อย
ได้ UIView รองรับอินสแตนซ์ UIGestureRecognizer หลายตัวพร้อมกัน เพื่อแก้ไขข้อขัดแย้ง ให้ใช้เมธอด require(toFail:) ซึ่งกำหนดลำดับการรับรู้ สำหรับการทำงานท่าทางแบบขนาน (เช่น pan + pinch บนแผนที่) ให้ใช้เมธอดตัวแทน gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) ที่คืนค่า true
UIGestureRecognizer เป็นสิ่งที่เป็นนามธรรมระดับสูงที่รับรู้รูปแบบการสัมผัสโดยอัตโนมัติและจัดการสถานะ touchesBegan เป็นเมธอดระดับต่ำของ UIResponder ที่ต้องการการติดตามพิกัด การจับเวลา และการยกเลิกการสัมผัสด้วยตนเอง Gesture Recognizer ง่ายกว่า เชื่อถือได้มากกว่า และเหมาะสำหรับท่าทางมาตรฐาน touchesBegan เหมาะสมสำหรับกราฟิกที่กำหนดเองเท่านั้น
SwiftUI ดั้งเดิมใช้ตัวปรับแต่ง: onTapGesture, onLongPressGesture, DragGesture, MagnificationGesture, RotationGesture สิ่งเหล่านี้เป็นอะนาล็อกเชิงประกาศของ UIGestureRecognizer ที่รวมอยู่ในลำดับชั้นของ SwiftUI หากจำเป็น สามารถห่อตัวรับรู้ UIKit ผ่าน UIViewRepresentable ได้ แต่ Apple แนะนำให้ใช้ท่าทาง SwiftUI ดั้งเดิม
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ