Protect sensitive information in SwiftUI

The goal of this post is to present some techniques for obfuscating or preventing easy access to highly sensitive information, such as account or credit card numbers.

Allow copy

The first thing to clarify is which pieces of information can be copied and which cannot. This behavior is controlled by the .textSelection() modifier.

struct SensitiveCard: View {
    let title: String
    let primary: String
    let secondary: String
    let icon: String
    let isSelectable: Bool

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            HStack(spacing: 12) {
                Image(systemName: icon)
                    .font(.title2)
                    .padding(10)
                    .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 14))
                Text(title)
                    .font(.title3.weight(.semibold))
                Spacer()
            }
            Text(primary)
                .font(.system(.title2, design: .monospaced).weight(.semibold))
                .conditionalTextSelection(isSelectable)
            Text(secondary)
                .font(.subheadline)
                .foregroundStyle(.secondary)
        }
        .padding(18)
        .background(
            RoundedRectangle(cornerRadius: 22, style: .continuous)
                .fill(.regularMaterial)
                .shadow(radius: 12, y: 6)
        )
    }
}


struct ConditionalTextSelection: ViewModifier {
    let enable: Bool
    
    func body(content: Content) -> some View {
        if enable {
            content.textSelection(.enabled)
        } else {
            content
        }
    }
}

extension View {
    func conditionalTextSelection(_ enable: Bool) -> some View {
        self.modifier(ConditionalTextSelection(enable: enable))
    }
}

In this example, we’ve chosen to make the IBAN copyable, while the card number remains restricted.

struct ContentView: View {
    @State private var cardNumber = "1234 5678 9012 3456"
    @State private var cvv = "123"
    @State private var iban = "ES12 3456 7890 1234 5678 9012"
    @State private var textSelectability: TextSelectability = .disabled

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 24) {
                
                SensitiveCard(title: "IBAN",
                              primary: iban,
                              secondary: "Demo Bank",
                              icon: "building.columns.fill",
                              isSelectable: true)
                
                SensitiveCard(title: "Card",
                              primary: cardNumber,
                              secondary: "CVV \(cvv)",
                              icon: "creditcard.fill",
                              isSelectable: false)

                VStack(alignment: .leading, spacing: 8) {
                    Text("Safety measures:")
                        .font(.title3.bold())
                    Text("• Avoid sharing screenshots.\n• Enable Face ID for showing sensitive information.")
                        .foregroundStyle(.secondary)
                }
            }
            .padding(24)
        }
    }
}

Run the app and long-press the IBAN code. A contextual pop-up will appear — select “Copy.” Then switch to the Notes app and paste it. You’ll notice that the same operation cannot be performed with the card information.

Privacy button

A measure that can help build user trust is to provide a button that hides sensitive information:

import SwiftUI

struct ContentView: View {
    .....
    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 24) {
                ....
                Button {
                    // Ejemplo: regenerar/limpiar datos
                    cardNumber = "•••• •••• •••• ••••"
                    cvv = "•••"
                    iban = "ES•• •••• •••• •••• •••• ••••"
                } label: {
                    Label("Mask manually", systemImage: "eye.trianglebadge.exclamationmark.fill")
                }
                .buttonStyle(.borderedProminent)
                .controlSize(.large)
            }
            .padding(24)
        }
    }
}

When changes are deployed:

review

Detect app activity

An alternative to using a button, or a complementary feature, is detecting changes in the app’s activity — for example, when the app is moved to the background:

import SwiftUI
import UIKit

struct ContentView: View {
    @Environment(\.scenePhase) private var scenePhase
    @State private var privacyMask = false
...

    var body: some View {
        ProtectedView {
            SensitiveView(hidden: $showScreenshotAlert)
        }
        .blur(radius: privacyMask ? 28 : 0)
        .overlay {
            if privacyMask {
                ZStack {
                    Color.black.opacity(0.6).ignoresSafeArea()
                    VStack(spacing: 12) {
                        Image(systemName: "eye.slash.fill")
                            .font(.system(size: 36, weight: .semibold))
                        Text("Content hidden whilst app is not active")
                            .multilineTextAlignment(.center)
                            .font(.headline)
                    }
                    .foregroundColor(.white)
                    .padding()
                }
                .accessibilityHidden(true)
            }
        }
        .onChange(of: scenePhase) { phase in
            privacyMask = (phase != .active)
        }
        ...
}

Deploy and move app to background:

review2

Detect screenshot action

An important point I discovered during this investigation is that, although the app can detect when a screenshot is taken — which sounds good — the screenshot is still captured anyway.

My recommendation in that case would be to invalidate or regenerate the information if temporary keys are involved. Depending on the scenario, you could also notify the backend services that a screenshot has been taken.

The code for detecting a screenshot is as follows:

struct ContentView: View {
...
    @State private var showScreenshotAlert = false

    var body: some View {
        ProtectedView {
            SensitiveView(hidden: $showScreenshotAlert)
        }
       ...
        .onReceive(NotificationCenter.default.publisher(
            for: UIApplication.userDidTakeScreenshotNotification)) { _ in
                showScreenshotAlert = true
            }
        .alert("Screenshot detected",
               isPresented: $showScreenshotAlert) {
            Button("OK", role: .cancel) {}
        } message: {
            Text("For security, sensitive content is hidden when the screen is being captured.")
        }
    }
}

Ofuscate on Recording screen

The final measure is to apply obfuscation when a screen recording is in progress:

struct ProtectedView<Content: View>: View {
    @State private var isCaptured = UIScreen.main.isCaptured
    @ViewBuilder var content: () -> Content

    var body: some View {
        content()
            .blur(radius: isCaptured ? 25 : 0)
            .overlay {
                if isCaptured {
                    ZStack {
                        Color.black.opacity(0.65).ignoresSafeArea()
                        VStack(spacing: 12) {
                            Image(systemName: "lock.fill")
                                .font(.system(size: 32, weight: .bold))
                            Text(String(localized: "protected.overlay.title",
                                        defaultValue: "Content hidden while the screen is being captured"))
                                .multilineTextAlignment(.center)
                                .font(.headline)
                                .padding(.horizontal)
                        }
                        .foregroundColor(.white)
                    }
                    .accessibilityHidden(true)
                }
            }
            .onReceive(NotificationCenter.default.publisher(
                for: UIScreen.capturedDidChangeNotification)) { _ in
                    isCaptured = UIScreen.main.isCaptured
                }
    }
}

When a screen recording session is active and the user switches to our privacy app, the app detects the recording and can respond by displaying an overlay.

recording

Conclusions

It’s not possible to achieve complete protection against data forgery or privacy breaches, but the more countermeasures you apply, the better your security becomes. That’s what I wanted to demonstrate in this post.

You can find the source code for this example in the following GitHub repository.

References

Copyright © 2024-2025 JaviOS. All rights reserved