Flutter项目结合iOS OC原生代码
需求:
设备截图,隐藏原有页面,截取到的内容需要展示'敏感数据,禁止截屏';
如果返回桌面进入网格桌面展示'敏感数据,禁止截屏'需要在
ScreenShieldView.m文件中 的 appWillLeave self.protectedView.hidden = NO;注释掉
iOS原生代码
ScreenShieldView.h
objectivec
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
API_AVAILABLE(ios(13.0))
@interface ScreenShieldView : UIView
+ (instancetype)createWithFrame:(CGRect)frame;
+ (instancetype)createWithFrame:(CGRect)frame protectedView:(UIView * _Nullable)protectedView;
@end
NS_ASSUME_NONNULL_END
ScreenShieldView.m
objectivec
#import "ScreenShieldView.h"
@interface ScreenShieldView ()
@property (nonatomic, strong, nullable) UIView *protectedView;
@property (nonatomic, strong, nullable) UIView *portiereView;
@property (nonatomic, strong, nullable) UIView *safeZone;
@end
@implementation ScreenShieldView
+ (instancetype)createWithFrame:(CGRect)frame {
return [[self alloc] initWithFrame:frame protectedView:nil];
}
+ (instancetype)createWithFrame:(CGRect)frame protectedView:(UIView * _Nullable)protectedView {
return [[self alloc] initWithFrame:frame protectedView:protectedView];
}
- (void)setBackgroundColor:(UIColor *)backgroundColor {
[super setBackgroundColor:backgroundColor];
self.portiereView.backgroundColor = backgroundColor;
}
- (instancetype)initWithFrame:(CGRect)frame protectedView:(UIView * _Nullable)protectedView {
self = [super initWithFrame:frame];
if (self) {
_safeZone = [self makeSecureView] ?: [[UIView alloc] init];
_protectedView = protectedView;
if (_safeZone) {
if (self.protectedView) {
[self.protectedView removeFromSuperview];
[super addSubview:self.protectedView];
self.protectedView.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[self.protectedView.topAnchor constraintEqualToAnchor:self.topAnchor],
[self.protectedView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
[self.protectedView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
[self.protectedView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor]
]];
}
[self addSubview:_safeZone];
UILayoutPriority layoutDefaultLowPriority = UILayoutPriorityDefaultLow - 1;
UILayoutPriority layoutDefaultHighPriority = UILayoutPriorityDefaultHigh - 1;
_safeZone.translatesAutoresizingMaskIntoConstraints = NO;
[_safeZone setContentHuggingPriority:layoutDefaultLowPriority forAxis:UILayoutConstraintAxisVertical];
[_safeZone setContentHuggingPriority:layoutDefaultLowPriority forAxis:UILayoutConstraintAxisHorizontal];
[_safeZone setContentCompressionResistancePriority:layoutDefaultHighPriority forAxis:UILayoutConstraintAxisVertical];
[_safeZone setContentCompressionResistancePriority:layoutDefaultHighPriority forAxis:UILayoutConstraintAxisHorizontal];
[NSLayoutConstraint activateConstraints:@[
[_safeZone.topAnchor constraintEqualToAnchor:self.topAnchor],
[_safeZone.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
[_safeZone.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
[_safeZone.trailingAnchor constraintEqualToAnchor:self.trailingAnchor]
]];
if (self.protectedView) {
self.portiereView = [[UIView alloc] init];
self.portiereView.backgroundColor = UIColor.whiteColor;
[self addSubview:self.portiereView];
self.portiereView.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[self.portiereView.topAnchor constraintEqualToAnchor:self.topAnchor],
[self.portiereView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
[self.portiereView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
[self.portiereView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor]
]];
}
[NSNotificationCenter.defaultCenter addObserver:self
selector:@selector(appWillLeave:)
name:UIApplicationWillResignActiveNotification
object:nil];
[NSNotificationCenter.defaultCenter addObserver:self
selector:@selector(appDidBack:)
name:UIApplicationDidBecomeActiveNotification
object:nil];
}
}
return self;
}
- (void)appWillLeave:(NSNotification *)notify {
// self.protectedView.hidden = NO;
// NSLog(@"appWillLeave");
}
- (void)appDidBack:(NSNotification *)notify {
self.protectedView.hidden = NO;
}
- (void)addSubview:(UIView *)view {
if (self.safeZone && view != self.safeZone) {
[self.safeZone addSubview:view];
} else {
[super addSubview:view];
}
}
- (void)insertSubview:(UIView *)view belowSubview:(UIView *)siblingSubview {
if (self.safeZone && view != self.safeZone) {
[self.safeZone insertSubview:view belowSubview:siblingSubview];
} else {
[super insertSubview:view belowSubview:siblingSubview];
}
}
- (void)insertSubview:(UIView *)view aboveSubview:(UIView *)siblingSubview {
if (self.safeZone && view != self.safeZone) {
[self.safeZone insertSubview:view aboveSubview:siblingSubview];
} else {
[super insertSubview:view aboveSubview:siblingSubview];
}
}
- (void)insertSubview:(UIView *)view atIndex:(NSInteger)index {
if (self.safeZone && view != self.safeZone) {
if (self.protectedView && index == 0) {
[self.safeZone insertSubview:view atIndex:1];
} else {
[self.safeZone insertSubview:view atIndex:index];
}
} else {
[super insertSubview:view atIndex:index];
}
}
- (void)exchangeSubviewAtIndex:(NSInteger)index1 withSubviewAtIndex:(NSInteger)index2 {
if (self.safeZone) {
[self.safeZone exchangeSubviewAtIndex:index1 withSubviewAtIndex:index2];
} else {
[super exchangeSubviewAtIndex:index1 withSubviewAtIndex:index2];
}
}
- (void)bringSubviewToFront:(UIView *)view {
if (self.safeZone && view != self.safeZone) {
[self.safeZone bringSubviewToFront:view];
} else {
[super bringSubviewToFront:view];
}
}
- (void)sendSubviewToBack:(UIView *)view {
if (self.safeZone && view != self.safeZone) {
[self.safeZone sendSubviewToBack:view];
} else {
[super sendSubviewToBack:view];
}
}
- (UIView *)makeSecureView {
if (![self isOSVersionSafe]) {
return nil;
}
UITextField *field = [[UITextField alloc] init];
field.secureTextEntry = YES;
UIView *fv = field.subviews.firstObject;
for (UIView *subview in fv.subviews) {
[subview removeFromSuperview];
}
fv.userInteractionEnabled = YES;
NSString *errorMsg = @"[ScreenShieldView log] Create safeZone failed!";
#if DEBUG
NSAssert(fv != nil, errorMsg);
#else
if (!fv) {
NSLog(@"%@", errorMsg);
}
#endif
return fv;
}
- (NSArray<NSString *> *)unsafeOSVersion {
return @[@"15.1"];
}
- (NSString *)osVersion {
return UIDevice.currentDevice.systemVersion;
}
- (BOOL)isOSVersionSafe {
for (NSString *version in [self unsafeOSVersion]) {
if ([self.osVersion containsString:version]) {
return NO;
}
}
return YES;
}
- (void)dealloc {
[NSNotificationCenter.defaultCenter removeObserver:self];
}
@end
ScreenShieldPlaceholder.h
objectivec
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface ScreenShieldPlaceholder : UIView
@end
NS_ASSUME_NONNULL_END
objectivec
#import "ScreenShieldPlaceholder.h"
#define SCREEN_HEIGHT [[UIScreen mainScreen] bounds].size.height
#define SCREEN_WIDTH [[UIScreen mainScreen] bounds].size.width
@implementation ScreenShieldPlaceholder
- (instancetype)init
{
self = [super init];
if (self) {
self.frame = CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
[self createUI];
}
return self;
}
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self createUI];
}
return self;
}
-(void)createUI{
UIView * navView = [[UIView alloc]initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, [self getNavHeight]+[self getStatusHeight])];
navView.backgroundColor = [UIColor whiteColor];
[self addSubview:navView];
UIView * stV = [[UIView alloc]initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, [self getStatusHeight])];
stV.backgroundColor = [UIColor clearColor];
[navView addSubview:stV];
UILabel * titleLab = [[UILabel alloc]initWithFrame:CGRectMake(0, CGRectGetMaxY(stV.frame), SCREEN_WIDTH, [self getNavHeight])];
titleLab.backgroundColor = [UIColor clearColor];
titleLab.text = @"APP名字";
titleLab.textColor = [UIColor colorWithRed:51/255.f green:51/255.f blue:51/255.f alpha:1];
titleLab.font = [UIFont boldSystemFontOfSize:17];
titleLab.textAlignment = NSTextAlignmentCenter;
[navView addSubview:titleLab];
UIView * botView = [[UIView alloc]initWithFrame:CGRectMake(0, CGRectGetMaxY(navView.frame), SCREEN_WIDTH, SCREEN_HEIGHT - ([self getNavHeight]+[self getStatusHeight]))];
botView.backgroundColor = [UIColor colorWithRed:249/255.0 green:249/255.0 blue:249/255.0 alpha:1];
[self addSubview:botView];
UIImageView *imgView = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 60, 60)];
imgView.center = CGPointMake(botView.center.x, CGRectGetHeight(botView.frame)/2-100);
imgView.image = [UIImage imageNamed:@"min_gan_ios"];
[botView addSubview:imgView];
UILabel * textLab = [[UILabel alloc]initWithFrame:CGRectMake(0, CGRectGetMaxY(imgView.frame), CGRectGetWidth(botView.frame), 50)];
textLab.text = @"敏感数据,禁止截屏";
textLab.textColor = [UIColor blackColor];
textLab.font = [UIFont systemFontOfSize:20.f];
textLab.textAlignment = NSTextAlignmentCenter;
[botView addSubview:textLab];
}
-(CGFloat)getNavHeight{
return 44.0;
}
-(CGFloat)getStatusHeight{
CGFloat statusBarHeight = [UIApplication sharedApplication].statusBarFrame.size.height;
return statusBarHeight;
}
@end
ScreenshotChannel.h
objectivec
#import <Foundation/Foundation.h>
#import <Flutter/Flutter.h>
NS_ASSUME_NONNULL_BEGIN
@interface ScreenshotChannel : NSObject
+(void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar;
@end
NS_ASSUME_NONNULL_END
ScreenshotChannel.m
objectivec
#import "ScreenshotChannel.h"
#import "ScreenShieldView.h"
#import "ScreenShieldPlaceholder.h"
@interface ScreenshotChannel ()<FlutterPlugin,FlutterStreamHandler>
@property(nonatomic,strong)FlutterResult result;
@property (nonatomic, strong) FlutterEventSink eventSink;
@property (nonatomic, strong) UIView *containerView;
@property(nonatomic,strong)ScreenShieldView *currentShieldView;
// 记录 Flutter 核心视图的原父视图和原索引(用于还原)
@property (nonatomic, weak) UIView *flutterOriginalSuperview; // 弱引用,避免内存泄漏
@property (nonatomic, assign) NSInteger flutterOriginalIndex; // 原父视图中的层级索引
@end
@implementation ScreenshotChannel
+ (instancetype)sharedInstance {
static ScreenshotChannel *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
+ (void)registerWithRegistrar:(nonnull NSObject<FlutterPluginRegistrar> *)registrar {
FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:@"ios_screenshot" binaryMessenger:[registrar messenger]];
ScreenshotChannel * instance = [ScreenshotChannel sharedInstance];
[registrar addMethodCallDelegate:instance channel:channel];
}
- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result{
if ([call.method isEqualToString:@"enableSecure"]) {
[self addSecureOverlay];
result(nil);
}
if ([call.method isEqualToString:@"disableSecure"]) {
// 关闭禁止截屏
[self removeSecureOverlay];
result(nil);
}
}
// 添加安全覆盖层(核心:阻止截屏)
- (void)addSecureOverlay {
UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
if (!keyWindow || !keyWindow.rootViewController) {
return;
}
UIViewController *rootVC = keyWindow.rootViewController;
// 找到 Flutter 核心视图(真正承载 UI 的视图,必须转移它本身)
UIView *flutterCoreView = [self findFlutterCoreViewInView:rootVC.view];
if (!flutterCoreView) {
return;
}
self.flutterOriginalSuperview = flutterCoreView.superview; // 原父视图
self.flutterOriginalIndex = [self.flutterOriginalSuperview.subviews indexOfObject:flutterCoreView]; // 原索引位置
self.currentShieldView = [ScreenShieldView createWithFrame:flutterCoreView.frame protectedView:[[ScreenShieldPlaceholder alloc]init]]; // 占满屏幕
self.currentShieldView.backgroundColor = [UIColor whiteColor];
self.containerView = [[UIView alloc] initWithFrame:self.currentShieldView.frame];
self.containerView.backgroundColor = [UIColor whiteColor];
[flutterCoreView removeFromSuperview];
[self.containerView addSubview:flutterCoreView];
//修正约束:让 FlutterCoreView 占满 containerView(适配屏幕旋转/尺寸变化)
flutterCoreView.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[flutterCoreView.leadingAnchor constraintEqualToAnchor:self.containerView.leadingAnchor],
[flutterCoreView.trailingAnchor constraintEqualToAnchor:self.containerView.trailingAnchor],
[flutterCoreView.topAnchor constraintEqualToAnchor:self.containerView.topAnchor],
[flutterCoreView.bottomAnchor constraintEqualToAnchor:self.containerView.bottomAnchor]
]];
self.currentShieldView.backgroundColor = [UIColor whiteColor];
self.currentShieldView.autoresizingMask = flutterCoreView.autoresizingMask;
self.currentShieldView.autoresizesSubviews = flutterCoreView.autoresizesSubviews;
[self.currentShieldView addSubview:self.containerView];
[keyWindow addSubview:self.currentShieldView];
NSLog(@"添加安全覆盖层完成");
}
- (UIView *)findFlutterCoreViewInView:(UIView *)view {
if ([view isKindOfClass:NSClassFromString(@"FlutterView")] ||
[view isKindOfClass:NSClassFromString(@"FlutterSurfaceView")]) {
return view;
}
for (UIView *subview in view.subviews) {
UIView *flutterView = [self findFlutterCoreViewInView:subview];
if (flutterView) return flutterView;
}
return nil;
}
// 移除安全覆盖层
- (void)removeSecureOverlay {
if (!self.currentShieldView || !self.containerView || !self.flutterOriginalSuperview) {
NSLog(@"无需要移除的遮罩视图或原父视图已释放");
return;
}
UIView *flutterCoreView = nil;
for (UIView *subview in self.containerView.subviews) {
if ([subview isKindOfClass:NSClassFromString(@"FlutterView")] ||
[subview isKindOfClass:NSClassFromString(@"FlutterSurfaceView")]) {
flutterCoreView = subview;
break;
}
}
if (flutterCoreView) {
//还原到原父视图的原索引位置
[flutterCoreView removeConstraints:flutterCoreView.constraints]; // 移除添加时的约束
flutterCoreView.translatesAutoresizingMaskIntoConstraints = YES; // 恢复默认适配
[flutterCoreView removeFromSuperview];
// 还原到原父视图:如果原父视图还存在,添加到原索引位置;否则添加到 rootVC.view
if (self.flutterOriginalSuperview.superview) { // 原父视图未被释放
[self.flutterOriginalSuperview insertSubview:flutterCoreView atIndex:self.flutterOriginalIndex];
} else { // 原父视图已释放(极端情况),降级添加到 rootVC.view
UIViewController *rootVC = [UIApplication sharedApplication].keyWindow.rootViewController;
if (rootVC && rootVC.view) {
[rootVC.view addSubview:flutterCoreView];
flutterCoreView.frame = rootVC.view.bounds;
flutterCoreView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
}
}
// 还原原有的 autoresizingMask(确保和添加前一致)
flutterCoreView.autoresizingMask = self.currentShieldView.autoresizingMask;
}
[self.containerView removeFromSuperview];
[self.currentShieldView removeFromSuperview];
self.containerView = nil;
self.currentShieldView = nil;
self.flutterOriginalSuperview = nil; // 弱引用无需手动释放,但置空避免重复使用
self.flutterOriginalIndex = 0;
NSLog(@"遮罩已成功移除,Flutter 视图还原完成");
}
// 获取当前活跃窗口
- (UIWindow *)getCurrentKeyWindow {
if (@available(iOS 13.0, *)) {
for (UIScene *scene in [UIApplication sharedApplication].connectedScenes) {
if (scene.activationState == UISceneActivationStateForegroundActive && [scene isKindOfClass:[UIWindowScene class]]) {
UIWindowScene *windowScene = (UIWindowScene *)scene;
for (UIWindow *win in windowScene.windows) {
if (win.isKeyWindow) {
return win;
}
}
}
}
} else {
return [UIApplication sharedApplication].keyWindow;
}
return nil;
}
- (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments eventSink:(FlutterEventSink)eventSink {
self.eventSink = eventSink;
return nil;
}
- (FlutterError* _Nullable)onCancelWithArguments:(id _Nullable)arguments {
self.eventSink = nil;
return nil;
}
@end
AppDelegate.m
objectivec
#import "AppDelegate.h"
#import "GeneratedPluginRegistrant.h"
#import "ScreenshotChannel.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[GeneratedPluginRegistrant registerWithRegistry:self];
[ScreenshotChannel registerWithRegistrar:[self registrarForPlugin:@"ios_screenshot"]];
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
@end
Flutterchannel
Dart
class ScreenshotChannel{
///iOS 截屏
static const MethodChannel screenshotIos = MethodChannel('ios_screenshot');
// iOS开启禁止截屏
static Future<void> enableSecureModeIOS() async {
await screenshotIos.invokeMethod('enableSecure');
}
// iOS关闭禁止截屏
static Future<void> disableSecureModeIOS() async {
await screenshotIos.invokeMethod('disableSecure');
}
}
Flutter页面调用
Dart
@override
void initState() {
// TODO: implement initState
super.initState();
Future.delayed(const Duration(milliseconds: 1000),(){
ScreenshotChannel.enableSecureModeIOS();
});
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
ScreenshotChannel.disableSecureModeIOS();
}