Demystifying iOS Event Handling: Deciphering How the Response Chain Works

iOSA core concept in development , the event response chain describes the system's process of delivering user interaction events to the object best suited to handle the event. Understanding the mechanics of the incident response chain is critical to developing high-quality applications. This article digs into how the event response chain works and provides code examples in Swift to help readers better understand the concept.

How the Incident Response Chain Works

iOSIn , the working principle of the event response chain can be briefly summarized as follows: starting from the topmost UIWindow, passing events down in sequence until finding the most suitable responder object for handling events. During this process, each responder object gets a chance to handle the event.

When the user performs an action, such as touching the screen or moving the device, the system creates a UIEvent object and sends it to the current first responder object. If the first responder object cannot handle the event, the system passes the event to the next object in the responder chain until an object that can handle the event is found. If eventually no object is able to handle the event, the event is discarded by the system.

The following is a schematic diagram of the incident response chain:

         UIWindow
             |
       UIViewController
             |
           UIView
             |
     subviews of UIView
复制代码

In this diagram, UIWindowis the starting point of the responder chain, which is the root view of all views. UIViewControllerand UIVieware both responder objects, both of which can handle events. UIViewControllerA can receive and handle events from its root view, while UIViewa can receive and handle events from itself, as well as events from its subviews.

Characteristics of Responder Objects

Responder objects are a special type of object that implement UIResponderthe class . A responder object can handle the event, can be the first responder object, and can pass the event to the next responder object. Here are some common methods in UIResponderthe class :

  • canBecomeFirstResponder: Returns a boolean indicating whether the object can become a first responder object.
  • becomeFirstResponder: Sets the object to be the first responder object.
  • resignFirstResponder: Relinquishes the status of the first responder object.
  • next: Returns the next responder object in the responder chain.

Responder objects can also implement a number of methods to handle events, such as:

  • touchesBegan(_:with:): Called when the user starts touching on the view.
  • touchesMoved(_:with:):当用户在视图上移动触摸时调用。
  • touchesEnded(_:with:):当用户在视图上结束触摸时调用。
  • touchesCancelled(_:with:):当系统取消触摸事件时调用。

自定义事件处理

Swift 中,可以通过重写 UIResponder 子类的方法来自定义事件处理。以下是一个示例代码,展示如何重写 UIView 子类的 touchesBegan 方法来处理触摸事件:

class CustomView: UIView {
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        
        // 处理触摸事件
        // ...
    }
}
复制代码

在这个示例中,当用户在 CustomView 上开始触摸时,系统会调用 CustomViewtouchesBegan 方法。在此方法中,开发者可以编写自己的触摸事件处理代码。

事件传递和事件响应

事件传递和事件响应是事件响应链的两个重要环节。在事件传递阶段,系统会将事件从上往下传递,直到找到最适合处理事件的对象。在事件响应阶段,系统会将事件从下往上响应,直到事件被处理或者传递到响应者链的顶部。

在事件传递阶段,UIViewUIViewController 都有一个 hitTest( *:with:) 方法,该方法返回一个 UIView 对象。当系统接收到事件时,它会调用 hitTest(* :with:) 方法来确定最适合处理该事件的视图对象。hitTest( *:with:) 方法首先检查自己是否能够处理该事件,如果不能,它会将事件传递给其子视图,并递归调用子视图的 hitTest(* :with:) 方法,直到找到能够处理该事件的视图对象。

在事件响应阶段,系统会将事件传递到第一响应者对象,并沿着响应者链向上传递,直到事件被处理或者传递到响应者链的顶部。在这个过程中,每个响应者对象都有机会处理事件。如果某个响应者对象能够处理事件,则它将调用相应的方法来处理事件,例如 touchesBegan(_:with:) 方法。如果该对象不能处理事件,则它将调用 next 方法,将事件传递给响应者链中的下一个对象。

事件拦截

hitTest(_:with:) 方法中,我们可以检查触摸点是否在指定区域内,如果在,则返回当前视图作为拦截目标,否则返回 nil,让系统将事件传递给下一个响应者。示例代码如下:

class CustomView: UIView {
    
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        if self.bounds.contains(point) {
            // 触摸点在视图内,拦截事件
            return self
        } else {
            // 触摸点不在视图内,将事件传递给下一个响应者
            return super.hitTest(point, with: event)
        }
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        print("CustomView touchesBegan")
    }
 
}
复制代码

在上述代码中,我们重写了 hitTest( *:with:) 方法,并在该方法中检查触摸点是否在视图内。如果在,则返回当前视图作为拦截目标,否则返回 nil,让系统将事件传递给下一个响应者。在 touchesBegan(* :with:) 方法中,我们打印了一条日志,以便在触摸事件发生时能够看到该方法是否被调用。

