Concealing your Views from Screenshots & Screen-Recordings (While still keeping them visible on screen?)
Introduction
A huge thank you goes to Jevin Sweval for bringing this API to my attention.
If you have ever tried to screenshot or screen record a secure textfield on iOS (typically, textfields used for entering password) you will have noticed that the password is hidden in the screenshot, despite appearing to you on your device’s screen. You might wonder, how? There is no API exposed for this?
…It all lies within a single, undocumented, private property of CALayer
.
Every single UIView on iOS is backed by a CALayer
, used for rendering content, animations, and geometric transformations. In fact, many popular methods of UIView interact with the layer such as frame
, backgroundColor
, etc.
The key to hiding our View from any form of Screen Capturing (Screenshots, Screen-Recordings) is to hide the View’s layer itself from Screen Capturing.
How?
As a famous Chinese man once said:
If there’s a will, there is most probably a private API to the way. (~Sun Tzu, The Art of War)
And that is exactly the case here. CALayer
has a private property to conceal the layer from Screen Capturing, disableUpdateMask
, as an int
which CoreAnimation treats as a bitmask, meaning we can add and remove flags as we like. The ones we want, in particular, are 1 << 1
and 1 << 4
(meaning we have to set the property of disableUpdateMask
to (1 << 1) | (1 << 4)
).
Okay, enough talk now. Let’s write an implementation of this. We will be writing our code to be usable on an App Store app:
The usage for the extension above would be yourUIView.hideViewFromCapture(hide: true)
.
Lets run through what happens in the code one by one: first, we instantiate a variable called propertyBase64
, a Base64 encoded String of the property we want to modify. Since App Store review runs down a static analysis of the application to make sure that we’re not using private APIs (but we want to, anyways), they can’t detect use of the API done this way. Then, we decode the Base64 string to get the name of the property as a normal String.
After that, we hit an extremely important (essential for any app in production) important check: layer.responds(to: NSSelectorFromString(propertyString))
, the reason this check is done is because private APIs are private for a couple of reasons, one of the more important of which is that they could be removed at a future update, therefore, we check for if CALayer
contains the property, otherwise, if we were to try to set a non-existing property, the app would crash. The Objective-C Compiler automatically generates 2 selectors per property: one to get the property (usually just the property name), and one to set the property if the property is settable (usually called setPropertyName:
). We just need to check for the presence of the property as a whole, meaning there is no need to check for the setter separately, just a check for the getter would be enough (which in this case is just the property name).
After that, we check the hide
parameter passed in, if it’s true, then we do want to hide the view from Screen Capture, and we set the property to the aforementioned (1 << 1) | (1 << 4)
flag (When setting the property with setValue
, we need to wrap the number around an NSNumber
due to how Objective-C works). If hide
is false, then we can just set the property to 0
, removing any flags.
Example Project
Here is an example project consisting of a toggle and a UIImageView, when the toggle is on, the image view is hidden from Screen Capture, and still remains visible on screen. The code for this project is available on GitHub
Screenshot taken while toggle is off | Screenshot taken while toggle is on | Video Demonstration |
---|---|---|
What about SwiftUI?
Great question! SwiftUI, unlike UIKit, has no way to directly provide the layer (if any) which it is manipulating. I heavily encourage you to try get the UIView of the SwiftUI View you are trying to apply this on through swiftui-introspect, then you could try to use the UIView extension above. Otherwise, this wouldn’t work.