
Для мобильных приложений, отображающих критичные данные хорошей практикой будет запретить делать снимки и запись экрана в областях отображающих конфиденциальную информацию. Иначе злоумышленники могут получить доступ к паролям или карточным данным при помощи вредоносного ПО, которое, делает записи экрана или скриншоты.
В этой статье мы рассмотрим способы защиты мобильных приложений от снятия критичной информации. Хотя в интернете можно найти материалы на эту тему, многие из них либо недостаточно подробны, либо устарели.
Материал будет полезен разработчикам, которые планируют внедрить защиту приложения от снятия критичных данных.
Механизмы защиты в 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, но скрывает вводимые символы, заменяя их точками или звёздочками.
Что в итоге
Используя описанные выше техники, можно защитить приложения от захвата конфиденциальной информации через скриншоты и запись экрана.
Внедрение этих механизмов защиты может предотвратить возможную утечку критичных пользовательских данных через вредоносное ПО.