事件传递到父视图

要将事件传递到父视图,可以调用 next?.touchesBegan(touches, with: event) 方法,让父视图处理触摸事件。示例代码如下:

class CustomView: UIView {
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        
        // 处理触摸事件
        // 如果无法处理该事件,传递给父视图进行处理
        next?.touchesBegan(touches, with: event)
    }
}

class ParentView: UIView {
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        
        // 处理触摸事件
    }
}
复制代码

在上面的示例中,CustomViewParentView 都是 UIView 的子类。当用户在 CustomView 上触摸时,CustomViewtouchesBegan 方法会被调用。在这个方法中,如果 CustomView 无法处理该事件,它会将该事件传递给其父视图(ParentView)进行处理。

通过 next?.touchesBegan(touches, with: event) 方法将事件传递给父视图,如果父视图能够处理该事件,它的 touchesBegan 方法将被调用。在这个方法中,可以处理触摸事件。如果父视图仍然无法处理该事件,该事件将被传递给更高级别的响应对象进行处理。

需要注意的是,当事件被传递到下一个响应对象时,会调用该对象的 touchesBegan 方法。因此,在这个方法中,可以对事件进行处理,也可以将其传递给更高级别的响应对象进行处理。

自定义事件响应链

iOS 中,每个视图都是一个 UIResponder 对象。UIResponder 是一个抽象类,它定义了响应者对象可以处理的事件类型,包括触摸事件、加速计事件、远程控制事件等。每个 UIResponder 对象都有一个 next 响应者,即下一个响应者对象。当一个事件发生时,系统会将该事件从前往后依次传递给响应者链中的对象,直到某个对象处理了该事件为止。如果没有任何对象处理该事件,则该事件将被丢弃。

我们可以通过自定义 UIResponder 子类来实现更灵活的事件处理逻辑。下面是一个简单的自定义响应者链的示例代码:

class CustomResponder: UIResponder {
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        print("CustomResponder touchesBegan")
        next?.touchesBegan(touches, with: event)
    }
    
}

class ViewController: UIViewController {
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        print("ViewController touchesBegan")
    }
    
}
复制代码

在上面的代码中,我们定义了一个名为 CustomResponder 的自定义响应者子类。在该类中,我们重写了 touchesBegan(_:with:) 方法,并在该方法中打印了一条日志。在该方法中,我们还调用了 next?.touchesBegan(touches, with: event) 方法,将触摸事件传递给下一个响应者对象。

在 ViewController 中,我们也重写了 touchesBegan( *:with:) 方法,并在该方法中打印了一条日志。当触摸事件发生时,首先会调用 CustomRespondertouchesBegan(* :with:) 方法,并打印出 "CustomResponder touchesBegan"。然后,由于我们调用了 next?.touchesBegan(touches, with: event) 方法,系统会将触摸事件传递给 CustomResponder 的下一个响应者对象,即 ViewController。此时,系统会调用 ViewControllertouchesBegan(_:with:) 方法,并打印出 "ViewController touchesBegan"。

通过自定义响应者子类,我们可以更加灵活地处理事件,实现更复杂的事件处理逻辑。例如,我们可以在响应者链中添加多个响应者对象,根据事件类型、触摸点位置等条件来决定哪个响应者对象处理该事件。

总结

事件响应链是 iOS 开发中的一个核心概念。了解事件响应链的工作原理和实现方式,可以帮助开发者更好地理解 iOS 应用的交互模型,编写更高效、可靠的交互代码。

以下是一些事件响应链的实践建议:

  • 在处理触摸事件时,始终调用父类的 touchesBegan( *:with:)touchesMoved(* :with:)touchesEnded( *:with:)touchesCancelled(* :with:) 方法。这样可以确保事件会正确地传递到响应者链的下一个对象。
  • 如果希望在事件传递过程中拦截事件,可以重写 hitTest(_:with:) 方法,并在该方法中检查事件是否应该被拦截。
  • 如果希望将事件传递到父视图,可以调用 next?.touchesBegan(touches, with: event) 方法。
  • 尽可能避免使用 touches 属性,因为该属性在多点触控环境下会出现问题。推荐使用 allTouches 属性来获取所有触摸点。
  • 尽可能避免使用 gesture recognizer 来处理触摸事件,因为这会增加系统的负担,并可能导致意外的行为。推荐使用触摸事件处理方法来处理触摸事件。

希望本文可以帮助开发者更好地理解 iOS 应用的交互模型,并编写更高效、可靠的交互代码。

Guess you like

Origin juejin.im/post/7203546909326721085