Skip to content

Instantly share code, notes, and snippets.

@tornikegomareli
Created February 3, 2025 11:45
Show Gist options
  • Save tornikegomareli/a0536cf98f7826a1041db848a8570ee0 to your computer and use it in GitHub Desktop.
Save tornikegomareli/a0536cf98f7826a1041db848a8570ee0 to your computer and use it in GitHub Desktop.
//
// ProtectionOptions.swift
// OneApp
//
// Created by Tornike Gomareli on 31.01.25.
//
/// A set of options that determine how content should be protected from being captured.
///
/// This type allows for combining multiple protection strategies:
/// ```swift
/// // Basic screenshot protection
/// myView.protected(from: .screenshots)
///
/// // Combined protection options (future use)
/// myView.protected(from: [.screenshots, .otherOption])
///
/// // All protection
/// myView.protected(from: .all)
/// ```
public struct ProtectionOptions: OptionSet {
public let rawValue: Int
/// Prevents the view from appearing in screenshots
public static let screenshots = ProtectionOptions(rawValue: 0x01)
/// Hide the view from screen sharing (e.g. AirPlay).
public static let screenSharing = ProtectionOptions(rawValue: 0x02)
/// Hide the view while the app is not active (e.g. while task switching).
public static let taskSwitching_inactive = ProtectionOptions(rawValue: 0x04)
/// Hide the view from screenshots, screen sharing and during inactivity (task switching).
public static let all = ProtectionOptions([.screenshots, .screenSharing, .taskSwitching_inactive])
public init(rawValue: Int) {
self.rawValue = rawValue
}
}
//
// ScreenshotProtectionPlaceholder.swift
// OneApp
//
// Created by Tornike Gomareli on 31.01.25.
//
import SwiftUI
/// A type that defines different placeholder views to be shown during screenshot protection.
///
/// Use this enum to specify how the protected content should be masked when a screenshot is attempted:
/// ```swift
/// // Basic protection with no placeholder
/// myView.protected(placeholder: .none)
///
/// // Protection with black screen
/// myView.protected(placeholder: .blackScreen)
///
/// // Protection with custom content
/// myView.protected(placeholder: .custom(content: AnyView(
/// Text("Screenshots not allowed")
/// )))
/// ```
public enum ScreenshotProtectionPlaceholder {
/// No placeholder view will be shown
case none
/// A black screen will cover the protected content
case blackScreen
/// A custom view will be shown instead of the protected content
case custom(content: AnyView)
/// The view to be displayed based on the selected placeholder type
@ViewBuilder
var view: some View {
switch self {
case .none:
EmptyView()
case .blackScreen:
ZStack {
Color.black
.ignoresSafeArea()
}
case .custom(let content):
content
}
}
}
import UIKit
import SwiftUI
/// A `UIViewController` subclass that hosts a SwiftUI view while preventing screenshots and screen recordings.
///
/// This controller wraps a `SwiftUI.View` inside a `UIHostingController` and embeds it within a `ScreenshotProtectingView`,
/// which adds protection against screen capture.
///
/// - Note: The protection mechanism relies on `ScreenshotProtectingView`, which should handle screenshot-blocking logic.
final class ScreenshotProtectingHostingViewController<Content: View>: UIViewController {
/// The SwiftUI content view to be hosted within this view controller.
private let content: () -> Content
/// A wrapper view that provides screenshot protection.
private let wrapperView = ScreenshotProtectingView()
/// Creates a `ScreenshotProtectingHostingViewController` that hosts a SwiftUI view with screenshot protection.
///
/// - Parameter content: A closure returning the SwiftUI view to be displayed.
init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
super.init(nibName: nil, bundle: nil)
setupUI()
}
/// This initializer is not implemented and will trigger a runtime error if called.
///
/// - Parameter coder: The coder instance.
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// Called when the view is about to appear. Ensures the navigation bar is transparent.
///
/// - Parameter animated: A Boolean value indicating whether the appearance transition is animated.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
configureNavigationBarTransparency()
}
/// Configures the navigation bar to have a transparent background.
///
/// This method updates the navigation bar appearance to remove any background color and shadows,
/// ensuring a clean and modern look.
private func configureNavigationBarTransparency() {
guard let navBar = navigationController?.navigationBar else { return }
let appearance = UINavigationBarAppearance()
appearance.configureWithTransparentBackground()
appearance.shadowColor = .clear
navBar.standardAppearance = appearance
navBar.compactAppearance = appearance
navBar.scrollEdgeAppearance = appearance
navBar.isTranslucent = true
}
/// Sets up the UI, embedding the SwiftUI content inside a `ScreenshotProtectingView`.
///
/// This method:
/// - Adds `wrapperView` as a subview to the main view.
/// - Embeds the SwiftUI view inside a `UIHostingController`.
/// - Adds constraints to ensure proper layout.
private func setupUI() {
view.addSubview(wrapperView)
wrapperView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
wrapperView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
wrapperView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
wrapperView.topAnchor.constraint(equalTo: view.topAnchor),
wrapperView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
let hostVC = UIHostingController(rootView: content())
hostVC.view.translatesAutoresizingMaskIntoConstraints = false
addChild(hostVC)
wrapperView.setup(contentView: hostVC.view)
hostVC.didMove(toParent: self)
}
}
import UIKit
/// A `UIView` subclass that prevents screenshots and screen recordings.
///
/// `ScreenshotProtectingView` uses a hidden `UITextField` with `isSecureTextEntry = true` to leverage iOS’s
/// built-in screenshot protection. This view acts as a secure container, ensuring its contents cannot be captured.
///
/// - Note: This approach relies on iOS internals and may be subject to changes in future updates.
public final class ScreenshotProtectingView: UIView {
/// The content view that will be protected from screenshots.
private var contentView: UIView?
/// A hidden `UITextField` used to enable secure content protection.
private let textField = UITextField()
/// The secure container extracted from the `UITextField`, which prevents screenshots.
private lazy var secureContainer: UIView? = try? getSecureContainer(from: textField)
/// Initializes a `ScreenshotProtectingView` with an optional content view.
///
/// - Parameter contentView: The view that should be protected from screenshots.
public init(contentView: UIView? = nil) {
self.contentView = contentView
super.init(frame: .zero)
setupUI()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// Sets up the UI components and enables screenshot protection.
///
/// This method:
/// - Configures the hidden `UITextField` to enable protection.
/// - Extracts the secure container from the `UITextField`.
/// - Embeds the content view inside the secure container.
private func setupUI() {
textField.backgroundColor = .clear
textField.isUserInteractionEnabled = false
textField.isSecureTextEntry = true
guard let container = secureContainer else { return }
addSubview(container)
container.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
container.leadingAnchor.constraint(equalTo: leadingAnchor),
container.trailingAnchor.constraint(equalTo: trailingAnchor),
container.topAnchor.constraint(equalTo: topAnchor),
container.bottomAnchor.constraint(equalTo: bottomAnchor)
])
guard let contentView = contentView else { return }
setup(contentView: contentView)
}
/// Embeds a new content view inside the secure container.
///
/// - Parameter contentView: The view to be protected from screenshots.
public func setup(contentView: UIView) {
self.contentView?.removeFromSuperview()
self.contentView = contentView
guard let container = secureContainer else { return }
container.addSubview(contentView)
container.isUserInteractionEnabled = isUserInteractionEnabled
contentView.translatesAutoresizingMaskIntoConstraints = false
let bottomConstraint = contentView.bottomAnchor.constraint(equalTo: container.bottomAnchor)
bottomConstraint.priority = .required - 1
NSLayoutConstraint.activate([
contentView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
contentView.topAnchor.constraint(equalTo: container.topAnchor),
bottomConstraint
])
}
/// Retrieves the secure container from a `UITextField`, which blocks screenshots.
///
/// - Parameter view: A `UITextField` used to extract the secure container.
/// - Throws: An error if the secure container is not found.
/// - Returns: The secure container view that prevents screenshots.
func getSecureContainer(from view: UIView) throws -> UIView {
let containerName: String = "_UITextLayoutCanvasView"
let containers = view.subviews.filter { type(of: $0).description() == containerName }
guard let container = containers.first else {
throw NSError(domain: "YourDomain", code: -1, userInfo: ["ContainerNotFound": containerName])
}
return container
}
}
import class Combine.AnyCancellable
import SwiftUI
/// A UIViewRepresentable wrapper that provides screenshot protection for SwiftUI views.
///
/// This view uses private iOS APIs to prevent content from appearing in screenshots
/// by leveraging the secure text entry system.
struct ProtectedView<Content: View>: UIViewControllerRepresentable {
typealias UIViewControllerType = ScreenshotProtectingHostingViewController<Content>
let options: ProtectionOptions
@State private var hostingController: ScreenshotProtectingHostingViewController<Content>
private let content: () -> Content
private var cancellables = Set<AnyCancellable>()
/// Creates a new protected view with the specified options and content
/// - Parameters:
/// - options: The protection options to apply
/// - content: A closure that provides the content to be protected
init(options: ProtectionOptions = .screenshots, content: @escaping () -> Content) {
self.options = options
self.content = content
self.hostingController = ScreenshotProtectingHostingViewController(content: content)
func subscribe(to notification: Notification.Name, shouldHide: @escaping (Notification) -> Bool) {
NotificationCenter.default.publisher(for: notification)
.sink { [weak hostingController] notification in
hostingController?.view.isHidden = shouldHide(notification)
}
.store(in: &cancellables)
}
if options.contains(.taskSwitching_inactive) {
subscribe(to: UIApplication.willResignActiveNotification) { _ in true }
subscribe(to: UIApplication.didBecomeActiveNotification) { _ in false }
}
if options.contains(.screenSharing) {
subscribe(to: UIScreen.capturedDidChangeNotification) {
($0.object as? UIScreen)?.isCaptured ?? false
}
}
}
func makeUIViewController(context: Context) -> UIViewControllerType {
ScreenshotProtectingHostingViewController(content: content)
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
}
import SwiftUI
public extension View {
/// Applies screenshot protection to a view with optional placeholder content.
///
/// Use this modifier to prevent your view's content from appearing in screenshots:
/// ```swift
/// Text("Sensitive Information")
/// .protected(from: .screenshots, placeholder: .blackScreen)
/// ```
///
/// - Parameters:
/// - options: The protection options to apply. Defaults to `.screenshots`
/// - placeholder: The content to show when protection is active. Defaults to `.none`
/// - Returns: A view that is protected according to the specified options
func protected(
from options: ProtectionOptions = .screenshots,
placeholder: ScreenshotProtectionPlaceholder = .blackScreen
) -> some View {
ProtectedView(options: options) {
self
}
.ignoresSafeArea()
.background(placeholder.view)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment