Last active
October 10, 2025 14:20
-
-
Save omidgolparvar/879bbb177c31b01fadf435c34ee88dbc to your computer and use it in GitHub Desktop.
A view protects content from screenshots and screen recordings.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /// 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