
Для мобильных приложений, отображающих критичные данные хорошей практикой будет запретить делать снимки и запись экрана в областях отображающих конфиденциальную информацию. Иначе злоумышленники могут получить доступ к паролям или карточным данным при помощи вредоносного ПО, которое, делает записи экрана или скриншоты.
В этой статье мы рассмотрим способы защиты мобильных приложений от снятия критичной информации. Хотя в интернете можно найти материалы на эту тему, многие из них либо недостаточно подробны, либо устарели.
Материал будет полезен разработчикам, которые планируют внедрить защиту приложения от снятия критичных данных.
Механизмы защиты в Android
Для начала давайте рассмотрим какие меры противодействия снятию информации есть в Android.
FLAG_SECURE
В Android защита реализуется с помощью флага окна FLAG_SECURE
. Установив его для конкретной Activity, можно запретить создание снимков экрана, а также скрыть содержимое приложения в списке недавно запущенных приложений. Пример реализации:
... getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); ...
После этого система автоматически блокирует скриншоты и запись экрана внутри приложения.
Рассмотрим работу этого механизма защиты на примере простого приложения с формой для ввода критичных данных. Без установки атрибута FLAG_SECURE запись экрана выполняется без каких-либо проблем:

Но, при попытке записи экрана в приложении с реализованным механизмом защиты, мы увидим следующее:

На записи видно, что вместо обычного экрана приложения отображается черный экран. При этом пользователь может взаимодействовать с приложением в обычном режиме.

android:inputType=”textPassword”
Стоит отметить, что критичные данные во время ввода могут “подсмотреть” не только автоматизированные средства для записи экрана, но и любопытные люди. Поэтому стоит добавить защиту данных при вводе.
Например, для полей, в которых вводятся платежные данные (CVC2/CVV2), можно использовать атрибут android:inputType="textPassword"
для того, чтобы скрыть введенные символы.
<EditText
android:id="@+id/passwordField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="CVV"
android:inputType="textPassword" />
RenderEffect
Экран могут снять не только в режиме использования, но и в фоновом режиме, поэтому рекомендуется скрывать критичные данные при отображении приложения в списке "недавние" или при переходе в фоновый режим. Для этого можно использовать размытие (blur) содержимого с помощью RenderEffect, однако данный API доступен только с Android версии 12.
Для Android более старших версий можно использовать RenderScript.
private void applyBlurEffect() {
RenderEffect blurEffect = RenderEffect.createBlurEffect(40f, 40f, Shader.TileMode.CLAMP);
rootView.setRenderEffect(blurEffect);
}
40f, 40f - это радиус размытия по горизонтали (X) и вертикали (Y). Значение определяет насколько сильно будет размыто изображение: чем больше значение, тем шире размываются пиксели по горизонтали и вертикали соответственно.
Вот так выглядит размытие страницы приложения в списке недавних приложений.

