iOS防截屏实战

一、背景

1、由于系统限制,iOS无法禁止用户的截屏行为;只有在发生截屏时触发了一个截屏通知-- UIApplication.userDidTakeScreenshotNotification

2、但是这个通知只起到告知作用,收到这个通知时,截屏已经发生了,截屏的内容会以图片的方式存到相册;

3、从系统特性层面来看,似乎无法限制iOS防截屏;

4、即然系统无法限制截屏,那我们就想办法修改截屏的内容呗!!!让截屏截了个寂寞😂

二、收益

  • 1、像我们做数据安全的公司,有些数据不希望用户随便就能分享出去,防截屏就挺不错的;
  • 2、一些设计类的App,比如美图,设计的图片在你没开会员前,也不允许随便分享出去,防截屏也很有用;
  • 3、当然,用另一个手机拍照咱就没招了,防截屏也是防君子不防"小可爱";

三、技术方案

基于 UITextField 的安全文本输入特性isSecureTextEntry)以及 私有视图层级的利用,从而在截屏或录屏时隐藏敏感内容。

现成的第三方库:github.com/RyukieSama/...

arduino 复制代码
pod 'RyukieSwifty/ScreenShield'

ScreenShield的用法很简单,我就不介绍了,有兴趣的自己玩儿。

四、技术细节

1、首先在UIViewController的loadView方法中设置 self.viewScreenShieldView

2、然后布局在self.view上的子视图都会加到安全图层上,整个页面就具有了防截屏功能;

3、当用户截屏时,截屏出来的将是一个空白页面,这样就起到了防截屏,防录屏的作用;

五、问题分析

真正使用过程中会遇到如下问题:

问题一:self.view.subviews数组存放的子视图不是真正添加到self.view上的子视图?,就是说无法通过self.view.subviews获取子视图数量了

查看ScreenShieldView的原码可以发现,虽然我们在loadView中将self.view设置成了ScreenShieldView,并且是通过self.view添加的子视图,但其实所有子视图都添加到了ScreenShieldViewsafeZone上了。

分析问题

想重写subviews方法?可以试一试!

swift 复制代码
public override var subviews: [UIView]{
    guard let safe = safeZone else {
        return super.subviews
    }
    return safe.subviews
 }

运行发现所有添加到self.view上的子视图都不显示了

  • 1、subviews不止开发者会调用,UIView内容系统也会调用,重写了subviews后系统调用的将是重写后的函数,并且返回的将是safe.subviews;
  • 2、所有子视图实际是被添加到safeZone上的,但是设置的约束都是与self.view的。重写subviews函数后,约束设置会失效;
  • 3、所以重写subviews函数会导致使用约束布局失效,所有通过约束添加到self.view上的子视图都将无法正常显示。但是不影响frame布局

解决方案

实现一个新的获取子视图的函数来获取真正的子视图

swift 复制代码
public var safeSubviews: [UIView]{
    guard let safe = safeZone else {
        return super.subviews
    }
    return safe.subviews
 }

问题二:ScreenShieldView无法被继承,因为ScreenShieldView只能通过create函数创建才有防截屏图层

分析问题

如果我们想在某个自定义的View中也加上防截屏图层,但是呢又不想改变View的初始化方法,但是通过将View继承自ScreenShieldView又行不通,因为ScreenShieldView只能通过create函数创建。

解决方案

既然不能继承,那可以加个中间层,我们可以创建一个BaseView,将ScreenShieldView添加到BaseView上,然后像ScreenShieldView的实现方式一样,将所有添加子视图的方法全部重写。

swift 复制代码
//
//  BaseScreenShieldView.swift
//  ScreenShieldDemo
//
//  Created by 熊进辉 on 2025/7/12
//  Copyright © 2025/7/12 datacloak. All rights reserved.
//
    

import UIKit
import SnapKit

class BaseScreenShieldView: UIView {
    private var contentView:ScreenShieldView? = nil
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.setupContent()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    private func setupContent(){
        self.contentView = ScreenShieldView.create(frame: CGRectZero)
        self.addSubview(self.contentView!)
        self.contentView?.snp.makeConstraints { make in
            make.top.equalTo(0)
            make.leading.equalTo(0)
            make.bottom.equalTo(0)
            make.trailing.equalTo(0)
        }
    }
   
    override func addSubview(_ view: UIView) {
        if (contentView != nil) {
            contentView!.addSubview(view)
        } else {
            super.addSubview(view)
        }
    }

    override func insertSubview(_ view: UIView, at index: Int) {
        if (contentView != nil) {
            contentView!.insertSubview(view, at: index)
        } else {
            super.insertSubview(view, at: index)
        }
    }

