一、背景
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.view
为ScreenShieldView
;
2、然后布局在self.view
上的子视图都会加到安全图层上,整个页面就具有了防截屏功能;
3、当用户截屏时,截屏出来的将是一个空白页面,这样就起到了防截屏,防录屏的作用;
五、问题分析
真正使用过程中会遇到如下问题:
问题一:self.view.subviews数组存放的子视图不是真正添加到self.view上的子视图?,就是说无法通过self.view.subviews获取子视图数量了
查看ScreenShieldView
的原码可以发现,虽然我们在loadView中将self.view
设置成了ScreenShieldView
,并且是通过self.view添加的子视图,但其实所有子视图都添加到了ScreenShieldView
的safeZone
上了。
分析问题
想重写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系统特性的基础上,按业务需求进行取舍的过程。