事件传递机制

IOS面试题(UIView) ----- 事件传递机制 - 简书

面试题:

在以下场景中,父视图 ParentView 上有三个子视图 ViewAViewBViewCViewA 完全位于 ParentView 的范围内,ViewB 有一半在 ParentView 的范围内,而 ViewC 完全位于 ParentView 的范围之外。假设用户在 ViewAViewBViewC 的区域上触摸屏幕,请描述事件处理的顺序和机制,并解释哪些视图将有机会响应触摸事件。

追问1:

如果 ViewAViewB 能够响应触摸事件,但 ParentViewclipsToBounds 属性设置为 YES,那么当用户触摸 ViewB 位于 ParentView 范围之外的部分时,事件的处理情况会如何变化?

追问2:

在同样的设置下,如果 ViewAViewBViewC 都不处理触摸事件(即它们没有重写 touchesBegan:withEvent: 方法或者在重写的方法中调用了 super),请说明事件处理将如何沿着响应链传递。

追问3:

如果希望当用户触摸 ViewC 时,即使它位于 ParentView 范围之外,ViewC 也能响应事件,你将如何修改 ParentView 的代码或属性以实现这一行为?

提问4: 如果 ViewDuserInteractionEnabled 属性被设置为 NO,点击 ViewD 时会发生什么?

提问5: 如果 ViewD 被部分遮挡,例如被另一个视图 ViewE 遮挡,点击 ViewDViewE 遮挡的部分会发生什么?

提问6: 假设用户在 ViewA、ViewB 和 ViewC 的区域上触摸屏幕,请描述事件处理的顺序和机制,并解释哪些视图将有机会响应触摸事件。请回答每个view的传递和响应的顺序?

提问7:事件的传递和响应分别是深度优先遍历还是广度优先遍历?怎么证明?

viewithtag呢?

追问8:view上同时有事件和手势,点击view之后会怎么处理?

追问9:

父视图有一个单击手势,它上面有一个子视图(subview)是UIControl,绑定action事件。这个时候,点击这个subview会发生什么?如果是UIButton呢?

//10-12尚需验证

追问10:

在一个复杂的视图层级结构中,ViewF覆盖在ViewG上,而且ViewF的userInteractionEnabled属性被设置为NO,那么当用户点击ViewF时,会发生什么?

追问11:

如果一个视图(比如ViewH)重写了touchesBegan:withEvent:方法,并在方法中没有调用super,那么这个视图的父视图会收到这个触摸开始事件吗?

追问12:

如果一个视图(比如ViewI)的alpha属性被设置为0(即完全透明),那么用户点击这个视图的区域时,触摸事件会被这个视图接收吗?

当用户在 ViewAViewBViewC 的区域上触摸屏幕时,iOS 系统会首先将触摸事件传递给主窗口,然后由主窗口沿着视图层级结构向下传递给最顶层的视图,即 ParentView。接下来:

  • 如果用户触摸在 ViewA 上,系统会将事件传递给 ViewA,因为 ViewA 完全位于 ParentView 内,并且是位于触摸点最上层的视图。如果 ViewA 能够处理该事件,它将成为第一响应者并对事件进行处理。
  • 如果用户触摸在 ViewBParentView 范围内的部分,系统同样会将事件传递给 ViewBViewB 将有机会处理该事件。
  • 如果用户触摸在 ViewC 上,因为 ViewC 完全位于 ParentView 范围之外,通常情况下 ViewC 不会接收到事件,除非特殊处理。事件不会传递给 ViewC,因为它不在 ParentView 的边界内。

追问1回答:

如果 ParentViewclipsToBounds 属性设置为 YES,这意味着 ParentView 会裁剪掉超出其边界的子视图部分。因此,即使 ViewB 能够响应触摸事件,当用户触摸 ViewBParentView 范围之外的部分时,触摸事件不会传递给 ViewB,因为该部分被裁剪掉了,对用户来说是不可见的。

追问2回答:

如果 ViewAViewBViewC 都不处理触摸事件,事件将会沿着响应链向上传递。以下是事件传递的顺序:

  • 对于 ViewA,事件首先传递到 ViewA,由于 ViewA 不处理该事件,事件将传递给 ParentView
  • 对于 ViewBParentView 范围内的部分,情况与 ViewA 相同,事件最终也会传递给 ParentView
  • ViewC 不会接收事件,因为它在 ParentView 范围之外。
  • 如果 ParentView 也不处理触摸事件,事件会继续沿响应链向上传递给包含 ParentView 的视图控制器,最终可能到达应用程序对象。

