avatar
Published on

Preventing Screenshot Capture in SwiftUI Views

Authors
  • avatar
    Name
    Mick MacCallum
    Twitter
    @0x7fs

If you're building apps that handle sensitive information like banking details, medical records, or private communications, you may need to prevent users from capturing screenshots of certain screens. While iOS doesn't provide a built-in way to completely block screenshots, you can use the secure text entry mechanism and field redaction to hide sensitive content when screenshots are taken.

SwiftUI doesn't expose direct screenshot prevention APIs, but we can bridge to UIKit's secure field functionality to protect sensitive views. This approach is commonly used in banking apps, password managers, and healthcare applications.

The Challenge

Unlike Android, iOS doesn't allow apps to completely prevent screenshots. However, iOS does provide a mechanism originally designed for password fields that makes content appear blank in screenshots, screen recordings, and the app switcher. This same mechanism can be applied to any view containing sensitive information.

The key is using UITextField's isSecureTextEntry property, but applying it at the view level rather than just for text input.

Solution: Secure Field View Wrapper

We'll create a SwiftUI view that wraps your sensitive content and uses UIKit's secure text entry mechanism to hide it from screenshots:

import SwiftUI
import UIKit

struct SecureView<Content: View>: UIViewRepresentable {
    let content: Content

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

    func makeUIView(context: Context) -> SecureUIView {
        let secureView = SecureUIView()

        // Add SwiftUI content as a child
        let hostingController = UIHostingController(rootView: content)
        hostingController.view.backgroundColor = .clear
        hostingController.view.translatesAutoresizingMaskIntoConstraints = false

        secureView.addSubview(hostingController.view)

        NSLayoutConstraint.activate([
            hostingController.view.leadingAnchor.constraint(equalTo: secureView.leadingAnchor),
            hostingController.view.trailingAnchor.constraint(equalTo: secureView.trailingAnchor),
            hostingController.view.topAnchor.constraint(equalTo: secureView.topAnchor),
            hostingController.view.bottomAnchor.constraint(equalTo: secureView.bottomAnchor)
        ])

        return secureView
    }

    func updateUIView(_ uiView: SecureUIView, context: Context) {
        // No update needed
    }
}

class SecureUIView: UIView {
    private let secureTextField = UITextField()

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupSecureField()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupSecureField()
    }

    private func setupSecureField() {
        // Create a secure text field to trigger screenshot protection
        secureTextField.isSecureTextEntry = true

        // Make it invisible but part of the view hierarchy
        secureTextField.isUserInteractionEnabled = false
        secureTextField.alpha = 0
        secureTextField.translatesAutoresizingMaskIntoConstraints = false

        addSubview(secureTextField)

        // Pin it to a corner (it won't be visible)
        NSLayoutConstraint.activate([
            secureTextField.leadingAnchor.constraint(equalTo: leadingAnchor),
            secureTextField.topAnchor.constraint(equalTo: topAnchor),
            secureTextField.widthAnchor.constraint(equalToConstant: 1),
            secureTextField.heightAnchor.constraint(equalToConstant: 1)
        ])

        // Bring content to front
        sendSubviewToBack(secureTextField)
    }
}

How It Works

The solution relies on a clever iOS behavior: when a view hierarchy contains a secure text field (isSecureTextEntry = true), the entire view subtree appears blank in screenshots and screen recordings.

Here's what's happening:

  1. SecureUIView: Creates an invisible UITextField with isSecureTextEntry = true
  2. View Hierarchy: The secure text field is added to the view but made completely transparent and non-interactive
  3. Screenshot Protection: iOS detects the secure field and blanks out the entire view in screenshots
  4. SwiftUI Integration: UIViewRepresentable bridges this UIKit solution into SwiftUI

Using the Secure View

Wrap any sensitive SwiftUI content in the SecureView:

struct BankingDetailsView: View {
    @State private var accountNumber = "1234567890"
    @State private var balance = "$10,453.21"

    var body: some View {
        VStack(spacing: 20) {
            // Regular content - can be screenshot
            Text("Account Summary")
                .font(.title)

            // Sensitive content - protected from screenshots
            SecureView {
                VStack(spacing: 12) {
                    HStack {
                        Text("Account Number:")
                        Spacer()
                        Text(accountNumber)
                            .fontWeight(.bold)
                    }

                    HStack {
                        Text("Available Balance:")
                        Spacer()
                        Text(balance)
                            .font(.title2)
                            .foregroundColor(.green)
                    }
                }
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(12)
            }

            Spacer()
        }
        .padding()
    }
}

Protecting the Entire Screen

For maximum security, you can wrap your entire screen:

struct SecureScreenView: View {
    var body: some View {
        SecureView {
            VStack {
                Text("Confidential Information")
                    .font(.title)

                // All content here is protected
                List {
                    Section("Patient Records") {
                        Text("Medical History: ...")
                        Text("Diagnosis: ...")
                        Text("Prescription: ...")
                    }
                }
            }
        }
    }
}

Also Protects App Switcher

A bonus benefit: this approach also hides content in the iOS app switcher (when users double-tap the home button or swipe up). This prevents sensitive information from being visible when switching between apps.

Important Considerations

Not Foolproof: Users can still photograph the screen with another device. This technique prevents digital screenshots but not physical photos.

User Experience: Consider showing a placeholder message like "Screenshot not allowed for security reasons" when content is hidden.

Accessibility: Ensure VoiceOver and other accessibility features still work properly with wrapped content.

Performance: The UIKit bridge adds minimal overhead, but avoid wrapping unnecessarily large view hierarchies.

Alternative: Privacy Sensitive Modifier (iOS 15+)

For less critical content, consider the built-in .privacySensitive() modifier:

Text("Sensitive data")
    .privacySensitive()

However, this only affects screenshots in some contexts and isn't as comprehensive as the secure field approach.

When to Use This Technique

Use screenshot prevention for:

  • Banking and financial information
  • Medical records and health data
  • Password managers and authentication screens
  • Private messaging content
  • Personal identification numbers
  • Credit card details

This approach gives you fine-grained control over what content is protected, allowing you to balance security with user experience.