You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter iOS端特定页面防截图功能异常排查求助

问题:Flutter iOS端截图监听后无法触发防截图逻辑

开发Flutter应用时,需实现特定页面在用户截图时隐藏内容的功能,Android端已通过原生Kotlin代码完成,但iOS端出现两个核心问题:

  • 监听到截图事件后,调用preventScreenshots方法无响应(该方法内的打印语句未执行)
  • 将preventScreenshots的代码直接写入监听逻辑,虽有打印输出但无法实现空白屏效果

相关代码片段

原生Swift(AppDelegate.swift)

import UIKit
import Flutter
import flutter_downloader
import GoogleMaps
import FirebaseCore
import FirebaseMessaging

@main
@objc class AppDelegate: FlutterAppDelegate {
    var blackOverlayView: UIView?
    var methodChannel: FlutterMethodChannel?

    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        guard let controller = window?.rootViewController as? FlutterViewController else {
            print("Error: RootViewController is not FlutterViewController")
            return false
        }

        methodChannel = FlutterMethodChannel(name: "my_channel_name",
                                           binaryMessenger: controller.binaryMessenger)

        methodChannel?.setMethodCallHandler { [weak self] (call, result) in
            guard let self = self else {
                result(FlutterError(code: "ERROR", message: "Self is nil", details: nil))
                return
            }

            switch call.method {
            case "preventScreenshots":
                print("Inside my prevent screenshot")
                DispatchQueue.main.async {
                    print("Inside 1")
                    guard let controller = self.window?.rootViewController as? FlutterViewController else {
                        print("Inside else")
                        result(FlutterError(code: "ERROR", message: "Root view controller not found", details: nil))
                        return
                    }
                    print("Inside 2")
                    let field = UITextField()
                    field.isSecureTextEntry = true
                    print("Inside 3")
                    field.translatesAutoresizingMaskIntoConstraints = false
                    controller.view.addSubview(field)
                    print("Inside 4")
                    NSLayoutConstraint.activate([
                        field.centerYAnchor.constraint(equalTo: controller.view.centerYAnchor),
                        field.centerXAnchor.constraint(equalTo: controller.view.centerXAnchor)
                    ])
                    print("Inside 5")
                }
                result(nil)
            case "allowScreenshots":
                DispatchQueue.main.async {
                    controller.view.subviews.forEach { view in
                        if view is UITextField {
                            view.removeFromSuperview()
                        }
                    }
                }
                result(nil)
            case "showBlackOverlay":
                self.showBlackOverlay()
                result(nil)
            case "hideBlackOverlay":
                self.hideBlackOverlay()
                result(nil)
            case "addScreenshotListener":
                self.addScreenshotObserver()
                result(nil)
            case "removeScreenshotListener":
                self.removeScreenshotObserver()
                result(nil)
            default:
                result(FlutterMethodNotImplemented)
            }
        }

        FirebaseApp.configure()
        GMSServices.provideAPIKey("my_api_key")
        GeneratedPluginRegistrant.register(with: self)
        FlutterDownloaderPlugin.setPluginRegistrantCallback(registerPlugins)

        if #available(iOS 10.0, *) {
            UNUserNotificationCenter.current().delegate = self
            let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
            UNUserNotificationCenter.current().requestAuthorization(
                options: authOptions,
                completionHandler: { _, _ in }
            )
        } else {
            let settings: UIUserNotificationSettings =
                UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil)
            application.registerUserNotificationSettings(settings)
        }

        application.registerForRemoteNotifications()
        Messaging.messaging().delegate = self

        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }

    @objc private func handleScreenshotNotification() {
        DispatchQueue.main.async {
            guard let controller = self.window?.rootViewController as? FlutterViewController else { return }
            print("Listened to screenshot, now calling method")
            self.methodChannel?.invokeMethod("preventScreenshots", arguments: nil)
        }
    }

    private func addScreenshotObserver() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(self.handleScreenshotNotification),
            name: UIApplication.userDidTakeScreenshotNotification,
            object: nil
        )
        print("Screenshot listener added")
    }

    private func removeScreenshotObserver() {
        NotificationCenter.default.removeObserver(
            self,
            name: UIApplication.userDidTakeScreenshotNotification,
            object: nil
        )
        print("Screenshot listener removed")
    }

    private func showBlackOverlay() {
        guard let window = self.window else { return }
        if blackOverlayView == nil {
            blackOverlayView = UIView(frame: window.bounds)
            blackOverlayView?.backgroundColor = UIColor.black
            blackOverlayView?.isUserInteractionEnabled = false
            window.addSubview(blackOverlayView!)
        }
        blackOverlayView?.isHidden = false
    }

    private func hideBlackOverlay() {
        blackOverlayView?.isHidden = true
    }
}

extension AppDelegate: MessagingDelegate {
    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
        print("Firebase registration token: \(String(describing: fcmToken))")

        let dataDict: [String: String] = ["token": fcmToken ?? ""]
        NotificationCenter.default.post(
            name: Notification.Name("FCMToken"),
            object: nil,
            userInfo: dataDict
        )
    }
}

