IOS事件的传递和响应

事件描述

事件:用户和应用程序之间的交互
IOS应用程序可以接收许多不同类型的事件吗,目前IOS事件分为4类:

public enum UIEventType : Int {
    case touches  // 触摸事件
    case motion   // 加速器事件
    case remoteControl // 远程控制
    @available(iOS 9.0, *) 
    case presses
}

UIEvent
每一个事件都对应着一个相应的描述对象UIEvent,它包含了事件的typetimestampallTouches和一系列返回UITouch序列的API

UITouch
表示屏幕上发生的触摸的位置大小移动力量的对象。
当用户用一根手指触摸屏幕时,会创建一个与手指相关的UITouch对象,一根手指对应一个UITouch。当触摸位置或其他参数的更改,UIKit会使用新信息更新相同的UITouch对象。

 open var timestamp: TimeInterval { get } // 事件产生的时间戳
 open var phase: UITouchPhase { get }  // 触摸状态
 @available(iOS 9.0, *)
 open var type: UITouchType { get } // 触摸类型
 open var tapCount: Int { get } // touch down within a certain point within a certain amount of time

 open func location(in view: UIView?) -> CGPoint  // 触摸在view上的位置
 open func previousLocation(in view: UIView?) -> CGPoint // 记录前一个触摸点的位置

UIResponder
在iOS中不是任何对象都能接收并处理事件,只有继承了UIResponder响应者对象才能接受并处理事件。通用的UIResponder子类包括UIViewUIViewControllerUIApplication。它提供了一系列常用的处理事件的Api:

    
    open var canBecomeFirstResponder: Bool { get } // default is NO
    open func becomeFirstResponder() -> Bool  成为第一响应者

    open var canResignFirstResponder: Bool { get } // default is YES
    open func resignFirstResponder() -> Bool  辞去第一响应者
    open var isFirstResponder: Bool { get }

    触摸事件
    open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)     触摸开始
    open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)     触摸点移动
    open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)     触摸接触
    open func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) 触摸取消

     按压事件
    @available(iOS 9.0, *)
    open func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?)
    @available(iOS 9.0, *)
    open func pressesChanged(_ presses: Set<UIPress>, with event: UIPressesEvent?)
    @available(iOS 9.0, *)
    open func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?)
    @available(iOS 9.0, *)
    open func pressesCancelled(_ presses: Set<UIPress>, with event: UIPressesEvent?)
  
    加速计事件
    open func motionBegan(_ motion: UIEventSubtype, with event: UIEvent?)
    open func motionEnded(_ motion: UIEventSubtype, with event: UIEvent?)
    open func motionCancelled(_ motion: UIEventSubtype, with event: UIEvent?)

    远程控制事件
    open func remoteControlReceived(with event: UIEvent?)

事件的传递

事件的传递:从父控件向子控件传递,即自上而下传递。

  • 事件产生时,系统会将该事件加入到一个由UIApplication管理的事件队列中。队列是FIFO的,因此先加入的事件优先处理。
  • UIApplication从队列中取出最前面的事件,并将事件分发给应用程序的主窗口keyWindow
  • keyWindow会在视图结构中找到一个最适合处理事件的视图,并将事件传递给该视图
应用程序是如何传递事件,并且找出最适合处理事件的视图?

事件传递的本质依然还是基于消息机制发送消息。继承自UIView的类拥有以下方法:

// 返回处理事件的视图,内部通过调用 -pointInside:withEvent: 判断
open func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
// 判断触摸点是否在视图上
open func point(inside point: CGPoint, with event: UIEvent?) -> Bool 

当控件调用 hitTest(_ , with:) -> UIView函数时,它会调用point(inside:, with:) -> Bool判断事件的触摸点是否在视图上。返回true说明触摸点在视图上,向视图分支传递触摸事件;false触摸点不在视图上,忽略视图及其内部结构分支。
hitTest(_ , with:) -> UIView内部实现大致如下:

   func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        // 1.判断下自己能否接收事件
        guard isUserInteractionEnabled && !isHidden && alpha > 0.01 else {
            return nil
        }
        // 2.判断下点在不在当前控件上
        guard self.point(inside: point, with: event) else {
            return nil
        }
         // 3.从后往前遍历自己的子控件,查找到更合适的view
        for subview in subviews.reversed() {
            let subPoint = convert(point, to: subview)
            let festView = subview.hitTest(subPoint, with: event)
            if let festView = festView {
                return festView
            }
        }
        // 4.如果没有比自己合适的子控件,最合适的view就是自己
        return self
    }

1、UIApplicationkeyWindow发送 hitTest(_ , with:)的消息
2、keyWindow(或控件)接受消息后将消息转发给自己的子控件,转发的顺序为子控件数组逆顺序(即从数组的最后一个子控件往前遍历),当某个子控件的hitTest(_ , with:)返回不为nil时,则说明该子控件最适合处理这个事件
3、将消息转发给找到的这个控件,返回2步骤
4、如果所有子控件hitTest(_ , with:)都返回nil,则说明子控件中没有更适合处理事件的对象,最适合的处理事件的对象是自己,这时控件的hitTest(_ , with:) -> UIView返回为自身

------------------------------让我们来做个实验:---------------------------------

自定义各个控件,并重写控件的hitTest(_ , with:)

var window: CustomWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        
        window = CustomWindow(frame: UIScreen.main.bounds)
        window?.backgroundColor = UIColor.white
        
        let mainSB = UIStoryboard(name: "Main", bundle: nil)
        window?.rootViewController = mainSB.instantiateInitialViewController()
        window?.makeKeyAndVisible()
        
        return true
    }
