As an almost obligatory iOS interview question, the response chain question is one that most people can’t escape. Today I saw a new question that rekindled my interest in this question:

  • Response chain: What happens to the child View if Swizzle uses the touchesBegan method of the parent View?

There are two aspects to this question:

  • Response chain and related touches methods
  • Method swizzle under Runtime

In that case, let’s code up!

Step 1 put the demo together

There are mainly three classes: page: ViewController, ancestor View: GView, parent View: PView, child View: SView. Details are as follows:

#import "ViewController.h"
#import "GView.h"
#import "PView.h"
#import "SView.h"

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    GView *g = [[GView alloc] initWithFrame:CGRectMake(100.150.300.300)];
    g.backgroundColor = [UIColor grayColor];
    [self.view addSubview:g];
    
    PView *p = [[PView alloc] initWithFrame:CGRectMake(50.50.150.150)];
    p.backgroundColor = [UIColor redColor];
    [g addSubview:p];
    
    SView *s = [[SView alloc] initWithFrame:CGRectMake(30.30.100.100)];
    s.backgroundColor = [UIColor greenColor];
    [p addSubview:s];
}

@end

#import "GView.h"

@implementation GView

@end

#import "PView.h"

@implementation PView

@end

#import "SView.h"

@implementation SView

@end
Copy the code

The following information is displayed:

Step 2 Add the ‘touches’ method

Create viewControllerViews (), PView (), SView ()


@implementation ViewController

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"VC touchBegan");
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"VC touchesEnded");
}

@end

#import "GView.h"

@implementation GView

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"GView touchBegan");
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"GView touchesEnded");
}

@end

#import "PView.h"

@implementation PView

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"PView touchBegan");
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"PView touchesEnded");
}

@end

#import "SView.h"

@implementation SView

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"SView touchBegan");
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"SView touchesEnded");
}

@end
Copy the code

Run demo, click the green area of SView, get log as follows:

According to official documentation, when an event occurs, it is first reported level by level to determine the App that responded (if not, the event is discarded), and then distributed level by level to determine the object that responded to the event using hitTest:withEvent: and pointInside:withEvent: methods. If the first responder is not identified, the event will be thrown up level by level, and if the first responder is not identified to the AppDelegate, the event will be discarded. This is the event response chain rule.

According to the rules of event response chain, when the green SView area is clicked, touchesBegan events and touchesEnded events will be reported by SView according to the click action. After the complete response chain, SView will respond and output the corresponding log information.

Because the red parent View: PView is not identified as the object responding to the event, but merely as the object passing the event, the touches method on PView does not respond.

This is something that most people will understand.

Step 3 Swap the parent View’s touchesBegan method

Exchange methods, which utilize the Objective-C message mechanism and Runtime mechanism, are implemented through exchange methods to achieve corresponding purposes. Let’s implement this part first:

#import "PView.h"
#import <objc/runtime.h>

@implementation PView

+ (void)load {
    / / the current class
    Class class = [self class];
    
    // The old method name and the replacement method name
    SEL originalSelector = @selector(touchesBegan:withEvent:);
    SEL swizzledSelector = @selector(newTouchesBegan:withEvent:);
    
    // Old method structure and replacement method structure
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
    // The call swaps the implementation of the two methods
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

- (void)newTouchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"PView newTouchesBegan");
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"PView touchBegan");
}

// Other methods are omitted

@end
Copy the code

To see the exchange effect, first comment the relevant methods in SView, then run demo, click on the red PView area, get log as follows:

Make sure the method has been swapped successfully.

At this time, release the relevant method annotation in SView, rerun the project, and click the red PView and green SView area respectively to get the result:

Click on the red PView area

Click on the green SView area

There seems to be no problem. Everything is as it should be.

But is it true?

Step 4: Debug

What happens if YOU comment out the Green SView’s touchesBegan method?

According to the rules of the response chain, after the event was distributed, the touchesBegan method of the green SView was annotated and not implemented, so the touchesBegan method of the red PView was handed over to respond. And the red PView’s ‘touchesBegan’ method has been swapped with the ‘newTouchesBegan’ method, so it’s going to respond to the red PView’s ‘newTouchesBegan’ method first.

