Skip to content

Instantly share code, notes, and snippets.

@omidgolparvar
Last active October 10, 2025 14:20
Show Gist options
  • Select an option

  • Save omidgolparvar/879bbb177c31b01fadf435c34ee88dbc to your computer and use it in GitHub Desktop.

Select an option

Save omidgolparvar/879bbb177c31b01fadf435c34ee88dbc to your computer and use it in GitHub Desktop.
A view protects content from screenshots and screen recordings.
/// This file is based on the original source code from:
/// https://github.com/kuttz/SecureYourView
/// Some modifications have been made to adapt it for our use case.
import Foundation
import UIKit
import Combine
/// This custom view is designed to protect its content against screenshots
/// and screen recordings, ensuring that sensitive information cannot be
/// captured by the user or external apps.
public final class SecureView: UIView {
/// Holds the observation object for screen capture notifications using Combine.
private var screenCaptureNotificationObservation: AnyCancellable?
/// A hidden `UITextField` used to leverage iOS secure text entry behavior.
private var secureTextField = UITextField()
/// Container view for the placeholder content, shown when `isSecure` is `true`.
private let placeholderContainerView = UIView()
/// Main container view for displaying the actual content view.
private var containerView = UIView()
/// Indicates whether the view is in secure mode.
/// When enabled, the placeholder is shown instead of the actual content.
private var isSecure: Bool {
get {
secureTextField.isSecureTextEntry
}
set {
secureTextField.isSecureTextEntry = newValue
placeholderContainerView.isHidden = !newValue
}
}
/// Enables or disables automatic handling of screen capture protection.
/// When set to `true`, the view will listen for system notifications and
/// automatically toggle visibility of its content based on capture state.
public var isScreenCaptureProtectionPresentationEnabled: Bool = true {
didSet {
let newValue = isScreenCaptureProtectionPresentationEnabled
setScreenCaptureNotificationObservationEnabled(newValue)
setupViewsBasedOnScreenCaptureState(screen: .main)
}
}
/// Initializes the view with a given frame.
public override init(frame: CGRect) {
super.init(frame: frame)
setupView()
setScreenCaptureNotificationObservationEnabled(isScreenCaptureProtectionPresentationEnabled)
setupViewsBasedOnScreenCaptureState(screen: .main)
}
/// Convenience initializer for setting up the main content view and optional placeholder.
public convenience init(contentView: UIView, placeholderView: UIView? = nil) {
self.init(frame: .zero)
setContentView(contentView)
if let placeholderView {
setPlaceholderView(placeholderView)
}
}
/// Required initializer (not implemented since this view is not expected to be loaded from Interface Builder).
public required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// Configures the internal view hierarchy.
/// Uses a hidden `UITextField` to extract its secure container, then sets up layout with SnapKit.
private func setupView() {
if let view = secureTextField.subviews.first {
containerView = view
containerView.removeFromSuperview()
}
isSecure = true
addSubview(placeholderContainerView)
placeholderContainerView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
addSubview(containerView)
containerView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
/// Replaces the content of the main container with a given view.
public func setContentView(_ contentView: UIView) {
replaceFirstSubview(in: containerView, with: contentView)
}
/// Replaces the content of the placeholder container with a given view.
public func setPlaceholderView(_ placeholderView: UIView) {
replaceFirstSubview(in: placeholderContainerView, with: placeholderView)
}
/// Utility method that replaces the first subview of a container with the given replacement view.
private func replaceFirstSubview(in containerView: UIView, with replacementSubview: UIView) {
// Ensure the replacement is not already added elsewhere.
replacementSubview.removeFromSuperview()
// Remove the existing subviews if present.
containerView.subviews.forEach { subview in
subview.removeFromSuperview()
}
// Add and constrain the replacement view to fill the container.
containerView.addSubview(replacementSubview)
replacementSubview.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
/// Enables or disables the observation of screen capture notifications.
/// When enabled, the view listens for system changes and updates its visibility accordingly.
private func setScreenCaptureNotificationObservationEnabled(_ enabled: Bool) {
if enabled {
setScreenCaptureNotificationObservationEnabled(false)
screenCaptureNotificationObservation = NotificationCenter
.default
.publisher(for: UIScreen.capturedDidChangeNotification)
.sink { [weak self] notification in
guard
let self,
let screen = notification.object as? UIScreen
else { return }
setupViewsBasedOnScreenCaptureState(screen: screen)
}
} else {
screenCaptureNotificationObservation?.cancel()
screenCaptureNotificationObservation = nil
}
}
/// Updates the view’s visibility based on the screen capture state.
/// Uses appropriate implementation depending on the iOS version.
private func setupViewsBasedOnScreenCaptureState(screen: UIScreen) {
if #available(iOS 17.0, *) {
setupViewsBasedOnScreenCaptureStateOniOS17()
} else {
setupViewsBasedOnScreenCaptureStatePreiOS17(screen: screen)
}
}
/// Updates visibility of content/placeholder for iOS 17 and later.
@available(iOS 17.0, *)
private func setupViewsBasedOnScreenCaptureStateOniOS17() {
let isContentHidden = switch traitCollection.sceneCaptureState {
case .unspecified, .inactive:
false
case .active:
true
@unknown default:
false
}
setContainersVisibility(isContentHidden: isContentHidden)
}
/// Updates visibility of content/placeholder for iOS versions 11.0 to 16.x,
/// using `UIScreen.isCaptured` property.
@available(iOS, introduced: 11.0, obsoleted: 17.0)
private func setupViewsBasedOnScreenCaptureStatePreiOS17(screen: UIScreen) {
let isContentHidden = screen.isCaptured
setContainersVisibility(isContentHidden: isContentHidden)
}
/// Updates the visibility of the main and placeholder containers based on the content state.
/// - Parameter isContentHidden: A Boolean value indicating whether the main content should be hidden.
/// When `true`, the main container is hidden and the placeholder is shown. When `false`, the opposite occurs.
private func setContainersVisibility(isContentHidden: Bool) {
containerView.isHidden = isContentHidden
placeholderContainerView.isHidden = !isContentHidden
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment