iOS实现 WKWebView 长截图的优雅方案

在 iOS 开发中,为 WKWebView 实现长截图功能是一个常见且棘手的需求。开发者通常会遇到以下几个痛点:

  • 网页内容高度不确定
  • 滚动区域难以完整截取
  • 截图过程中的界面闪烁影响用户体验

本文将介绍一种高效、稳定 的解决方案,通过分段渲染图像拼接,完美捕获整个网页内容,并提供可直接集成的完整代码。


🎯 核心思路

我们的方案主要分为三个清晰的步骤:

  1. 布局调整:将 WebView 移至临时容器,为完整渲染做准备。
  2. 分段渲染:按屏幕高度分段捕获内容,生成多张切片图像。
  3. 图像拼接:将所有切片图像无缝拼接成一张完整的长图。

这种方法巧妙地绕过了直接截取 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 的原始状态,确保业务无感知。

如果你有更好的实现思路,或在实际应用中遇到了特殊场景,欢迎在评论区分享交流!

相关推荐
坏小虎1 小时前
Expo 快速创建 Android/iOS 应用开发指南
android·ios·rn·expo
光影少年3 小时前
Android和iOS原生开发的基础知识对RN开发的重要性,RN打包发布时原生端需要做哪些配置?
android·前端·react native·react.js·ios
北京自在科技3 小时前
Find My 修复定位 BUG,AirTag 安全再升级
ios·findmy·airtag
Digitally3 小时前
如何不用 USB 线将 iPhone 照片传到电脑?
ios·电脑·iphone
Sim148016 小时前
iPhone将内置本地大模型,手机端AI实现0 token成本时代来临?
人工智能·ios·智能手机·iphone
Digitally18 小时前
如何将 iPad 上的照片传输到 U 盘(4 种解决方案)
ios·ipad
报错小能手20 小时前
ios开发方向——swift并发进阶核心 @MainActor 与 DispatchQueue.main 解析
开发语言·ios·swift
LcGero20 小时前
Cocos Creator 业务与原生通信详解
android·ios·cocos creator·游戏开发·jsb
ii_best20 小时前
lua语言开发脚本基础、mql命令库开发、安卓/ios基础开发教程,按键精灵新手工具
android·ios·自动化·编辑器
用户223586218202 天前
WebKit WebPage API 的引入尝试与自研实现
ios