uni-app原生插件(1)--截图

现在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原生开发,这里并不过多描述,主要时把插件开发流程记录下来。

相关推荐
逻辑克2 小时前
使用 MultipeerConnectivity 在 iOS 中实现近场无线数据传输
ios
dnekmihfbnmv6 小时前
好用的电容笔有哪些推荐一下?年度最值得推荐五款电容笔分享!
ios·电脑·ipad·平板
Magnetic_h1 天前
【iOS】单例模式
笔记·学习·ui·ios·单例模式·objective-c
归辞...1 天前
「iOS」——单例模式
ios·单例模式·cocoa
yanling20231 天前
黑神话悟空mac可以玩吗
macos·ios·crossove·crossove24
归辞...1 天前
「iOS」viewController的生命周期
ios·cocoa·xcode
crasowas1 天前
Flutter问题记录 - 适配Xcode 16和iOS 18
flutter·ios·xcode
2401_852403551 天前
Mac导入iPhone的照片怎么删除?快速方法讲解
macos·ios·iphone
SchneeDuan1 天前
iOS六大设计原则&&设计模式
ios·设计模式·cocoa·设计原则
JohnsonXin2 天前
【兼容性记录】video标签在 IOS 和 安卓中的问题
android·前端·css·ios·h5·兼容性