Welcome to follow the official wechat account: FSA Full stack action 👋

A, problem,

I jumped to the Flutter page first and added a native popover view to the Flutter page after 1 second.

let flutterVc = FlutterViewController(engine: fetchFlutterEngine(), nibName: nil, bundle: nil)
self.navigationController?.pushViewController(flutterVc, animated: true)

DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1)) {[self] in
    
    let popView = LXFPopView(frame: CGRect(x: 0, y: 0, width: screenW, height: screenH))
    flutterVc.view.addSubview(popView)
    popView.checkInfoBlock ={[weak self] in
        guard let self = self else { return }
        self.navigationController?.pushViewController(InfoViewController(), animated: true)}}Copy the code

As you can see, when I swipe and click on the native popover view, I am responding to the underlying Flutter content

Some people say, well, you can just add it to navigationController’s View.

// flutterVc.view.addSubview(popView) self.navigationController? .view.addSubview(popView)Copy the code

No, because jumping to other pages will block other page content, look at the effect of a clear picture

Let’s take a look at the Source code for FlutterViewController to see why

Second, the source

1, location matching source code

Start by finding the source content that matches the current Flutter environment

➜ flutter doctor -v [✓] flutter (Channel stable, 2.10.4, on macOS 12.2.1 21D62 darwin-x64, The locale useful - Hans - CN), Flutter version 2.10.4 ats/Users/LXF/developer/Flutter, Upstream repository https://github.com/flutter/flutter.git • Framework revision c860cba910 (9 days line), 2022-03-25 00:23:12-0500 • Engine Revision 57D3BAC3DD • Dart Version 2.16.2 • DevTools version 2.9.2 • Pub Download Mirror https://pub.flutter-io.cn • Flutter Download mirror https://storage.flutter-io.cn...Copy the code

This is where you get the Commit ID of the Engine

Engine revision 57d3bac3dd
Copy the code

Replace the [commit ID] in the link below to get the matching source link

https://github.com/flutter/engine/blob/ / shell/platform/commit id 】 【 Darwin/ios/framework/Source/FlutterViewController. MmCopy the code

Link: engine/FlutterViewController. At 57 mm d3bac3dd

2, locate the source that caused the problem

After viewing the source code, you can quickly locate the following parts of the content:

. - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
  [self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
}

- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
  [self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
}

- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
  [self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
}

- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
  [self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
}

- (void)forceTouchesCancelled:(NSSet*)touches {
  flutter::PointerData::Change cancel = flutter::PointerData::Change::kCancel;
  [selfdispatchTouches:touches pointerDataChangeOverride:&cancel event:nullptr]; }...// Dispatches the UITouches to the engine. Usually, the type of change of the touch is determined
// from the UITouch's phase. However, FlutterAppDelegate fakes touches to ensure that touch events
// in the status bar area are available to framework code. The change type (optional) of the faked
// touch is specified in the second argument.
- (void)dispatchTouches:(NSSet*)touches
    pointerDataChangeOverride:(flutter::PointerData::Change*)overridden_change
                        event:(UIEvent*)event {
...
}
Copy the code

As you can see, inside the FlutterViewController, rewrite the touchesXXX series method, and then unified call – dispatchTouches: pointerDataChangeOverride: event: Method, distribute UITouches to the Flutter engine to interact with the Flutter content

This is why our click-and-drag actions on the original pop-up window are responded to by the Flutter content.

Third, solve the problem

Now that we know the cause, it’s time to find a way to solve the problem

Here I give the final implementation code directly:

// LXFFlutterViewController.swift

import Foundation
import Flutter

protocol LXFFlutterForbidResponseProtocol {}class LXFFlutterViewController: FlutterViewController {
    
    var isForbidResponseForFlutter = false
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?). {
        if self.isForbidResponse() {
            self.isForbidResponseForFlutter = true
            return
        }
        print("touches began")
        super.touchesBegan(touches, with: event)
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?). {
        if isForbidResponseForFlutter { return }
        print("touches move")
        super.touchesMoved(touches, with: event)
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?). {
        if isForbidResponseForFlutter {
            self.isForbidResponseForFlutter = false
            return
        }
        print("touches ended")
        super.touchesEnded(touches, with: event)
    }
    
    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?). {
        if self.isForbidResponse() {
            self.isForbidResponseForFlutter = false
            return
        }
        print("touches cacelled")
        super.touchesCancelled(touches, with: event)
    }
}

extension LXFFlutterViewController {
    func isForbidResponse(a) -> Bool {
        var subViews = self.view?.subviews ?? []
        subViews = subViews.reversed()
        
        for i in 0..<subViews.count {
            let subView = subViews[i]
            if self.isHadForbidResponseView(view: subView) {
                return true}}return false
    }
    
    fileprivate func isHadForbidResponseView(view: UIView) -> Bool {
        if view is LXFFlutterForbidResponseProtocol {
            return true
        }
        let subViews = view.subviews
        for i in 0..<subViews.count {
            let subView = subViews[i]
            if self.isHadForbidResponseView(view: subView) {
                return true}}return false}}Copy the code

As can be seen from the figure above, in FlutterView, popover views such as LXFPopView are generally inserted into the view when they are used. Therefore, reverse traversal of sub-views is performed in isForbidResponse method to reduce traversal times.

TouchesMoved call number is more, so in order to avoid the high frequency in the touchesMoved traversal subViews, used isForbidResponseForFlutter variables here, When touchesBegan judgment and record whether need to disable the Flutter content respond to touch events, in touchesEnded and touchesCancelled isForbidResponseForFlutter reset to false

Four, the use of steps

Step one:

Using LXFFlutterViewController

let flutterVc = LXFFlutterViewController(engine: fetchFlutterEngine(), nibName: nil, bundle: nil)
Copy the code

Step 2:

The pop-up view class abide by the agreement: where LXFFlutterForbidResponseProtocol

extension LXFPopView: LXFFlutterForbidResponseProtocol {}Copy the code

See how it looks:

Perfect 😃

LinXunFeng/ Flutter_hybrid_touch_response_demo (github.com)