在 iOS 开发中,为 WKWebView 实现长截图功能是一个常见且棘手的需求。开发者通常会遇到以下几个痛点:
- 网页内容高度不确定
- 滚动区域难以完整截取
- 截图过程中的界面闪烁影响用户体验
本文将介绍一种高效、稳定 的解决方案,通过分段渲染 与图像拼接,完美捕获整个网页内容,并提供可直接集成的完整代码。
🎯 核心思路
我们的方案主要分为三个清晰的步骤:
- 布局调整:将 WebView 移至临时容器,为完整渲染做准备。
- 分段渲染:按屏幕高度分段捕获内容,生成多张切片图像。
- 图像拼接:将所有切片图像无缝拼接成一张完整的长图。
这种方法巧妙地绕过了直接截取
UIScrollView的局限性,同时通过遮罩视图,保证了用户界面的视觉稳定性,避免闪烁。
💻 完整实现代码
WKWebView分类中添加长截图方法
- WKWebView+Capture.h
objective-c
#import <WebKit/WebKit.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface WKWebView (Capture)
/**
* 捕获 WKWebView 的完整内容并生成长截图
* @param completion 完成回调,返回拼接好的长图(失败则返回 nil)
*/
- (void)captureEntireWebViewWithCompletion:(void (^)(UIImage * _Nullable capturedImage))completion;
@end
NS_ASSUME_NONNULL_END
- WKWebView+Capture.m
objective-c
#import "WKWebView+Capture.h"
@implementation WKWebView (Capture)
/**
* 捕获 WKWebView 的完整内容并生成长截图
* @param completion 完成回调,返回拼接好的长图(失败则返回 nil)
*/
- (void)captureEntireWebViewWithCompletion:(void (^)(UIImage *capturedImage))completion {
// ⚠️ 关键:确保在主线程执行
if (![NSThread isMainThread]) {
NSLog(@"错误:WebView 截图必须在主线程执行");
if (completion) completion(nil);
return;
}
// 步骤1: 检查父视图并保存原始状态
UIView *parentView = self.superview;
if (!parentView) {
if (completion) completion(nil);
return;
}
CGRect originalFrame = self.frame;
CGPoint originalContentOffset = self.scrollView.contentOffset;
// 步骤2: 创建遮罩视图,保持界面"静止"的视觉效果,可以额外添加loading
UIView *snapshotCoverView = [self snapshotViewAfterScreenUpdates:NO];
snapshotCoverView.frame = self.frame; // 确保遮罩视图位置与 WebView 完全一致
[parentView insertSubview:snapshotCoverView aboveSubview:self];
// 步骤3: 创建隐藏的临时窗口和容器
UIWindow *temporaryWindow = [[UIWindow alloc] initWithFrame:self.bounds];
temporaryWindow.windowLevel = UIWindowLevelNormal - 1000; // 置于底层
temporaryWindow.hidden = NO;
temporaryWindow.alpha = 0;
temporaryWindow.userInteractionEnabled = NO;
UIView *captureContainerView = [[UIView alloc] initWithFrame:self.bounds];
captureContainerView.clipsToBounds = YES;
// 将 WebView 移入临时容器
[self removeFromSuperview];
[captureContainerView addSubview:self];
[temporaryWindow addSubview:captureContainerView];
// 步骤4: 获取完整内容高度并调整布局
CGFloat fullContentHeight = self.scrollView.contentSize.height;
self.frame = CGRectMake(0, 0, originalFrame.size.width, fullContentHeight);
self.scrollView.contentOffset = CGPointZero;
__weak typeof(self) weakSelf = self;
// ⭐ 延迟执行,确保 WebView 内容布局与渲染完成
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
if (completion) completion(nil);
return;
}
// 步骤5: 分段截图核心逻辑
CGFloat pageHeight = captureContainerView.bounds.size.height; // 单屏高度
CGFloat totalHeight = fullContentHeight; // 总内容高度
NSMutableArray<UIImage *> *imageSlices = [NSMutableArray array];
CGFloat offsetY = 0;
while (offsetY < totalHeight) {
CGFloat remainingHeight = totalHeight - offsetY;
CGFloat sliceHeight = MIN(pageHeight, remainingHeight);
// 处理最后一段高度不足一屏的情况
if (remainingHeight < pageHeight) {
CGRect containerFrame = captureContainerView.frame;
containerFrame.size.height = remainingHeight;
captureContainerView.frame = containerFrame;
}
// 移动 WebView,将当前要截取的区域"暴露"出来
CGRect webViewFrame = strongSelf.frame;
webViewFrame.origin.y = -offsetY;
strongSelf.frame = webViewFrame;
// 渲染当前分段到图像上下文
UIGraphicsBeginImageContextWithOptions(
CGSizeMake(originalFrame.size.width, sliceHeight),
NO,
[UIScreen mainScreen].scale
);
CGContextRef context = UIGraphicsGetCurrentContext();
CGFloat scaleX = originalFrame.size.width / captureContainerView.bounds.size.width;
CGFloat scaleY = sliceHeight / captureContainerView.bounds.size.height;
CGContextScaleCTM(context, scaleX, scaleY);
[captureContainerView.layer renderInContext:context];
UIImage *sliceImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
if (sliceImage) {
[imageSlices addObject:sliceImage];
}
offsetY += sliceHeight; // 移动到下一段
}
UIImage *finalImage = nil;
// 步骤6: 图像拼接
if (imageSlices.count == 1) {
finalImage = imageSlices.firstObject;
} else if (imageSlices.count > 1) {
UIGraphicsBeginImageContextWithOptions(
CGSizeMake(originalFrame.size.width, totalHeight),
NO,
[UIScreen mainScreen].scale
);
CGFloat drawOffsetY = 0;
for (UIImage *slice in imageSlices) {
[slice drawInRect:CGRectMake(0,
drawOffsetY,
slice.size.width,
slice.size.height)];
drawOffsetY += slice.size.height;
}
finalImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
// 步骤7: 恢复原始状态
[strongSelf removeFromSuperview];
[captureContainerView removeFromSuperview];
temporaryWindow.hidden = YES;
strongSelf.frame = originalFrame;
strongSelf.scrollView.contentOffset = originalContentOffset;
[parentView insertSubview:strongSelf belowSubview:snapshotCoverView];
[snapshotCoverView removeFromSuperview];
// 步骤8: 在主线程回调最终结果
if (completion) {
completion(finalImage);
}
});
}
@end
📱 效果展示

🚀 使用方法
调用方式非常简单,只需一行代码。
objective-c
// 在需要截图的地方调用
[webView captureEntireWebViewWithCompletion:^(UIImage *capturedImage) {
if (capturedImage) {
// ✅ 截图成功,处理结果
// 例如:保存到相册
UIImageWriteToSavedPhotosAlbum(capturedImage, nil, nil, nil);
// 或:上传、分享、预览等
} else {
// ❌ 截图失败
NSLog(@"截图失败");
}
}];
📝 总结
本文提供的方案通过以下关键技术,优雅地解决了 WKWebView 长截图的难题:
- 临时容器管理:隔离渲染环境,避免干扰主界面。
- 分段渲染:将长内容分解为多个可管理的屏幕片段。
- 状态恢复:完整保存并恢复 WebView 的原始状态,确保业务无感知。
如果你有更好的实现思路,或在实际应用中遇到了特殊场景,欢迎在评论区分享交流!