追问3回答:

要使位于 ParentView 范围之外的 ViewC 能够响应触摸事件,可以重写 ParentViewhitTest:withEvent: 方法。这个方法决定了哪个子视图应该接收触摸事件。通过在 hitTest:withEvent: 中添加检查,即使 ViewC 位于 ParentView 范围之外,我们也可以将事件手动转发给它:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView *view = [super hitTest:point withEvent:event];
    if (view == nil) {
        // 将触摸点转换到 ViewC 的坐标系
        CGPoint pointInC = [self convertPoint:point toView:ViewC];
        // 检查触摸点是否在 ViewC 内
        if ([ViewC pointInside:pointInC withEvent:event]) {
            view = ViewC;
        }
    }
    return view;
}

这段代码首先调用 superhitTest:withEvent: 方法来尝试按常规规则找到一个响应者。如果没有找到,并且触摸点正好在 ViewC 的范围内,则直接将 ViewC 作为响应者返回。这样即使 ViewC 不在 ParentViewclipsToBounds 边界内,它也能接收和处理触摸事件。

提问4: 如果 ViewDuserInteractionEnabled 属性被设置为 NO,点击 ViewD 时会发生什么?

ViewDuserInteractionEnabled 属性被设置为 NO 时,ViewD 将不会接收任何触摸事件,也就是说,它会忽略所有的触摸。因此,点击 ViewD 时,触摸事件会穿透 ViewD 传递给它下面的视图。如果点击的是 ViewDViewA 上的半边,那么触摸事件会传递给 ViewA。如果点击的是 ViewDParentView 上的半边,那么触摸事件会传递给 ParentView

提问5: 如果 ViewD 被部分遮挡,例如被另一个视图 ViewE 遮挡,点击 ViewDViewE 遮挡的部分会发生什么?

假设 ViewE 是另一个子视图,它被添加到 ParentView 上,并且位于 ViewD 的上方,那么点击 ViewDViewE 遮挡的部分时,触摸事件首先会被 ViewE 接收,因为它位于最上层。如果 ViewE 能够接收并处理该触摸事件,那么事件处理到此结束。如果 ViewE 不处理事件(可能是因为 userInteractionEnabled 被设置为 NO,或者它选择不处理该事件),那么事件将会穿透 ViewE 并传递给它下面的视图,也就是 ViewD。如果 ViewD 同样选择不处理或者无法处理该触摸事件,那么事件最终会传递给 ViewD 的父视图 ParentView。如果 ViewDViewE 遮挡的部分正好是在 ViewA 之上,那么 ViewA 不会接收到该触摸事件,因为 ViewEViewD 都位于它的上方。

  • 上述问题中ViewD和ViewE是什么关系?兄弟关系,还是父子关系?

在上述问题中,ViewDViewE 被描述为都是 ParentView 的子视图,因此它们之间是兄弟关系(sibling relationship)。兄弟视图是相对于共同的父视图来说的------在这个例子中,ParentView 是它们共同的父视图。

视图层级结构中,兄弟视图的相对层次(谁在上,谁在下)取决于它们被添加到父视图的顺序。后添加的视图会覆盖在先添加的视图上面。如果 ViewE 被添加到 ParentView 上且在 ViewD 之后添加,那么 ViewE 将会覆盖 ViewD,处于更上层的位置。

当处理触摸事件时,iOS 首先会将事件传递给最上层的视图,即在视图层级中最接近用户的那个视图。如果 ViewE 覆盖了 ViewD,点击重叠区域时,系统会先把事件传递给 ViewE。只有当 ViewE 不能处理该事件时,事件才会向下穿透到下一层的 ViewD,或者如果 ViewD 也不处理,最终传递到 ParentView

  • ViewD不是ViewE的父视图,为什么ViewE 不处理事件时,事件将沿响应链传递给 ViewD?

在iOS的事件处理系统中,当一个视图(比如 ViewE)不处理一个事件时,这个事件通常会沿着响应链向上传递。响应链是由当前响应者(比如控件或视图)及其上级响应者(如父视图或视图控制器)组成的链式结构。

如果 ViewEViewD 是兄弟视图(即它们都是同一个父视图 ParentView 的子视图),事件的传递会有点不同。当用户在 ViewE 上触摸屏幕,而 ViewE 不处理该事件时,iOS会将事件传递给 ViewE 的父视图,也就是 ParentView,而不是直接传递给 ViewD