private func registerPlugins(registry: FlutterPluginRegistry) {
    if (!registry.hasPlugin("FlutterDownloaderPlugin")) {
        FlutterDownloaderPlugin.register(with: registry.registrar(forPlugin: "FlutterDownloaderPlugin")!)
    }
}

Flutter端(Method Channel代码)

class AppMethodChannels {
  static const _channel = MethodChannel("eduvate_admin/channel");

  ///Currently implemented for android only
  ///This method prevents user from taking screenshots
  static void preventScreenshots() {
    if (Platform.isAndroid || Platform.isIOS) {
      _channel.invokeMethod('preventScreenshots');
    }
  }

  ///Currently implemented for android only
  ///This method allows user to take screenshots
  static void allowScreenshots() {
    if (Platform.isAndroid || Platform.isIOS) {
      _channel.invokeMethod('allowScreenshots');
    }
  }

  ///Shows a black overlay on the screen.
  ///Currently implemented for iOS only
  static void showBlackOverlay() {
    if (Platform.isIOS) {
      _channel.invokeMethod('showBlackOverlay');
    }
  }

  ///Hides the black overlay from the screen.
  ///Currently implemented for iOS only
  static void hideBlackOverlay() {
    if (Platform.isIOS) {
      _channel.invokeMethod('hideBlackOverlay');
    }
  }

  static void setScreenshotListener() {
    if (Platform.isIOS) {
      print("Setting ss listener");
      _channel.invokeMethod("addScreenshotListener");
    }
  }
}

问题原因分析

  1. Method Channel名称不匹配
    原生Swift端创建的Channel名称是my_channel_name,但Flutter端使用的是eduvate_admin/channel,导致跨端方法调用无法匹配,所以preventScreenshots方法不会被触发。

  2. 截图事件触发时机滞后
    UIApplication.userDidTakeScreenshotNotification是截图完成后才发送的通知,此时再添加遮罩或安全输入框,已经无法覆盖已生成的截图内容,所以即使执行代码也看不到空白屏效果。

  3. 防截图视图尺寸不足
    原preventScreenshots方法中创建的UITextField只有居中的小尺寸,没有覆盖整个屏幕,即使添加成功也无法隐藏全部内容。

解决方法

1. 统一Method Channel名称

将原生端和Flutter端的Channel名称修改为一致,比如都使用eduvate_admin/channel:

// 原生Swift端修改
methodChannel = FlutterMethodChannel(name: "eduvate_admin/channel", binaryMessenger: controller.binaryMessenger)

2. 改用iOS13+截图前拦截方案

利用UIScreen的captured属性监听,在截图开始时就显示遮罩,确保遮罩能被包含在截图中:

// 修改AppDelegate中的监听相关方法
private func addScreenshotObserver() {
    // 移除旧的通知监听
    NotificationCenter.default.removeObserver(self, name: UIApplication.userDidTakeScreenshotNotification, object: nil)
    
    if #available(iOS 13.0, *) {
        // iOS13+使用屏幕捕获状态监听
        UIScreen.main.addObserver(self, forKeyPath: "captured", options: .new, context: nil)
    } else {
        // 兼容iOS13以下,保留旧通知
        NotificationCenter.default.addObserver(self, selector: #selector(self.handleScreenshotNotification), name: UIApplication.userDidTakeScreenshotNotification, object: nil)
    }
    print("Screenshot listener added")
}

private func removeScreenshotObserver() {
    if #available(iOS 13.0, *) {
        UIScreen.main.removeObserver(self, forKeyPath: "captured")
    } else {
        NotificationCenter.default.removeObserver(self, name: UIApplication.userDidTakeScreenshotNotification, object: nil)
    }
    print("Screenshot listener removed")
}

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if keyPath == "captured", let isCaptured = change?[.newKey] as? Bool, isCaptured {
        // 截图开始时立即显示遮罩
        DispatchQueue.main.async {
            self.showBlackOverlay()
            // 截图完成后延迟隐藏,避免影响正常界面操作
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                self.hideBlackOverlay()
            }
        }
    }
}

3. 优化防截图视图实现

如果仍使用安全输入框方案,需要创建全屏的输入框以覆盖全部内容:

// 修改preventScreenshots分支代码
case "preventScreenshots":
    print("Inside my prevent screenshot")
    DispatchQueue.main.async {
        guard let controller = self.window?.rootViewController as? FlutterViewController else {
            print("Root view controller not found")
            result(FlutterError(code: "ERROR", message: "Root view controller not found", details: nil))
            return
        }
        // 创建全屏安全输入框
        let field = UITextField(frame: controller.view.bounds)
        field.isSecureTextEntry = true
        field.backgroundColor = .black // 设置背景色确保完全覆盖内容
        controller.view.addSubview(field)
    }
    result(nil)

内容的提问来源于stack exchange,提问作者Shreyansh Sharma

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 12:32:33