markbattistella/maskingtape
`MaskingTape` is a Swift package for capture protection and capture-aware watermarking in SwiftUI.
Installation
Add MaskingTape to your Swift project using Swift Package Manager:
dependencies: [
.package(url: "https://github.com/markbattistella/MaskingTape", from: "1.0.0")
]Alternatively, add it using Xcode via File > Add Packages and entering the package repository URL.
How Capture Protection Works (iOS)
MaskingTape uses the UITextField.isSecureTextEntry rendering side-effect. Content hosted inside the private secure container remains visible in the live app, but iOS omits it from screenshots, screen recordings, and mirroring capture pipelines.
When you provide a replacement overlay with maskingTape { ... }, that replacement is placed behind the secure content so it becomes visible in captured output, like tape appearing on a photocopy.
Quick Start
import MaskingTapeHide sensitive content in captures
Text("4111 1111 1111 1111")
.maskingTape()Hide content and show replacement UI in captures
CardView()
.maskingTape {
VStack(spacing: 8) {
Image(systemName: "lock.fill")
Text("Protected")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.background)
}Capture-reactive watermark (recording / mirroring)
DocumentView()
.watermark {
Text("CONFIDENTIAL")
.font(.title.bold())
.foregroundStyle(.red.opacity(0.22))
.rotationEffect(.degrees(-30))
}Always-visible watermark (use SwiftUI directly)
DocumentView()
.overlay {
Text("CONFIDENTIAL")
.font(.title.bold())
.foregroundStyle(.red.opacity(0.22))
.rotationEffect(.degrees(-30))
}Public API (Simplified)
Capture Protection
.maskingTape().maskingTape { replacement }MaskingTapeView { ... }
Capture-Reactive Watermark
.watermark { overlay }WatermarkView { ... }
watermark is intentionally capture-aware only. If you want a watermark that is always visible, use SwiftUI's built-in .overlay.
Full-Screen Usage (Scrolling Screens)
To keep protection/watermarking tied to the visible viewport while content scrolls, apply the modifier to the outer container (NavigationStack, TabView, etc.), not the inner ScrollView content:
NavigationStack {
ScrollView {
// content
}
}
.maskingTape {
Color(uiColor: .systemBackground)
}NavigationStack {
ScrollView {
// content
}
}
.watermark {
Text("MASKINGTAPE")
}Platform Notes
iOS:maskingTapeuses the secure text-field container technique;watermarkreacts toUIScreen.isCapturedmacOS:maskingTapeusesNSWindow.sharingType = .none(window-wide); capture state for reactive watermarking is not publicly available without extra permissionstvOS: secure masking is not applied; capture-reactive watermarking is availablewatchOS: watermark APIs fall back to always-hidden-unless-explicit behavior (no capture-state concept)visionOS: secure masking behavior is unverified and currently treated conservatively
Important Limitations
- iOS does not provide a public "will screenshot" callback
UIApplication.userDidTakeScreenshotNotificationfires after the screenshot is already captured- You cannot insert a watermark into a system screenshot after the capture has occurred
- The iOS secure masking technique depends on UIKit internals and may break if Apple changes the private view hierarchy
Example App
The included example demonstrates:
maskingTape()on individual viewsmaskingTape { replacement }with custom capture replacement content- Capture-reactive
.watermark { ... } - Full-screen masking and full-screen watermarking on scrolling screens
- A bottom inset watermark pattern for active recording / mirroring sessions
Contributing
Contributions are welcome. Please open an Issue or PR for fixes, feature proposals, or documentation improvements.
PR titles should follow the format: YYYY-mm-dd - Title
Licence
MaskingTape is released under the MIT licence.
Package Metadata
Repository: markbattistella/maskingtape
Default branch: main
README: README.md