    override func insertSubview(_ view: UIView, aboveSubview siblingSubview: UIView) {
        if (contentView != nil) {
            contentView!.insertSubview(view, aboveSubview: siblingSubview)
        } else {
            super.insertSubview(view, aboveSubview: siblingSubview)
        }
    }

    override func insertSubview(_ view: UIView, belowSubview siblingSubview: UIView) {
        if (contentView != nil) {
            contentView!.insertSubview(view, belowSubview: siblingSubview)
        } else {
            super.insertSubview(view, belowSubview: siblingSubview)
        }
    }

    override func exchangeSubview(at index1: Int, withSubviewAt index2: Int) {
        if (contentView != nil) {
            contentView!.exchangeSubview(at: index1, withSubviewAt: index2)
        } else {
            super.exchangeSubview(at: index1, withSubviewAt: index2)
        }
    }

    override func bringSubviewToFront(_ view: UIView) {
        if (contentView != nil) {
            contentView!.bringSubviewToFront(view)
        } else {
            super.bringSubviewToFront(view)
        }
    }

    override func sendSubviewToBack(_ view: UIView) {
        if (contentView != nil) {
            contentView!.sendSubviewToBack(view)
        } else {
            super.sendSubviewToBack(view)
        }
    }
}

问题三:如何定制化截屏样式

分析问题 先来看添加ScreenShieldView后的图层

1、可以看到ViewController上的图层self.view就是ScreenShieldView图层; 2、self.view的子视图是添加在_UITextLayoutCanvasView上的,即safeZone;

再来看看ScreenShieldView的safeZone是添加在哪里的

分析代码发现,safeZone是添加到ScreenShieldView的;

结合图层和代码分析,要显示的内容需要添加到safeZone上,而要定制化的截屏图层需要放在ScreenShieldView上,并且在safeZone图层下方

解决方案 在safeZone下方放一个protectedView

swift 复制代码
@objc public static func create(frame: CGRect = .zero, protectedView: UIView?) -> ScreenShieldView {
        return ScreenShieldView(frame: frame,protectedView: protectedView)
    }
    
    private init(frame: CGRect, protectedView: UIView?) {
        super.init(frame: frame)
        safeZone = makeSecureView() ?? UIView()
        self.protectedView = protectedView
        
        if let sf = safeZone {
            if self.protectedView != nil {
                self.protectedView?.removeFromSuperview()
                super.addSubview(self.protectedView!)
                self.protectedView!.snp.makeConstraints { make in
                    make.top.equalTo(0)
                    make.bottom.equalTo(0)
                    make.left.equalTo(0)
                    make.right.equalTo(0)
                }
            }
            
            addSubview(sf)
            
            let layoutDefaultLowPriority = UILayoutPriority(rawValue: UILayoutPriority.defaultLow.rawValue-1)
            let layoutDefaultHighPriority = UILayoutPriority(rawValue: UILayoutPriority.defaultHigh.rawValue-1)
            
            sf.translatesAutoresizingMaskIntoConstraints = false
            sf.setContentHuggingPriority(layoutDefaultLowPriority, for: .vertical)
            sf.setContentHuggingPriority(layoutDefaultLowPriority, for: .horizontal)
            sf.setContentCompressionResistancePriority(layoutDefaultHighPriority, for: .vertical)
            sf.setContentCompressionResistancePriority(layoutDefaultHighPriority, for: .horizontal)
            
            let top = NSLayoutConstraint.init(item: sf, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1, constant: 0)
            let bottom = NSLayoutConstraint.init(item: sf, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1, constant: 0)
            let leading = NSLayoutConstraint.init(item: sf, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1, constant: 0)
            let trailing = NSLayoutConstraint.init(item: sf, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1, constant: 0)
            
            self.addConstraints([top, bottom, leading, trailing])
        }
    }

通过传递进来的protectedView添加到ScreenShieldView上,将protectedView的背景颜色设置为红色,将self.view的背景颜色设置为黄色;然后显示时,页面显示红色,再截屏,截屏出来的图片背景也是红色的。说明这样做是可以定制截屏图层的。

但是同样引入了一个新的问题:设置的self.view的黄色不生效了,也就是说虽然定制了截屏页面样式,但是这个样式成为了所有的页面背景。

这不是我们想要的,我们想要的是只有截屏时,这个截屏图层才显示在截屏图片上,正常情况下不要显示出来

解决截屏图层异常显示的问题

我们在safeZone的子视图最下面的图层上,再放置一个图层,用于显示self.view的背景颜色,然后重写背景颜色的setter函数

swift 复制代码
//修改背景颜色
public override var backgroundColor: UIColor? {
        get {
            super.backgroundColor
        }
        set {
            super.backgroundColor = newValue
            self.portiereView?.backgroundColor = newValue
        }
    }