The green SView’s touchesEnded method is implemented and therefore responds.

To sum up, it should be the newTouchesBegan method in response to the red PView and the touchesEnded method in response to the green SView.

The actual result is:

After the touchesEnded method of the green SView finishes executing, the touchesEnded method of the red PView will also be executed.

Breakpoint:

The green SView’s touchesEnded method’s call stack:

The red PView’s touchesEnded method call stack:

Compared the two, in addition to red PView touchesEnded methods among the call stack one more – [UIResponder _completeForwardingTouches: phase: event: index:] calls, both are basically identical.

For extra – [UIResponder _completeForwardingTouches: phase: event: index:] method calls, also temporarily not search on the Internet to have a clear answer.

In this case, debug several more rounds:

Comment out the PView and SView ‘touchesBegan’ methods, leave all other methods open, and click SView to get this:

Comment out the GView, PView, and SView ‘touchesBegan’ methods, leave all other methods open, and click SView to get this:

Comment out the GView and PView ‘touchesBegan’ methods, leave all other methods open, and click SView to get this:

Comment out all the ‘touchesBegan’ methods, leave all the other methods open, and click SView to get this:

Comment out the GView and SView ‘touchesBegan’ methods, leave all other methods open, and click SView to get this:

In the meantime, I check the breakpoint to see if the corresponding View and ViewController are the first responders, and all the results are NO

Assuming that the AppDelegate -> ViewController -> View relationship is the root -> branch -> leaf of the tree, I have a preliminary summary based on the debugging results above:

  1. Occurs when the first responder is not identified because of the click actiontouchesEndedMethod recursively calls from leaf to root;
  2. touchesEndedThe recursive throw up call of the implementation has expiredtouchesBeganMethod responder.

I seem to have found a convincing argument based on the UIResponder touches method:

According to the interface description, the following points can be extracted:

  • You should rewrite all four Touches when customizable in response;
  • For a responder, if a click action is receivedtouchesBegan:withEvent:Event, then will also receive the same click actiontouchesEnded:withEvent:ortouchesCancelled:withEvent:Events;
  • The click cancel operation must be handled correctly; incorrect handling may result in incorrect behavior or crashes.

Based on the results of our actual debugging and the key points extracted from the interface specification, we can conclude the following:

  1. Occurs if the response chain cannot identify a clear first responder for a click operationtouchesEnded:withEvent:Recursive upthrow of methods;
  2. If there’s a responder in the responder chain that’s implementedtouchesBegan:withEvent:Method, and be responded to, thentouchesEnded:withEvent:The recursive upthrow of the method ends at the responder;
  3. If there is no responder in the responder chain implementedtouchesBegan:withEvent:Method, thentouchesEnded:withEvent:A recursive up-throw of a method will go up to the AppDelegate, and then end.

Similarly, if you let go of the Green SView’s touchesBegan method and comment out the ‘touchesEnded’ method, the response to a click would look like this:

Any touches found in the red PView will no longer be implemented. Analysis is as follows:

Because the Green SView’s touchesBegan method is implemented, but ‘touchesEnded’ is not overridden, the default action is done, which is to do nothing. Therefore, although there is no first responder in the response chain, the ‘touchesEnded’ recursive upthrow also ends at SView.

conclusion

Looking back at the original interview question, we can see that if Swizzle did the touchesBegan method for the parent View, it wouldn’t have any effect on the child View.

Compared with Swizzle’s method, the response chain between parent and child views deserves more attention. The previous summary of handling is as follows:

  1. Occurs if the response chain cannot identify a clear first responder for a click operationtouchesEnded:withEvent:Recursive upthrow of methods;
  2. If there’s a responder in the responder chain that’s implementedtouchesBegan:withEvent:Method, and be responded to, thentouchesEnded:withEvent:The recursive upthrow of the method ends at the responder;
  3. If there is no responder in the responder chain implementedtouchesBegan:withEvent:Method, thentouchesEnded:withEvent:A recursive up-throw of a method will go up to the AppDelegate, and then end.

Referring to the interface description, the dempresses correlation method has the same problems as the touches method.

Reference Documents:

Debug the iOS user interaction event response process

Using Responders and the Responder Chain to Handle Events