CustomWIndow:
class CustomWindow: UIWindow {
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        print("\(type(of: self))")
        return super.hitTest(point, with: event)
    }
@end

OrangeView:
class OrangeView: UIView {
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        print("\(type(of: self))")
        return super.hitTest(point, with: event)
    }
@end
...
其他颜色视图与 OrangeView 一样的定义,省略不写
视图结构

注意: 以上视图结构图,橙色先于绿色加载在白色上,红色先于蓝色加载在橙色上

猜测:

  1. 点击 白色,事件传递顺序:
    UIApplication -> UIWindow -> 白色 -> 绿色 -> 橙色
  2. 点击 绿色,事件传递顺序:
    UIApplication -> UIWindow -> 白色 -> 绿色
  3. 点击 橙色
    UIApplication -> UIWindow -> 白色 -> 绿色 -> 橙色 -> 蓝色 -> 红色
  4. 点击 蓝色
    UIApplication -> UIWindow -> 白色 -> 绿色 -> 橙色 -> 蓝色 -> 黄色

逐一进行操作,并查看输出:


点击 白色
点击 绿色
点击 橙色
点击 蓝色

可见我们之间的结论是正确的:事件的传递:从父控件向子控件传递,即自上而下传递。当某个控件可接收触摸事件时,则遍历它的子视图查看是否有更好的接收事件的视图。当某个控件不可接收事件时,则忽略它的内部视图分支。

事件拦截

事件的传递中已经讲述了事件传递的实现和过程。基于此,可以实现事件的拦截实现:

  • 重写func hitTest(_ , with:) -> UIView,可以返回希望的最适合处理事件的控件,return nil 则表示当前控件和它的子控件都不处理事件。
  • 重写func point(inside:, with:) -> Bool,只要返回 false,不论触摸点是否在视图上,都不接收事件。
  • userInteractionEnable = falsehidden = truealpha < 0.01 的控件会被忽略,不接收事件。

事件响应

事件的响应:从子控件向父控件传递,即自下而上传递。

响应者链

讲到事件的响应,不得不提响应者链。响应者链是由多个响应者对象链接起来的层次结构。每个应用都存在着响应者链,它是由最表层的子控件途径各个控件直至UIApplication的一个响应者路径。根据第一响应者的不同,路径上的控件会跟着变化。

如何判断控件的下一响应者?(或如何辨析响应者链的组成?)
  • UIView对象。 如果视图是视图控制器的根视图,则下一个响应者是视图控制器; 否则,下一个响应者是视图的父视图。

  • UIViewController对象。如果视图控制器的视图是窗口的根视图,则下一个响应者是窗口对象。如果视图控制器由另一个视图控制器呈现,则下一个响应者是呈现视图控制器。

  • UIWindow对象。 窗口的下一个响应者是UIApplication对象。

  • UIApplication对象。 下一个响应者是app Delegate,但只有当app Delegate是UIResponder的实例并且不是view、viewController或者application自身时才成立

响应过程

事件产生并找到最适合处理事件的控件(即第一响应者)后,就会沿着响应者链向上传递事件直到某一响应者调用相应的响应处理方法处理事件。见下图:

每类事件都有相应的响应处理方法,例如触摸事件为:

    open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)     触摸开始
    open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)     触摸点移动
    open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)     触摸接触
    open func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) 触摸取消

以上四个函数默认做法:将事件顺着响应者链向上传递,即touch方法默认不响应并处理事件,仅仅传递事件给下一响应者

对上面的例子进行改造来验证:

class ViewController: UIViewController {
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        print("\(type(of: self)) - touch")
    }
}

重写白色视图的父控件viewController的touchesBegan(_: , with),接着点击黄色视图:


打印结果显示这时的响应对象为viewController。
可见YellowView并没有处理触摸事件,只是将触摸事件沿着响应者链向上传递,而其父视图BlueView同样没有处理事件继续传递,接着是依然没有处理事件的OrangeView和WhiteView,当事件传递给ViewController时,ViewController响应了事件,并实现了func touchBegon(_: with:),至此事件传递结束。

继续修改黄色控件,实现func touchBegon(_: with:)

class YellowView: UIView {
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        print("\(type(of: self))")
        return super.hitTest(point, with: event)
    }
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        print("\(type(of: self)) - touch")
    }
}
@end

再次点击黄色视图:


事件响应者为YellowView。
结论:响应处理函数默认实现仅仅只是传递事件给下一响应者
注意:加速计事件不遵循响应链,详情请参考苹果官方文档

同一事件多个响应对象处理实现:
上面说过找到第一响应者之后,系统默认将事件沿着响应者链传递,既然如果,可以在某一响应者处理事件后,继续执行系统默认处理就能实现多个响应者处理事件:

class YellowView: UIView {
    ...
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        print("\(type(of: self)) - touch")
        super.touchesBegan(touches, with: event)
    }
}

扩大响应区域

扩大响应区域亦是对事件的拦截,在不改变控件尺寸的情况下实现如下:

  • 重写func hitTest(_ , with:) -> UIView,直接返回控件自身
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        return self
    }
  • 重写func point(inside:, with:) -> Bool,比较扩展的后的尺寸和触摸较。
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        let expand: CGFloat = 50
        let newX = self.frame.midX + expand
        let newY = self.frame.midY + expand
        let newW = self.frame.width + 2 * expand
        let newH = self.frame.height + 2 * expand
        let expandRect = CGRect(x: newX, y: newY, width: newW, height: newH)
        let newPoint = self.convert(point, to: self.superview)
        return expandRect.contains(newPoint)
    }

参考:Apple Document

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容