private init(frame: CGRect, protectedView: UIView?) {
 上面的代码不变...
 if self.protectedView != nil {
    let portiereView = UIView()
    portiereView.backgroundColor = .white
    self.addSubview(portiereView)
    portiereView.snp.makeConstraints { make in
        make.top.equalTo(0)
        make.bottom.equalTo(0)
        make.left.equalTo(0)
        make.right.equalTo(0)
    }
    self.portiereView = portiereView
  }
}
   

上面的操作虽然解决了self.view背景颜色失效的问题,但是改变了safeZone的子视图数量,所以要修改以下函数:

swift 复制代码
    public override func insertSubview(_ view: UIView, at index: Int) {
        guard
            let safe = safeZone,
            view != safeZone
        else {
            super.insertSubview(view, at: index)
            return
        }
        if self.protectedView != nil ,index == 0 {
            safe.insertSubview(view, at: 1)
        }else{
            safe.insertSubview(view, at: index)
        }
    }

    public override func exchangeSubview(at index1: Int, withSubviewAt index2: Int) {
        guard
            let safe = safeZone
        else {
            super.exchangeSubview(at: index1, withSubviewAt: index2)
            return
        }
        safe.exchangeSubview(at: index1, withSubviewAt: index2)
    }

问题四:ScreenShieldView如何应用到swiftUI中

问题分析 根据swiftUI的特性,图层都是一层一层叠上去的,所以我们应该将ScreenShieldView放在body的最下方

解决方案 使用UIViewRepresentable将ScreenShieldView进行封装,使其可以在swiftUI上使用

swift 复制代码
import SwiftUI

struct ScreenShieldSwiftUIView<Content: View>: UIViewRepresentable{
    let content: Content
    let antiScreenshot:Bool

    init(antiScreenshot:Bool, @ViewBuilder content: () -> Content ) {
        self.antiScreenshot = antiScreenshot
        self.content = content()
    }

    // MARK: - Coordinator 用于缓存 HostingController
    class Coordinator {
        var hostingController: UIHostingController<Content>?

        init(_ hostingController: UIHostingController<Content>?) {
            self.hostingController = hostingController
        }
    }

    func makeCoordinator() -> Coordinator {
        return Coordinator(nil)
    }

    func makeUIView(context: Context) -> UIView {
        var shieldView:UIView
        if self.antiScreenshot == true{
            let protectedView = UIView();
            shieldView = ScreenShieldView.create(frame: .zero,protectedView: protectedView)
        }else{
            shieldView = UIView()
        }

        let hostingController = UIHostingController(rootView: content)
        context.coordinator.hostingController = hostingController

        let hostedView = hostingController.view!
        hostedView.translatesAutoresizingMaskIntoConstraints = false //是 Auto Layout 中的一个属性,用于控制视图的自动布局行为。当你手动使用 Auto Layout(比如添加约束)时,这个属性的设置至关重要。

        shieldView.addSubview(hostedView)

        NSLayoutConstraint.activate([
            hostedView.topAnchor.constraint(equalTo: shieldView.topAnchor),
            hostedView.bottomAnchor.constraint(equalTo: shieldView.bottomAnchor),
            hostedView.leadingAnchor.constraint(equalTo: shieldView.leadingAnchor),
            hostedView.trailingAnchor.constraint(equalTo: shieldView.trailingAnchor)
        ])

        return shieldView
    }

    func updateUIView(_ uiView: UIView, context: Context) {
        // 关键:在状态变化时更新 rootView 内容
        context.coordinator.hostingController?.rootView = content
    }
}

六、回顾

iOS无法禁止用户的截屏,但是可以通过一定的手段将修改截屏的内容。但是整个实现的过程也是在解决问题、引入新问题、再解决新问题的过程中不断的探索。在iOS系统特性的基础上,按业务需求进行取舍的过程。

Demo

相关推荐
fundroid3 小时前
Swift 进军 Android,Kotlin 该如何应对?
android·ios
吴Wu涛涛涛涛涛Tao5 小时前
Flutter 弹窗解析:从系统 Dialog 到完全自定义
flutter·ios
kymjs张涛8 小时前
零一开源|前沿技术周报 #7
android·前端·ios
思考着亮9 小时前
15-错误处理
ios
思考着亮10 小时前
9.方法
ios
思考着亮10 小时前
6.结构体和类
ios
思考着亮10 小时前
7.闭包
ios
咕噜签名分发冰淇淋12 小时前
申请注册苹果iOS企业级开发者证书需要公司拥有什么规模条件
macos·ios·cocoa
2501_915918411 天前
Fiddler中文版全面评测:功能亮点、使用场景与中文网资源整合指南
android·ios·小程序·https·uni-app·iphone·webview