事件传递给 ParentView 后,ParentView 有机会接收并处理事件。如果 ParentView 也不处理该事件,那么 ParentView 的父视图或者它的视图控制器将有机会处理事件,如此类推,直到找到能够处理该事件的响应者。

然而,如果 ViewE 被设计成不应该拦截某些事件,或者在某些情况下应该把事件传递给特定的兄弟视图(比如 ViewD),开发者可以通过覆盖 hitTest:withEvent:pointInside:withEvent: 方法来自定义事件的传递逻辑。通过这种方式,开发者可以改变默认的事件传递行为,使得事件在 ViewE 不处理时直接传递给 ViewD,而不是传递给它们共同的父视图 ParentView。然而,这种做法相对少见,因为它可能会违反视图层次结构的自然流程,因此需要谨慎使用。

提问6:

在iOS开发中,事件的传递和响应是遵循的原则是:从最底层的子视图开始,然后是其父视图,再到上一层的父视图,依次往上。同时,只有在父视图范围内的子视图才能接收到事件。具体到这个场景中:

  1. 如果用户在ViewA区域触摸屏幕,事件将首先传递给ViewA。ViewA作为第一响应者,有机会首先处理这个触摸事件。如果ViewA不处理这个事件,那么事件会向上传递,由ParentView进行处理。

  2. 如果用户在ViewB区域触摸屏幕,事件将首先传递给ViewB。同样,ViewB作为第一响应者,有机会首先处理这个触摸事件。如果ViewB不处理这个事件,那么事件会向上传递,由ParentView进行处理。

  3. 如果用户在ViewC区域触摸屏幕,因为ViewC完全位于ParentView的范围之外,所以它无法接收到触摸事件,也就没有机会处理这个事件。这个事件会直接由ParentView进行处理。

所以,事件的传递和响应顺序为:ViewA/ViewB -> ParentView。而ViewC因为完全位于ParentView的范围之外,因此无法接收和处理触摸事件。

以上是基于默认情况的分析,具体的事件处理还会受到具体代码的影响,例如是否重写了hitTest:withEvent:或者pointInside:withEvent:方法,或者是否设置了userInteractionEnabled、isHidden或者alpha属性等。

提问7:

在iOS开发中,UIView的viewWithTag:方法使用的是深度优先遍历(Depth-First Search)。

这个方法会先检查自身的tag,如果匹配,就直接返回自身。如果不匹配,它会遍历其所有子视图,对每个子视图,都调用这个视图的viewWithTag:方法。这样就形成了一个递归的过程,最终形成的遍历路径是深度优先的。这个过程会一直到达视图树的最深层,如果还没有找到,就返回nil。

事件的传递和响应在iOS中确实是遵循深度优先遍历的原则。我们可以通过以下方式来验证这一点:

首先,我们可以创建一个视图层级结构,比如ParentView中包含ChildView1和ChildView2,ChildView1中又包含GrandChildView。然后我们在每一个视图的touchesBegan:withEvent:方法中打印一些信息。

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"%@", self);
    [super touchesBegan:touches withEvent:event];
}

当我们在GrandChildView上点击时,我们会看到控制台上首先打印出GrandChildView的信息,然后是ChildView1的信息,最后是ParentView的信息。这就是深度优先遍历的表现。

其次,深度优先遍历的原则也体现在UIView的hitTest:withEvent:方法中。在这个方法中,UIView会首先检查它的所有子视图,然后是它自己,最后是它的父视图。这个过程就是深度优先遍历。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
    if ([self pointInside:point withEvent:event]) {
        for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
            CGPoint convertedPoint = [subview convertPoint:point fromView:self];
            UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                return hitTestView;
            }
        }
        return self;
    }
    return nil;
}

这段代码首先检查自己的所有子视图(由于子视图数组是按照添加顺序排列的,所以这里使用reverseObjectEnumerator进行了反序遍历,确保后添加的子视图能够优先接收事件),然后是自己,最后返回nil表示事件需要继续向上传递给父视图。这个过程也是深度优先遍历。

追问8:

在iOS中,手势识别器和触摸事件可以并存。当一个视图同时添加了触摸事件和手势识别器时,当触摸开始,手势识别器和视图都可以接收到触摸事件。但是,手势识别器会首先接收并处理触摸事件,如果手势识别器识别出了手势,那么它就会"吞掉"这次触摸事件,视图的触摸事件处理方法就不会被调用;如果手势识别器没有识别出手势,那么这次触摸事件就会被视图的触摸事件处理方法接收并处理。

