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"); } } }
问题原因分析
Method Channel名称不匹配
原生Swift端创建的Channel名称是my_channel_name,但Flutter端使用的是eduvate_admin/channel,导致跨端方法调用无法匹配,所以preventScreenshots方法不会被触发。截图事件触发时机滞后
UIApplication.userDidTakeScreenshotNotification是截图完成后才发送的通知,此时再添加遮罩或安全输入框,已经无法覆盖已生成的截图内容,所以即使执行代码也看不到空白屏效果。防截图视图尺寸不足
原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
相关产品推荐
相关产品推荐