Механизмы защиты в iOS
В отличие от Android, в iOS нет готового решения, чтобы запретить пользователям делать снимки экрана или осуществлять его запись. Однако Apple позволяет отслеживать факт записи экрана и реагировать на это событие.
UIScreen.main.isCaptured
Для этого можно использовать свойство UIScreen.main.isCaptured
, которое возвращает true
, если экран в данный момент записывается или транслируется, и false
в противном случае. Например:
if UIScreen.main.isCaptured {
print("Экран записывается!")
}
Кроме того, можно подписаться на UIScreen.capturedDidChangeNotification, чтобы отслеживать изменение статуса захвата экрана в режиме реального времени:
NotificationCenter.default.addObserver(forName: UIScreen.capturedDidChangeNotification,
object: nil,
queue: .main) { _ in
if UIScreen.main.isCaptured {
print("Началась запись экрана!")
} else {
print("Запись экрана остановлена.")
}
}
Однако, согласно документации Apple isCaptured находится в статусе deprecated и его поддержка прекращается, начиная с iOS 18.4. Поэтому вместо isCaptured следует использовать sceneCaptureState, доступную с iOS 17.0.
Кроме того, в iOS можно оповещать пользователя, если был сделан снимок экрана. Это можно сделать с помощью UIApplication.userDidTakeScreenshotNotification:
NotificationCenter.default.addObserver(forName: UIApplication.userDidTakeScreenshotNotification,
object: nil,
queue: .main) { _ in
print("Пользователь сделал снимок экрана!")
}
Учитывайте, что такие методы лишь сообщают о событии после его совершения, а не предотвращают утечку данных.
Получается, что “коробочных” средств для предотвращения скриншотов в iOS нет. Но мы не отчаивались и продолжили поиски.
Скрытие контента от захвата экрана с помощью isSecureTextEntry
Мы нашли интересную статью на эту тему в блоге Кирилла Сидорова. В ней он описывал, как искал решение данной проблемы, изучая работу Telegram. Ссылка на оригинальную статью: https://sidorov.tech/all/lovim-skrinshoty/.
Основной механизм защиты, описанный в статье, основан на системной функции UITextField.isSecureTextEntry
, которая изначально предназначена для скрытия контента (например, паролей) при вводе. При активации этого флага, iOS автоматически предотвращает попадание реального текста в системные буферы рендеринга, что защищает данные от скриншотов и записи экрана.
В примере ниже создается специальный слой, который временно заменяет слой View, и с помощью флага isSecureTextEntry
заставляет UITextField перерисовать его содержимое, например, скрыв текст.
public extension CALayer {
func makeHiddenOnCapture() {
// Поиск специального системного View для защиты
let captureSecuredView: UIView? = captureSecuredView
?? uiKitTextField.subviews.first(where: { NSStringFromClass(type(of: $0)).contains("LayoutCanvasView") })
// Сохраняем оригинальный слой и подменяем его текущим слоем
let originalLayer = captureSecuredView?.layer
captureSecuredView?.setValue(self, forKey: "layer")
// Активируем и деактивируем secureTextEntry для триггера защиты
uiKitTextField.isSecureTextEntry = false
uiKitTextField.isSecureTextEntry = true
// Возвращаем оригинальный слой
captureSecuredView?.setValue(originalLayer, forKey: "layer")
}
}
Как работает этот код:
Сначала находим нужный внутренний слой, который будет временно заменен.
Заменяем этот слой на новый, который будет скрывать содержимое.
С помощью переключения значения
isSecureTextEntry
для UITextField перерисовываем содержимое, скрывая текст (например, пароль или номер карты). Стоит отметить, чтоisSecureTextEntry
для TextField устанавливает атрибутdisableUpdateMask
, который отправляется на рендеринг (Подробнее о disableUpdateMask).После того как процесс перерисовки завершен, восстанавливается оригинальный слой.
Напишем интеграцию UIKitView для использования в SwiftUI с возможностью скрытия от скриншотов и записи.
// Представляем UIView внутри SwiftUI, позволяя использовать UIKit-функциональность
struct HiddenOnCaptureColorView: UIViewRepresentable {
let color: UIColor
// Создаем и возвращаем UIView для отображения в SwiftUI
func makeUIView(context: Context) -> UIView {
let view = UIView()
// Применяем защиту от захвата экрана к слою View
view.layer.makeHiddenOnCapture()
updateViewColor(view: view)
return view
}
// Обновление View при изменении данных
func updateUIView(_ uiView: UIView, context: Context) {
updateViewColor(view: uiView)
}
func updateViewColor(view: UIView) {
view.backgroundColor = color
}
}
Как работает этот фрагмент кода:
Создает обертку UIViewRepresentable для использования UIKit View в SwiftUI.
При создании View применяет защиту от захвата экрана к его слою.
Обновляет цвет фона View при изменениях.
Служит мостом между UIKit и SwiftUI для реализации функции защиты.
Далее добавим модификатор-маску для скрытия контента.
// Модификатор для скрытия контента при захвате экрана (в обычном режиме контент виден)
public struct HiddenOnCaptureModifier: ViewModifier {
public func body(content: Content) -> some View {
content.mask {
// Создаем маску на основе яркости
ZStack {
Color.black // Будет полностью прозрачным (яркость → 0% alpha)
HiddenOnCaptureColorView(color: .white) // Будет видимым (яркость → 100% alpha) с защитой
}
.compositingGroup() // Обрабатываем стек как единое изображение перед преобразованием
.luminanceToAlpha() // Преобразуем яркость в прозрачность (0% яркости = прозрачно, 100% = непрозрачно)
}
}
}
Как работает этот фрагмент кода:
Создает модификатор для SwiftUI, который можно применять к любым View.
Использует технику маскирования через преобразование яркости в прозрачность.
compositingGroup()
гарантирует, что преобразование применяется к объединенному результату ZStack.luminanceToAlpha()
выполняет ключевую работу:Анализирует яркость каждого пикселя;
Преобразует значение яркости в значение прозрачности;
Создает маску, где только защищенный View остается видимым.
Добавляем для удобства расширения View, чтобы можно было использовать модификатор .hiddenOnCapture
во View.
// Расширение для View, добавляющее модификатор скрытия контента при захвате экрана
public extension View {
func hiddenOnCapture() -> some View {
modifier(HiddenOnCaptureModifier())
}
}
Как работает этот фрагмент кода:
Добавляет метод
hiddenOnCapture()
ко всем SwiftUI View.Позволяет легко применять защиту вызовом
.hiddenOnCapture()
.Упрощает использование защиты без необходимости помнить детали реализации.
Далее остается только добавить ContentView
, и применить к нему описанные ранее модификаторы для подключения реализованной защиты.
struct ContentView: View {
...
var body: some View {
VStack {
Text("Hello, World!")
...
.hiddenOnCapture()
}
}
}
Рассмотрим, как ведет себя приложение без использования защиты:

Как мы видим - данные в открытом виде попадают на запись экрана. Добавим .hiddenOnCapture()
и посмотрим, как данные будут скрываться на записи.

Стоит отметить, что при защите платежных данных на страницах оплаты следует использовать кастомную клавиатуру с применением той же защиты и отключить системную. В противном случае нажатия клавиш при вводе информации будут видны в итоговом скринкасте.
Использование модификаторов для скрытия критичной информации в фоновом режиме
Для того, чтобы скрыть критичную информацию при отображении приложения в списке "недавние" или при переходе в фоновый режим, можно написать кастомный модификатор, например:
// Модификатор, который добавляет размытие и затемнение View при переходе приложения в фоновый режим
struct AppBlurModifier: ViewModifier {
// Локальное состояние, определяющее, должно ли содержимое быть размытым
@State private var isBlurred = false
// Основное тело модификатора, которое применяет изменения к View
func body(content: Content) -> some View {
ZStack {
content
.blur(radius: isBlurred ? 10 : 0)
.animation(.easeInOut(duration: 0.2), value: isBlurred)
// Наложение полупрозрачного черного цвета при активном размытии
if isBlurred {
Color.black.opacity(0.4)
}
}
// Подписка на уведомление о переходе приложения в фоновый режим
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { _ in
isBlurred = true
}
// Подписка на уведомление о возврате приложения в активный режим
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
isBlurred = false
}
}
}
// Расширение для View, добавляющее удобный метод применения модификатора
extension View {
func blurWhenBackgrounded() -> some View {
self.modifier(AppBlurModifier())
}
}
В дальнейшем наш модификатор можно применить ко всему View:
View {
VStack {
// UI, в котором отображаются критичные данные
}
.blurWhenBackgrounded() // кастомный модификатор
}
Для маскирования критичных данных, таких, как пароли или CVV в iOS, можно использовать компонент SecureField. Он работает аналогично обычному TextField, но скрывает вводимые символы, заменяя их точками или звёздочками.
Что в итоге
Используя описанные выше техники, можно защитить приложения от захвата конфиденциальной информации через скриншоты и запись экрана.
Внедрение этих механизмов защиты может предотвратить возможную утечку критичных пользовательских данных через вредоносное ПО.
