【iOS】SwiftUIで画面キャプチャからコンテンツを保護する

Advent Calendar 2024, iOS, Swift, SwiftUI

この記事はレコチョク Advent Calendar 2024の3日目の記事となります。

はじめに

こんにちは、永田です。
株式会社レコチョクでiOSアプリ開発をしています。

今年の個人的ベストライブは凛として時雨 TOUR 2024 Pierrrrrrrrrrrrrrrrrrrre Vibesです。
古参大歓喜のセットリストで最高でした。映像化されないかなーと楽しみにしています。


さて、今回はアプリ上で提供しているコンテンツを画面キャプチャから保護する方法についてご紹介します。

おことわり

本記事では、以下の2つをまとめて「画面キャプチャ」と称します。

  • スクリーンショット
    • 電源ボタン+音量ボタン(上)or電源ボタン+ホームボタンの同時押下で撮影できる画像
  • 画面収録
    • コントロールセンターの「画面収録」から撮影できる動画

実現したいこと・方針

音源・動画を再生できるアプリにおいて、それぞれのコンテンツを画面キャプチャから保護できるようにしたいと考えました。
具体的には以下のような状態を実現する必要があります。

  • 音源
    • 画面収録にコンテンツの音声が含まれない
  • 動画
    • スクリーンショットに映像の一部が含まれない
    • 画面収録に動画の内容(映像・音声)が含まれない

今回はこの状態を実現するために、それぞれ以下のような方針で対応を進めることとしました。

  • 画面収録中に音声の再生ボリュームを0にする
    • 音源・動画用の対策
  • スクリーンショット・画面収録を検知して動画再生Viewをマスクする
    • 動画用の対策

画面キャプチャを検知する技術

ここまでで、どういった方針で画面キャプチャからコンテンツを保護するかは決まりました。
しかし、それらを実現するにはどのような技術を使えばいいのでしょうか?

基本的には、画面キャプチャが実行・開始されたイベントを検知して、コンテンツ保護のための処理を実行すれば良さそうです。これを検知できる方法には以下のようなものがあります。

これを踏まえて、画面キャプチャからコンテンツを保護する実装を見ていきましょう。

画面収録中に音声の再生ボリュームを0にする

画面収録の開始・終了を検知して、コンテンツを再生している AVPlayer volumeを更新しましょう。

iOS 17以降をサポートしている場合は、 @Environment(\.isSceneCaptured)を用いて以下のように実装できます。

iOS 17未満をサポートしている場合は、deprecatedではありますが UIScreen.isCapturedを使う必要があります。

スクリーンショット・画面収録を検知して動画再生Viewをマスクする

画面収録を検知して音声の再生ボリュームを変更するのは比較的簡単に実装できました。
それでは動画再生Viewのマスクはどうでしょうか?

画面収録は音声と同じ方法で検知し動画再生Viewをマスクできそうですが、実はスクリーンショットの対策が鬼門なのです。

なぜなら、スクリーンショットを検知できる UIApplication.userDidTakeScreenshotNotificationはスクリーンショットの撮影後に通知されるのみであり、撮影前に対策処理を実行することができないためです。

また、画面収録の検知に使った UIScreen.isCaptured, UIScreen.capturedDidChangeNotificationではスクリーンショットの撮影が検知できません。

ではどのようにコンテンツを保護すれば良いのでしょうか?

UITextFieldを使う

ここで UITextFieldの登場です。
UITextField.isSecureTextEntrytrueにした時に、入力した内容が画面キャプチャから除外される性質を利用します。

これは、 UITextFieldのsubviewである _UITextLayoutCanvasViewによって実現されています。このViewは、親となる UITextFieldisSecureTextEntrytrueになった時に、自身のsubviewを画面キャプチャから除外するという性質を持っています。

_UITextLayoutCanvasView

これを利用し、 isSecureTextEntrytrueにした UITextFieldから
_UITextLayoutCanvasViewを取り出し、保護したいコンテンツを表示するViewを addSubviewする方針で実装します。

今回はSwiftUIから使えるように実装した内容をご紹介します。

SwiftUI.Viewを保護する

SwiftUI.Viewを受け取り、それを _UITextLayoutCanvasViewで保護するViewControllerを実装します。

_UITextLayoutCanvasViewは、OSバージョンによって名称が変わり得るため、ある程度緩めの条件で一致判定をしました。

SwiftUIから呼び出せるようにする

先ほど実装した CapturePreventingViewControllerUIViewControllerRepresentableでラップし、SwiftUIから呼び出せるようにします。

モディファイア化する

モディファイア化してより呼び出しやすくします。

挙動の確認

実装した preventCapture(_:)を実行すると、以下のような挙動になります。

実装結果

モディファイアを使用したViewが問題なく保護されていることが確認できました。
添付したGIFではわかりませんが、スクリーンショットだけでなく画面収録からも同じ実装で保護できます。

リスクと回避策

前述の実装で無事要件を満たすことはできましたが、この実装には

  • OSアップデートにより、 _UITextLayoutCanvasViewの名前が変更され、正しく保護されなくなる
  • プライベートAPIを使用しているため、Appleの審査に落ちてしまう

などのリスクが考えられます。

動画コンテンツをキャプチャから保護する実装はFairPlay Streamingで実現可能であるため、本来はこちらで実装するべきだと思います。

あくまで動画の場合はFairPlay Streamingを導入するまでの暫定実装として、動画以外の場合はどうしてもスクリーンショットから保護する必要がある場合のみ使うのがいいのかなと思います。

終わりに

今回は画面キャプチャからアプリ内のコンテンツを保護する方法をご紹介しました。

画面収録を検知して保護することは比較的簡単ですが、スクリーンショットから保護するのは意外と大変だということが伝わったのではないでしょうか。

_UITextLayoutCanvasViewを使う方法にはリスクが伴うため、どうしてもスクリーンショットから保護しないといけないのかなどはしっかり考慮した上で実装することをお勧めします。

明日の レコチョク Advent Calendar 2024 は4日目「大規模サービスのAmazon Aurora MySQLのテーブル変更で直面した3つの課題」です。お楽しみに!

参考