现在uni-app使用的越来越多,但是有些功能需要原生开发插件放入uni-app程序中来调用,本篇文章记录以下iOS原生截图插件的开发过程
1:首先创建插件工程
打开 Xcode,创建一个新的Framework工程,然后点击 Next,然后选中工程名,在TARGETS->Build Settings中,将 Mach-O Type 设置为 Static Library
输入插件工程名称,然后点击Next,然后选择存放路劲
2:导入插件工程
打开工程目录,双击目录中的HBuilder-uniPlugin.xcodeproj 文件运行插件开发主工程, 在 Xcode 项目左侧目录选中主工程名,然后点击右键选择Add Files to "HBuilder-uniPlugin"导入插件工程
然后选择您刚刚创建的插件工程路径中,选中插件工程文件,勾选 Create folder references 和 Add to targets 两项,然后点击Add
3:工程配置
在 Xcode 项目左侧目录选中主工程名,在TARGETS->Build Phases->Dependencies中点击+,在弹窗中选中插件工程,然后点击Add,将插件工程添加到Dependencies中;然后在Link Binary With Libraries中点击+,同样在弹窗中选中插件工程,点击Add.
接下来需要在插件工程的Header Search Paths中添加开发插件所需的头文件引用,头文件存放在主工程的HBuilder-Hello/inc中,,在 Xcode 项目左侧目录选中插件工程名,找到TARGETS->Build Settings->Header Search Paths双击右侧区域打开添加窗口,然后将inc目录拖入会自动填充相对路径,然后将模式改成recursive
4:代码实现
本文需求是浏览网页的长截图,苹果没有单独的生成长截图的API,所以要采取替代方案,使用多次截图并屏截方案,多次测试中,屏截方案会有显示空白页面问题,所以才去了笨办法,使用createPDFWithConfiguration把网页先转换成PDF文件再生成image,然后与截图的图片再次屏截在一个页面,生成最后的图片; 方案执行中,百度浏览器搜索内容网页,截图后生成图片会有黑色遮挡,所以采取了新的截图方案
objectivec
//保存地址 并传参uni-app
- (void)saveAsPDFWithTitle:(NSString *)title {
if (@available(iOS 13.4, *)) {
__weak __typeof(self)weakSelf = self;
__strong __typeof(weakSelf)strongSelf = weakSelf;
NSLog(@"%f",[self getWebView].wkWebView.frame.size.height);
//创建的截图页面
UIView *backgroundView = [[UIView alloc] init];
[self snapshotForWKWebView:[self getWebView].wkWebView CaptureCompletionHandler:^(UIImage *capturedImage) {
NSLog(@"%@",capturedImage);
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDownloadsDirectory, NSUserDomainMask, YES);
NSString *downloadsDirectory = [paths firstObject];
if (downloadsDirectory) {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *myPathDocs = [documentsDirectory stringByAppendingPathComponent:@"/Pandora/documents/output_image.jpeg"];
if([[NSFileManager defaultManager] fileExistsAtPath:myPathDocs]) {
[[NSFileManager defaultManager] removeItemAtPath:myPathDocs error:nil];
}
NSURL *outputURL = [NSURL fileURLWithPath:myPathDocs];
NSData *jpegData = UIImageJPEGRepresentation(capturedImage, 0.8);
NSData *jpegData2 = UIImagePNGRepresentation(capturedImage);
NSLog(@"%@==%@",jpegData,jpegData2);
[jpegData2 writeToURL:outputURL atomically:YES];
WebView *webView = [strongSelf getWebView];
NSString *url = webView.wkWebView.URL.absoluteString;
backgroundView.frame = CGRectMake(0, 0, capturedImage.size.width, capturedImage.size.height);
UIImageView *image = [[UIImageView alloc] init];
image.frame = backgroundView.frame;
image.image = capturedImage;
[backgroundView addSubview:image];
NSLog(@"%@==%@",outputURL.path,url);
WKPDFConfiguration *pdfConfiguration = [[WKPDFConfiguration alloc] init];
UIView *contentView = [self getWebView].wkWebView.scrollView.subviews.firstObject.subviews.firstObject;
/// 使用`webView.scrollView.frame` 可以捕获整个页面,而不仅仅是可见部分
pdfConfiguration.rect = CGRectMake(0, 0, contentView.frame.size.width, contentView.frame.size.height);
if (@available(iOS 14.0, *)) {
//__weak __typeof(self)weakSelf = self;
[webView.wkWebView createPDFWithConfiguration:pdfConfiguration completionHandler:^(NSData *pdfData, NSError *error) {
if (pdfData) {
//__strong __typeof(weakSelf)strongSelf = weakSelf;
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDownloadsDirectory, NSUserDomainMask, YES);
NSString *downloadsDirectory = [paths firstObject];
if (downloadsDirectory) {
NSString *fileName = title ?: @"PDF";
NSString *savePath = [[downloadsDirectory stringByAppendingPathComponent:fileName] stringByAppendingPathExtension:@"pdf"];
NSError *writeError;
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *myPathDocs = [documentsDirectory stringByAppendingPathComponent:@"/Pandora/documents/output_image.jpeg"];
if([[NSFileManager defaultManager] fileExistsAtPath:myPathDocs]) {
[[NSFileManager defaultManager] removeItemAtPath:myPathDocs error:nil];
}
NSURL *outputURL = [NSURL fileURLWithPath:myPathDocs];
UIImage *pdfImage = [strongSelf convertPDFDataToImage:pdfData];
//UIImage *pdfImage = [UIImage imageWithData:pdfData];
NSData *jpegData = UIImageJPEGRepresentation(pdfImage, 0.8);
[jpegData writeToURL:outputURL atomically:YES];
NSLog(@"%@",outputURL.path);
if (!writeError) {
NSLog(@"Successfully created and saved PDF at %@", savePath);
} else {
NSLog(@"Could not save pdf due to %@", writeError.localizedDescription);
}
WebView *webView = [strongSelf getWebView];
// 计算页面停留时间
NSDate *screenshotTimestamp = [NSDate date];
NSTimeInterval timeInterval = [screenshotTimestamp timeIntervalSinceDate:webView.pageLoadTimestamp];
NSLog(@"页面停留时间: %f 秒", timeInterval);
// 转换 timeInterval 为字符串
NSString *timeIntervalString = [NSString stringWithFormat:@"%f", timeInterval];
// 获取当前访问的 url
NSString *url = webView.wkWebView.URL.absoluteString;
NSLog(@"加载PDF前 W==%f H==%f PDF图片大小%f %f",backgroundView.frame.size.width,backgroundView.frame.size.height,pdfImage.size.width,pdfImage.size.height);
CGFloat pdfImgHeight = pdfImage.size.height;
CGFloat pdfImgWidth = pdfImage.size.width;
if (pdfImage.size.width > backgroundView.bounds.size.width) {
pdfImgHeight = backgroundView.bounds.size.width/pdfImage.size.width*pdfImage.size.height;
pdfImgWidth = backgroundView.bounds.size.width;
}
UIImageView *image = [[UIImageView alloc] init];
image.frame = CGRectMake(0, 0, pdfImgWidth, pdfImgHeight);;
image.image = [UIImage imageWithData:jpegData];
//[backgroundView addSubview:image];
if ([[NSString stringWithFormat:@"%@",self->_loadURLStr] containsString:@"https://www.baidu.com"]) {
} else {
[backgroundView addSubview:image];
}
NSLog(@"加载PDF后 W==%f H==%f 截图大小%f==%f",backgroundView.frame.size.width,backgroundView.frame.size.height,capturedImage.size.width,capturedImage.size.height);
//把view转换成image截图
UIImage *imageRet = [self getImageFromView:backgroundView];
NSData *jpegData3 = UIImagePNGRepresentation(imageRet);
[jpegData3 writeToURL:outputURL atomically:YES];
NSLog(@"%@==%@==",outputURL.path,jpegData3);
NSDictionary *params = @{@"detail":@{@"image":outputURL.path,
@"url": url,
@"time": timeIntervalString}};
NSLog(@"截图完成,把参数传递给uniapp %@", params);
self.loadGifImgView.hidden = YES;
//与uni-app交互方法,给uni-app传参
[strongSelf fireEvent:@"screenshotFinish" params:params domChanges:nil];
}
} else {
NSLog(@"%@==", error.localizedDescription);
}
}];
} else {
// Fallback on earlier versions
}
}
}];
} else {
// Fallback on earlier versions
}
}
- (void)snapshotForWKWebView:(WKWebView *)webView CaptureCompletionHandler:(void (^)(UIImage * _Nonnull))completionHandler {
if ([[NSString stringWithFormat:@"%@",self->_loadURLStr] containsString:@"https://www.baidu.com"]) {
//1.添加遮罩层
UIView *snapshotView = [webView snapshotViewAfterScreenUpdates:YES];
snapshotView.frame = webView.frame;
[webView.superview addSubview:snapshotView];
//2.初始化数组
self->_ImageAry = [NSMutableArray array];
//3.进行截图操作
CGPoint savedCurrentContentOffset = webView.scrollView.contentOffset;
webView.scrollView.contentOffset = CGPointZero;
NSLog(@"网页也高度=====%f===w:%f",webView.scrollView.contentSize.height,webView.scrollView.contentSize.width);
[self createSnapshotForWKWebView:webView offset:0.0 remainingOffset_y:webView.scrollView.contentSize.height comletionBlock:^(UIImage *snapshotImg) {
webView.scrollView.contentOffset = savedCurrentContentOffset;
[snapshotView removeFromSuperview];
UIImage *shotImg = [self captureScrollView:webView.scrollView];
completionHandler(snapshotImg);
}];
} else {
// 1.为了截图时对 frame 进行操作不会出现闪屏等现象,我们需要盖一个"假"的 webView 到现在的位置上,并将真正的 webView "摘下来"。调用 snapshotViewAfterScreenUpdates 即可得到这样一个"假"的 webView
UIView *snapshotView = [webView snapshotViewAfterScreenUpdates:YES];
snapshotView.frame = webView.frame;
[webView.superview addSubview:snapshotView];
// 2. 保存真正的 webView 的偏移、位置等信息,以便截图完成之后"还原现场"
CGPoint currentOffset = webView.scrollView.contentOffset;
CGRect currentFrame = webView.frame;
UIView *currentSuperView = webView.superview;
NSUInteger currentIndex = [webView.superview.subviews indexOfObject:webView];
// 3. 用一个新的视图承载"真正的" webView,这个视图也是绘图所用到的上下文
UIView *containerView = [[UIView alloc] initWithFrame:webView.bounds];
[webView removeFromSuperview];
[containerView addSubview:webView];
// 4. 将 webView 按照实际内容高度和屏幕高度分成 page 页
CGSize totalSize = webView.scrollView.contentSize;
NSLog(@"%f",totalSize.height);
[webView evaluateJavaScript:@"document.body.scrollHeight" completionHandler:^(id _Nullable result,NSError * _Nullable error) {
CGFloat webViewHeight = [result doubleValue];
NSLog(@"%f",webViewHeight);
}];
NSInteger page = ceil(totalSize.height / containerView.bounds.size.height);
webView.scrollView.contentOffset = CGPointZero;
[webView evaluateJavaScript:@"window.scrollTo(0,0)" completionHandler:nil];
webView.frame = CGRectMake(0, 0, containerView.bounds.size.width, webView.scrollView.contentSize.height);
UIGraphicsBeginImageContextWithOptions(totalSize, YES, UIScreen.mainScreen.scale);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self drawContentPage:containerView webView:webView index:0 maxIndex:page completion:^{
UIImage *snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
NSLog(@"生成的图片%ld==%@",(long)page,snapshotImage);
//[webView removeFromSuperview];
completionHandler(snapshotImage);
[currentSuperView insertSubview:webView atIndex:currentIndex];
webView.frame = currentFrame;
webView.scrollView.contentOffset = currentOffset;
// 8. 调用 UIGraphicsGetImageFromCurrentImageContext 方法从当前上下文中获取到完整截图,将第 2 步中保存的信息重新赋予到 webView 上,"还原现场"
[snapshotView removeFromSuperview];
}];
});
}
}
- (void)drawContentPage:(UIView *)targetView webView:(WKWebView *)webView index:(NSInteger)index maxIndex:(NSInteger)maxIndex completion:(dispatch_block_t)completion
{
// 5. 得到每一页的实际位置,并将 webView 往上推到该位置
CGRect splitFrame = CGRectMake(0, index * CGRectGetHeight(targetView.bounds), targetView.bounds.size.width, targetView.frame.size.height);
CGRect myFrame = webView.frame;
myFrame.origin.y = -(index * targetView.frame.size.height);
webView.frame = myFrame;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 6. 调用 drawViewHierarchyInRect 将当前位置的 webView 渲染到上下文中
[targetView drawViewHierarchyInRect:splitFrame afterScreenUpdates:YES];
// 7. 如果还未到达最后一页,则递归调用 drawViewHierarchyInRect 方法进行渲染;如果已经渲染完了全部页,则回调通知截图完成
if (index < maxIndex) {
[self drawContentPage:targetView webView:webView index:index + 1 maxIndex:maxIndex completion:completion];
} else {
completion();
}
});
}
/** 新的截图方法*/
- (void)createSnapshotForWKWebView:(WKWebView *)webView offset:(float)offset_y remainingOffset_y:(float)reOffset_y comletionBlock:(void(^)(UIImage *snapshotImg))completeBlock
{
//判断scrollView是否已经滚动到底
if (reOffset_y>0) {
//设置
[webView.scrollView setContentOffset:CGPointMake(0, offset_y) animated:NO];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(),^{
//对页面进行截图操作
UIGraphicsBeginImageContextWithOptions(webView.frame.size, YES, [UIScreen mainScreen].scale);
[webView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage * img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
//将截图添加进数组
[self->_ImageAry addObject:img];
//修改offsetY偏移量
CGFloat newOffset_y = offset_y + webView.scrollView.frame.size.height;
CGFloat newReOffset_y = reOffset_y - webView.frame.size.height;
[self createSnapshotForWKWebView:webView offset:newOffset_y remainingOffset_y:newReOffset_y comletionBlock:completeBlock];
});
}else {
//合成截图为最终截图
UIView * containerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, webView.frame.size.width, webView.scrollView.contentSize.height)];
CGFloat originYOfImgView = 0;
for (int i = 0; i<self->_ImageAry.count; i++) {
UIImageView * imgView = [[UIImageView alloc] initWithFrame:CGRectMake(0, originYOfImgView, webView.frame.size.width, webView.frame.size.height)];
UIImage * img = self->_ImageAry[i];
imgView.image = img;
originYOfImgView += webView.frame.size.height;
[containerView addSubview:imgView];
}
//添加合成视图
[webView.superview addSubview:containerView];
//处理最终合并截图
UIGraphicsBeginImageContextWithOptions(containerView.frame.size, YES, [UIScreen mainScreen].scale);
[containerView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage * img2 = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
//移除合成视图
[containerView removeFromSuperview];
//返回截图
if (completeBlock) {
completeBlock(img2);
}
}
}
- (UIImage *)captureScrollView:(UIScrollView *)scrollView
{
UIImage* image = nil;
UIGraphicsBeginImageContextWithOptions(scrollView.contentSize, NO, 0.0);
{
CGFloat scale = 1;
CGPoint savedContentOffset = scrollView.contentOffset;
CGRect savedFrame = scrollView.frame;
scrollView.contentOffset = CGPointZero;
scrollView.frame = CGRectMake(0, 0, scrollView.contentSize.width * scale, scrollView.contentSize.height * scale + 30);
[scrollView.layer renderInContext:UIGraphicsGetCurrentContext()];
image = UIGraphicsGetImageFromCurrentImageContext();
scrollView.contentOffset = savedContentOffset;
scrollView.frame = savedFrame;
}
UIGraphicsEndImageContext();
if (image != nil) {
return image;
}
return nil;
}
-(UIImage *)getImageFromView:(UIView *)view{
CGSize s = view.bounds.size;
// 下面方法,第一个参数表示区域大小。第二个参数表示是否是非透明的。如果需要显示半透明效果,需要传 NO,否则传YES。第三个参数就是屏幕密度了
UIGraphicsBeginImageContextWithOptions(s, YES, [UIScreen mainScreen].scale);
[view.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage*image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
//pdf转换成image方法
- (UIImage *)convertPDFDataToImage:(NSData *)pdfData {
CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)pdfData);
CGPDFDocumentRef pdfDocument = CGPDFDocumentCreateWithProvider(provider);
if (pdfDocument == NULL) {
NSLog(@"Could not create PDF document from data");
return nil;
}
CGPDFPageRef pdfPage = CGPDFDocumentGetPage(pdfDocument, 1);
if (pdfPage == NULL) {
NSLog(@"Could not get first page of PDF document");
return nil;
}
CGRect pdfPageRect = CGPDFPageGetBoxRect(pdfPage, kCGPDFMediaBox);
UIGraphicsBeginImageContext(pdfPageRect.size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
CGContextFillRect(context, pdfPageRect);
CGContextTranslateCTM(context, 0.0, pdfPageRect.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
CGAffineTransform pdfTransform = CGPDFPageGetDrawingTransform(pdfPage, kCGPDFMediaBox, pdfPageRect, 0, YES);
CGContextConcatCTM(context, pdfTransform);
CGContextDrawPDFPage(context, pdfPage);
UIImage *resultingImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
CGPDFDocumentRelease(pdfDocument);
CGDataProviderRelease(provider);
return resultingImage;
}
5:配置插件信息
uni-app网站有这些配置信息代码 搬过来就行,选中工程中的HBuilder-uniPlugin-Info.plist文件右键->Open As->Source Code找到dcloud_uniplugins节点,copy下面的内容添加到dcloud_uniplugins节点下,对应填写
xml
<dict>
<key>hooksClass</key>
<string>填写 hooksClass 类名 </string>
<key>plugins</key>
<array>
<dict>
<key>class</key>
<string>填写 module 或 component 的类名</string>
<key>name</key>
<string>填写暴露给js端对应的 module 或 component 名称</string>
<key>type</key>
<string>填写 module 或 component</string>
</dict>
</array>
</dict>
在 uni-app 项目中调用 module 方法
6:导入uni-app资源
首先需要生成本地打包资源,在 HBuilderX 中选您的 uni-app 工程,右键->发现->原生App-本地打→生成本地打包App资源
项目编译完成后会在 HBuilderX 控制台输出资源存路径,点击路径会自动打开资源所在文件 将应用资源导入到插件开发主工程的HBuilder-Hello/Pandora/apps/中,如下图所示,直接拖进去即可
然后打开工程的 control.xml 文件,将 appid 改成 uni-app项目的 id,info.plist修改id
然后运行项目测试,如下图所示(能调到 module 的方法,并且可以获取 module 返回的数据,则说明功能正常)
7: 生成插件包
编译生成插件库文件(.framework 或 .a) 如下图所示,将编译工程选择为插件项目(DCTestUniPlugin),运行设备选择Generic iOS Device
然后点击Edit Scheme...在弹窗中,将Run->Info->Build Configuration切换到Release,然后点击Close关闭弹窗 然后在 Xcode 左侧目录中选中插件工程名,查看TARGETS->Build Settings->Architectures,确保 Build Active Architecture Only->Release 为 No Valid Architectures 中至少包含 arm64 和 armv7(一般保持工程默认配置即可),然后Command + B 编译运行工程 编译完成后,在插件工程 Products 下生成的库(DCTestUniPlugin.framework)即为插件所需要的依赖库文件,右键->Show in Finder,可打开库所在文件夹
8:编写page.JSON文件,生成标准的插件包
package.json 为插件的配置文件,配置了插件id、格式、插件资源以及插件所需权限等等信息,uni-app官网有标准格式,直接拷贝过来,填入相应的内容
json
{
"name": "TestUniPlugin",
"id": "DCTestUniPlugin",
"version": "1.0.0",
"description": "uni示例插件",
"_dp_type": "nativeplugin",
"_dp_nativeplugin": {
"ios": {
"plugins": [{
"type": "module",
"name": "DCTestUniPlugin-TestModule",
"class": "TestModule"
}, {
"type": "component",
"name": "dc-testmap",
"class": "TestComponent"
}],
"frameworks": ["MapKit.framework"],
"integrateType": "framework",
"deploymentTarget": "9.0"
}
}
}
插件id为名新建一个文件夹,将编辑好的 package.json 放进去,然后在文件夹中在新建一个 ios (小写)文件夹,将刚刚生成的依赖库(DCTestUniPlugin.framework)copy 到 ios 根目录,这样我们的插件包就构建完成了
9:使用插件
HBuilderX 的 uni-app 项目创建中"nativeplugins"目录(如不存在则创建)将插件配置到uni-app项目下的"nativeplugins"目录
将原生插件配置到uni-app项目的"nativeplugins"下,还需要在manifest.json文件的"App原生插件配置"项下点击"选择本地插件",在列表中选择需要打包生效的插件:
无论时真机测试与发布,都需要打包基座插件,打包流程如下图
截图插件完整流程到这里就结束了,关于截图功能就是iOS原生开发,这里并不过多描述,主要时把插件开发流程记录下来。