iOS 长截图#
背景#
Twitter 上看到 TaioApp 的作者说,iOS 系统有支持长截图的 API——UIScreenshotService,从 iOS 13 开始就可以使用,下午的时候就在自己的 APP 中体验了一下。
过程#
UIScreenshotService官方的说明如下:
When the user takes a screenshot of your app's content, you work with a UIScreenshotService object to provide a PDF version of that screenshot. You do not create a UIScreenshotService object directly. Instead, you retrieve the object from the screenshotService property of your window scene and assign a delegate to it. When the user takes a screenshot, UIKit asks your delegate for the PDF data.
截屏时,使用UIScreenshotService 最终提供的是 PDF,需要通过UIScreenshotServiceDelegate
生成 PDF data.
使用如下:
把方法处理封装到单独的类,通过方法传入 view,来决定截屏时使用那个 view 来生成 PDF data。
.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 的 size,保证 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 的 frame
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 的 Height 一直都是那么高,滑动的部分不是这个 scrollview。
-
使用了
CAShapeLayer
之类的,生成圆角或者其他,如果没有设置 layer 的 fillColor,最终UIScreenshotService显示出来会是纯黑的。 -
由于方法的回调依赖 scrollView 生成 PDF data,所以每个需要截图的界面,在进入时,都需要手动更新 contentScrollView,二级界面的还好说,可以 hook scrollView 的初始化方法,保证每次创建时都更新 helper 的 contentScrollView,但是一级界面的几个 tab 切换,则只能通过手动更新的方式来设置(或者根据 tabIndex 获取不同的 scrollview),总之这也是需要注意的地方。
总结#
UIScreenshotService确实能生成长截图,对于项目结构相对简洁明了、代码比较规范、只需要某个原生页面支持长截图的 APP 来说,可以使用。
但是如果项目中 H5 多,且项目结构复杂的话,使用就不太方便了。