要注意的是,这种行为可以通过手势识别器的cancelsTouchesInView属性来改变。如果将该属性设置为NO,那么即使手势识别器识别出了手势,视图的触摸事件处理方法仍然会被调用。另外,还可以通过delegate来更精细的控制手势识别器和触摸事件的交互行为。

追问9:

实际上uicontrol不会响应这个事件,会给父视图响应;但是上uibutton会响应这个事件,不会给父视图响应。

在iOS中,触摸事件的传递是有规律的。默认情况下,触摸事件会从最底层的视图开始,如果这个视图不处理,事件就会传递给上一层的视图,直到找到可以处理这个事件的视图为止。

UIButton是UIControl的子类,它可以接收并处理触摸事件。当你在UIButton上触摸时,UIButton会响应这个事件,并且阻止事件向更上层的视图传递。

而UIControl本身并不会处理触摸事件,它只是一个可以接收触摸事件的容器。当你在UIControl上触摸时,如果它没有子视图可以处理这个事件,那么这个事件就会继续向上传递,由它的父视图来处理。

如果你希望UIControl能够像UIButton一样处理触摸事件,那么你需要自己在UIControl中添加对触摸事件的处理。你可以通过重写UIControl的触摸事件处理方法(比如touchesBegan:withEvent:)来实现这个目标。

在这种情况下,会先执行手势识别器的行为,然后再执行UIButton的点击事件。这是因为UIButton内部有一个私有的手势识别器用于处理点击事件,并且这个手势识别器的优先级要高于其他手势识别器。

当你点击UIButton时,首先是UIButton的私有手势识别器捕获到这个点击事件,并触发UIButton的点击行为。然后,如果UIButton的点击行为没有消耗这个点击事件(也就是说,没有阻止这个点击事件的进一步传递),这个点击事件就会继续传递给UIButton的父视图的手势识别器。

对于UIControl的情况,由于UIControl本身并没有手势识别器,所以当你点击UIControl时,这个点击事件会直接传递给UIControl的父视图的手势识别器。

总的来说,UIButton和UIControl的主要区别在于,UIButton有一个私有的手势识别器用于处理点击事件,而UIControl没有。因此,当UIButton和UIControl的父视图都有手势识别器的时候,UIButton的点击行为会优先于父视图的手势识别器,而UIControl的点击事件则会直接传递给父视图的手势识别器。

//10-12尚需验证

追问10:

当用户点击ViewF时,由于ViewF的userInteractionEnabled属性被设置为NO,所以ViewF无法接收和处理触摸事件,触摸事件会直接传递给它的下一个响应者,也就是ViewG。

追问11:

如果ViewH重写了touchesBegan:withEvent:方法,并在方法中没有调用super,那么ViewH的父视图将不会收到这个触摸开始事件。因为在视图的事件处理方法中不调用super,会阻止事件的继续传递。

追问12:

即使视图ViewI的alpha属性被设置为0(完全透明),只要它的userInteractionEnabled属性为YES,用户点击这个视图的区域时,触摸事件仍然会被这个视图接收。在iOS中,视图的透明度不影响其接收触摸事件的能力。

相关推荐
Magnetic_h21 小时前
【iOS】单例模式
笔记·学习·ui·ios·单例模式·objective-c
名字不要太长 像我这样就好2 天前
【iOS】push和pop、present和dismiss
学习·macos·ios·objective-c·cocoa
Magnetic_h4 天前
【iOS】ViewController的生命周期
笔记·学习·ui·ios·objective-c
Magnetic_h4 天前
【iOS】present和push
笔记·学习·ui·ios·objective-c
名字不要太长 像我这样就好5 天前
【iOS】单例模式
开发语言·ios·单例模式·objective-c
键盘敲没电5 天前
【iOS】UIViewController的生命周期
学习·ios·objective-c·xcode
归辞...7 天前
暑假第四周——天气预报仿写
macos·objective-c·cocoa
键盘敲没电8 天前
【iOS】MVC模式
ios·mvc·objective-c·xcode
lzhdim8 天前
苹果宣布iOS 18正式版9月17日推送:支持27款iPhone升级
macos·ios·objective-c·cocoa·iphone
不会敲代码的VanGogh9 天前
【iOS】——渲染原理与离屏渲染
学习·ios·objective-c