iOS 長スクリーンショット#
背景#
Twitter で TaioApp の作者が、iOS システムには長スクリーンショットをサポートする API——UIScreenshotServiceがあると述べており、iOS 13 から使用可能であることを知り、午後に自分の APP で体験してみました。
プロセス#
UIScreenshotServiceの公式説明は以下の通りです:
ユーザーがアプリのコンテンツのスクリーンショットを撮ると、UIScreenshotService オブジェクトを使用してそのスクリーンショットの PDF バージョンを提供します。UIScreenshotService オブジェクトを直接作成することはありません。代わりに、ウィンドウシーンの screenshotService プロパティからオブジェクトを取得し、それにデリゲートを割り当てます。ユーザーがスクリーンショットを撮ると、UIKit はデリゲートに PDF データを要求します。
スクリーンショットを撮る際、UIScreenshotServiceを使用すると最終的に提供されるのは PDF であり、UIScreenshotServiceDelegate
を介して PDF データを生成する必要があります。
以下のように使用します:
メソッド処理を別のクラスにカプセル化し、メソッドを通じて view を渡して、スクリーンショットを撮る際にどの view を使用して PDF データを生成するかを決定します。
.h ファイル
#import <Foundation/Foundation.h>
@interface WPSScreenShotHelper : NSObject
+ (instancetype)helper;
- (void)configScreenShotHelper:(UIScrollView *)scrollView;
@end
.m ファイル
#import "MKScreenShotHelper.h"
@interface MKScreenShotHelper ()<UIScreenshotServiceDelegate>
@property (nonatomic, strong) UIScrollView *contentScrollView;
@end
@implementation MKScreenShotHelper
+ (instancetype)helper {
static WPSScreenShotHelper *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (instance == nil) {
instance = [WPSScreenShotHelper new];
}
});
return instance;
}
- (instancetype)init {
self = [super init];
if (self) {
if (@available(iOS 13.0, *)) {
[UIApplication sharedApplication].keyWindow.windowScene.screenshotService.delegate = self;
}
}
return self;
}
- (void)configScreenShotHelper:(UIScrollView *)scrollView {
self.contentScrollView = scrollView;
}
#pragma mark - UIScreenshotServiceDelegate
- (void)screenshotService:(UIScreenshotService *)screenshotService generatePDFRepresentationWithCompletion:(void (^)(NSData * _Nullable, NSInteger, CGRect))completionHandler API_AVAILABLE(ios(13.0)){
// ScrollViewのサイズを一時的に変更し、ScrollViewが完全にレンダリングされることを保証します
CGRect frame = self.contentScrollView.frame;
CGPoint contentOffset = self.contentScrollView.contentOffset;
UIEdgeInsets contentInset = self.contentScrollView.contentInset;
CGRect toFrame = frame;
toFrame.size = self.contentScrollView.contentSize;
self.contentScrollView.frame = toFrame;
// scrollViewをPDFとしてレンダリング
NSMutableData *pdfData = [NSMutableData data];
UIGraphicsBeginPDFContextToData(pdfData, self.contentScrollView.frame, nil);
UIGraphicsBeginPDFPage();
CGContextRef pdfContext = UIGraphicsGetCurrentContext();
[self.contentScrollView.layer renderInContext:pdfContext];
UIGraphicsEndPDFContext();
// scrollViewのフレームを元に戻す
self.contentScrollView.frame = frame;
self.contentScrollView.contentOffset = contentOffset;
self.contentScrollView.contentInset = contentInset;
// 結果をコールバック
completionHandler(pdfData, 0, CGRectZero);
}
@end
UIScreenshotServiceを使用すると、確かに長スクリーンショットが可能で、キャプチャされる内容のサイズは ScrollView の contentSize です。
しかし、以下のいくつかの問題に注意が必要です:
-
ScrollView 上にないコンテンツは、UIScreenshotServiceを使用しても表示されません。例えば、navigationBar や tabBar などです。
-
webview のスクリーンショットは、webview の scrollview の高さを取得することでキャプチャできます。ただし、webview 内で読み込まれたウェブページの中間部分やウェブページの一部がスクロールを使用している場合、つまり WebView の Scroll ではなく H5 ウェブページ内の Scroll の場合、その表示はUIScreenshotServiceでは取得できません。なぜなら、最終的に webview の scrollview の contentSize の高さは常に同じで、スクロール部分はこの scrollview ではないからです。
-
CAShapeLayer
などを使用して角丸やその他を生成する場合、layer の fillColor が設定されていないと、最終的にUIScreenshotServiceで表示されるのは真っ黒になります。 -
メソッドのコールバックが scrollView に依存して PDF データを生成するため、各スクリーンショットが必要な画面では、入る際に手動で contentScrollView を更新する必要があります。二次画面はまだ良いですが、scrollView の初期化メソッドをフックして、毎回作成時に helper の contentScrollView を更新することができます。しかし、一次画面のいくつかのタブ切り替えは、手動で更新する方法(または tabIndex に基づいて異なる scrollview を取得する)で設定する必要があります。いずれにせよ、これは注意が必要な点です。
まとめ#
UIScreenshotServiceは確かに長スクリーンショットを生成できます。プロジェクト構造が比較的シンプルで、コードが比較的規範的で、特定のネイティブページが長スクリーンショットをサポートする APP には使用できます。しかし、プロジェクト内に H5 が多く、プロジェクト構造が複雑な場合、使用するのはあまり便利ではありません。