欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

UIWebView保存图片

程序员文章站 2024-02-01 20:28:28
现在h5混合原生开发的方式越来越流行,也就要用到uiwenview控件。在开发过程中,我们可能会遇到一个需求,要求我们保存网页上的图片,当用户点击图片的时候,就可以让用户选择是否下载图片。 在自带的...

现在h5混合原生开发的方式越来越流行,也就要用到uiwenview控件。在开发过程中,我们可能会遇到一个需求,要求我们保存网页上的图片,当用户点击图片的时候,就可以让用户选择是否下载图片。

在自带的safari已经实现了该功能,但是ios开发中我们如果调用uiwebview加载图片,会发现无法使用safari保存图片的功能的。这就需要我们自己去实现。

要保存网页中的图片,关键是要获取手指点击位置的图片的url地址,这就需要从js调用oc的方法。下面介绍两种方法来实现图片保存功能,但是这两种方法都只适用于图片地址用如下形式表示才可以获取:

![](https://www.img0.bdstatic.com/img/image/meinvtoutu1242.png)

如果图片是通过js动态生成的,就无法使用下面的方法获取。


方法1、获取点击位置的图片的src属性

实现原理:

该方法主要是获取手指点击的位置,然后获取该位置的标签的src属性,如果是img标签,那么就可以获取到url。如果是其他标签,就无法获取到url属性。

实现代码:

@interface viewcontroller ()
@property (weak, nonatomic) iboutlet uiwebview *webview;
@end


@implementation viewcontroller
- (void)viewdidload
{
    self.webview.delegate = self;
    [self.webview  loadrequest:[nsurlrequest requestwithurl:[nsurl urlwithstring:@"https://image.baidu.com/wisebrowse/index?tag1=%e7%be%8e%e5%a5%b3&tag2=%e5%85%a8%e9%83%a8&tag3=&pn=0&rn=10&from=index&fmpage=index&pos=magic#/home"]]];
    uilongpressgesturerecognizer * longpressed = [[uilongpressgesturerecognizer alloc] initwithtarget:self action:@selector(longpressed:)];
    longpressed.delegate = self;
    [self.webview addgesturerecognizer:longpressed];
}

- (void)longpressed:(uitapgesturerecognizer*)recognizer{

    //只在长按手势开始的时候才去获取图片的url
    if (recognizer.state != uigesturerecognizerstatebegan) {
        return;
    }

    cgpoint touchpoint = [recognizer locationinview:self.webview];

    nsstring *js = [nsstring stringwithformat:@"document.elementfrompoint(%f, %f).src", touchpoint.x, touchpoint.y];
    nsstring *urltosave = [self.webview stringbyevaluatingjavascriptfromstring:js];

    if (urltosave.length == 0) {
        return;
    }

    nslog(@"获取到图片地址:%@",urltosave);

}

//可以识别多个手势
-(bool)gesturerecognizer:(uigesturerecognizer *)gesturerecognizer shouldrecognizesimultaneouslywithgesturerecognizer:(uigesturerecognizer *)othergesturerecognizer
{
    return yes;
}

分析:

上述代码的核心功能实现就是如下两行代码:

    nsstring *js = [nsstring stringwithformat:@"document.elementfrompoint(%f, %f).src", touchpoint.x, touchpoint.y];
    nsstring *urltosave = [self.webview stringbyevaluatingjavascriptfromstring:js];

第一行代码是通过js获取点击位置的标签的src属性,第二行代码是接受向webview注入第一行的js代码后返回的src属性。
如果点击位置是图片,那么久可以通过img.src拿到图片的url地址,如果不是就返回空值。

效果展示:

打开的网页如下所示:

长按不同图片输出结果如下:

可以看到获取到了正确的图片地址


方法二、遍历dom树获取src属性

该方法的实现比较复杂,但是灵活性更高,不仅仅适用于保存图片。而是适用于任何从js调用oc方法的场景。

1、nsobject类扩展

首先我们来给nsobject加一个类扩展,是为了可以给方法传递任意个参数,因为默认系统的performselector方法最多只能传递两个参数

#import "nsobject+extension.h"

@implementation nsobject (extension)
- (id)performselector:(sel)selector withobjects:(nsarray *)objects
{
    // 方法签名(对方法的描述)
    nsmethodsignature *signature = [[self class] instancemethodsignatureforselector:selector];

    if (signature == nil) {
        [nsexception raise:@"严重错误" format:@"(%@)方法找不到", nsstringfromselector(selector)];
    }

    /*nsinvocation : 利用一个nsinvocation对象通过调用方法签名来实现对方法的调用(方法调用者、方法名、方法参数、方法返回值)
    如果仅仅完成这步,和普通的函数调用没有区别,但是关键在于nsinvocation可以对传递进来的selector进行包装,实现可以传递无限多个参数
   普通的方法调用比如:[self performselector:<#(sel)#> withobject:<#(id)#> withobject:<#(id)#>]顶多只能传递两个参数给selector*/
    nsinvocation *invocation = [nsinvocation invocationwithmethodsignature:signature];
    invocation.target = self; //调用者是自己
    invocation.selector = selector; //调用的方法是selector

    // 设置参数,可以传递无限个参数
    nsinteger paramscount = signature.numberofarguments - 2; // 除self、_cmd以外的参数个数
    paramscount = min(paramscount, objects.count); //防止函数有参数但是不传递参数时,导致崩溃
    for (nsinteger i = 0; i < paramscount; i++) {
        id object = objects[i];
        if ([object iskindofclass:[nsnull class]]) continue; //如果传递的参数为null,就不处理了
        [invocation setargument:&object atindex:i + 2]; // +2是因为第一个和第二个参数默认是self和_cmd
    }

    // 调用方法
    [invocation invoke];

    // 获取返回值
    id returnvalue = nil;
    if (signature.methodreturnlength) { // 有返回值类型,才去获得返回值
        [invocation getreturnvalue:&returnvalue];
    }

    return returnvalue;
}
@end

2、向webview注入js

在webview的代理方法webviewdidfinishload注入如下js代码

-(void)injectjs:(uiwebview *)webview{
    //js方法遍历图片添加点击事件 返回图片个数
    static  nsstring * const jsgetimages =
    @"function getimages(){\
    var objs = document.getelementsbytagname(\"img\");\
    for(var i=0;i

上面的方法是遍历dom树,从中找到img标签,然后给每个img标签加上点击事件,让图片的url变为"jscallbackoc://saveimage_*\"+this.src;形式,我们先不着急为什么这样做,先记住这里就好了,下面会给大家解释为什么这么做。

3、切割跳转url

在点击图片的时候,上面的注入的js代码就会起作用了,那么点击图片,就会触发绑定在该图片的时间,开始跳转,然后执行如下js代码

 document.location.href=\"jscallbackoc://saveimage_*\"+this.src;\

此时就会生成新的图片跳转url为:jscallbackoc://saveimage_*www.baidu.com

我们在webview的如下代理方法中,可以捕获到该url,然后切割处理

(bool)webview:(uiwebview *)webview shouldstartloadwithrequest:(nsurlrequest *)request navigationtype:(uiwebviewnavigationtype)navigationtype

我们捕获到跳转url后,就开始做切割处理,把他转换成oc方法,先看代码

- (void)performjsmethodwithurl:(nsstring *)url protocolname:(nsstring *)name performviewcontroller:(uiviewcontroller *)viewcontroller {
    /*
     跳转url :        jscallbackoc://sendmessage_number2_number3_*100$100$99
     protocolname:   jscallbackoc://
     方法名:          sendmessage:number2:number3
     参数:            100,100,99

     方法名和参数组合为oc的方法为:- (void)sendmessage:(nsstring *)number number2:(nsstring *)number2 number3:(nsstring *)number3
     */

    // 获得协议后面的路径为: sendmessage_number2_*200$300
    nsstring *path = [url substringfromindex:name.length];

    // 利用“*”切割路径,“*”前面是方法名,后面是参数
    nsarray *subpaths = [path componentsseparatedbystring:@"*"];

    // 方法名 methodname == sendmessage:number2:
    // 下面的方法是把"_"替换为"?', js返回的url里面把“:”直接省略了,只能在js里面使用“_”来表示,然后在oc里面再替换回“:”
    nsstring *methodname =[[subpaths firstobject ] stringbyreplacingoccurrencesofstring:@"_" withstring:@":"];

    // 参数  200$300,每个参数之间使用符号$链接(避免和url里面的其他字符混淆,因为一般url里面不会出现这个字符)
    nsarray *params = nil;
    if (subpaths.count == 2) {
        params = [[subpaths lastobject] componentsseparatedbystring:@"$"];
    }
    nslog(@"方法名:%@-----参数:%@", methodname,params);
    // 调用nsobject类扩展方法,传递多个参数
    [viewcontroller performselector:nsselectorfromstring(methodname) withobjects:params];
}

需要注意的一点就是跳转url的格式必须如下所示:

 jscallbackoc://sendmessage_number2_number3_*100$100$99

其中jscallbackoc使我们自定义的协议名字,你可以改成任意的。但是其他的部分格式必须按照上面所示

协议名格式: 协议名://

方法名: 方法签名1:方法签名2:方法签名3(方法签名用冒号连接)

参数: 参数1$参数2$参数3(多个参数用$连接)

协议名和方法名直接连接,方法名和参数用*连接

不建议把上面的冒号和$改成其他符号,比如你改成问号(?),如果原来的src的url里面包括?,那么就会出现错误。

假设被点击的图片的src地址是:www.baidu.com。那么js代码`` document.location.href=\"jscallbackoc://saveimage_\"+this.src;``生成的图片跳转url就是jscallbackoc://saveimage_www.baidu.com

经过上面的方法切割之后,得到了方法名:saveimage和参数:www.baidu.com。

然后把方法名和参数传入最后一行的该方法,就可以执行oc代码了

[viewcontroller performselector:nsselectorfromstring(methodname) withobjects:params];

此时只要在oc代码里面实现方法-(void)saveimage:(nsstring *)imageurl就可以被执行了。

4、拦截跳转url

我们在webview的代理方法中,可以捕获到点击图片后的跳转url,然后做处理

- (bool)webview:(uiwebview *)webview shouldstartloadwithrequest:(nsurlrequest *)request navigationtype:(uiwebviewnavigationtype)navigationtype
{
    //点击web页面的图片调用如下代码
    nsstring *url = [[request url] absolutestring];
    nsstring *protocolname = @"jscallbackoc://";

    if ( [url hasprefix:protocolname]) {
        [self performjsmethodwithurl:url protocolname:protocolname performviewcontroller:self ];
        return no;
    }
    return yes;

}

在如上方法中我们做了判断,如果跳转过来的url包含了前缀"jscallbackoc://,就说明使我们自定义的方法,我们需要做拦截处理,转换为oc方法,但是其他跳转url就不做任务处理。

5、验证结果

我们分别点击网页上的不同图片,输出结果如下:

可以看到oc的方法-(void)saveimage:(nsstring *)imageurl被执行了。

6、完整代码

#import "viewcontroller.h"
#import "nsobject+extension.h"

@interface viewcontroller ()
@property (weak, nonatomic) iboutlet uiwebview *webview;
@end

@implementation viewcontroller

- (void)viewdidload
{
    self.webview.delegate = self;
    [self.webview  loadrequest:[nsurlrequest requestwithurl:[nsurl urlwithstring:@"https://image.baidu.com/wisebrowse/index?tag1=%e7%be%8e%e5%a5%b3&tag2=%e5%85%a8%e9%83%a8&tag3=&pn=0&rn=10&from=index&fmpage=index&pos=magic#/home"]]];
}

-(void)webviewdidfinishload:(uiwebview *)webview
{
    [self injectjs:webview];

}


//每次网页在需要跳转之前,就会执行该方法。返回yes,表示可以跳转。返回no,表示不跳转。我们可以在这个方法里面拦截到跳转的url,然后做处理
- (bool)webview:(uiwebview *)webview shouldstartloadwithrequest:(nsurlrequest *)request navigationtype:(uiwebviewnavigationtype)navigationtype
{
    //点击web页面的图片调用如下代码
    nsstring *url = [[request url] absolutestring];
    nsstring *protocolname = @"jscallbackoc://";

    if ( [url hasprefix:protocolname]) {
        [self performjsmethodwithurl:url protocolname:protocolname performviewcontroller:self ];
        return no;
    }
    return yes;

}


-(void)injectjs:(uiwebview *)webview{
    //js方法遍历图片添加点击事件 返回图片个数
    static  nsstring * const jsgetimages =
    @"function getimages(){\
    var objs = document.getelementsbytagname(\"img\");\
    for(var i=0;i<objs.length;i&#43;&#43;){\ objs[i].onclick="function(){\" document.location.href="\&quot;jscallbackoc://saveimage_*\&quot;+this.src;\" };\="" return="" objs.length;\="" };";="" [webview="" stringbyevaluatingjavascriptfromstring:jsgetimages];="" 注入js方法="" 注入自定义的js方法后别忘了调用="" 否则不会生效="" stringbyevaluatingjavascriptfromstring:@"getimages()"];="" 调用js方法="" 禁用用户选择="" stringbyevaluatingjavascriptfromstring:@"document.documentelement.style.webkituserselect="none" ;"];="" 禁用长按弹出框="" stringbyevaluatingjavascriptfromstring:@"document.documentelement.style.webkittouchcallout="none" }="" -="" (void)performjsmethodwithurl:(nsstring="" *)url="" protocolname:(nsstring="" *)name="" performviewcontroller:(uiviewcontroller="" *)viewcontroller="" {="" *="" 跳转url="" :="" jscallbackoc:="" sendmessage_number2_number3_*100$100$99="" protocolname:="" 方法名:="" sendmessage:number2:number3="" 参数:="" 100,100,99="" 方法名和参数组合为oc的方法为:-="" (void)sendmessage:(nsstring="" *)number="" number2:(nsstring="" *)number2="" number3:(nsstring="" *)number3="" 获得协议后面的路径为:="" sendmessage_number2_*200$300="" nsstring="" *path="[url" substringfromindex:name.length];="" 利用“*”切割路径,“*”前面是方法名,后面是参数="" nsarray="" *subpaths="[path" componentsseparatedbystring:@"*"];="" 方法名="" methodname="=" sendmessage:number2:="" 下面的方法是把"_"替换为"?',="" js返回的url里面把“:”直接省略了,只能在js里面使用“_”来表示,然后在oc里面再替换回“:”="" *methodname="[[subpaths" firstobject="" ]="" stringbyreplacingoccurrencesofstring:@"_"="" withstring:@":"];="" 参数="" 200$300,每个参数之间使用符号$链接(避免和url里面的其他字符混淆,因为一般url里面不会出现这个字符)="" *params="nil;" if="" (subpaths.count="=" 2)="" params="[[subpaths" lastobject]="" componentsseparatedbystring:@"$"];="" nslog(@"方法名:%@-----参数:%@",="" methodname,params);="" 调用nsobject类扩展方法,传递多个参数="" [viewcontroller="" performselector:nsselectorfromstring(methodname)="" withobjects:params];="" -(void)saveimage:(nsstring="" *)imageurl="" nslog(@"获取到图片地址:%@",imageurl);="" @end

总结:

方法1比较简洁,但是仅仅只能获取图片。

方法二比较繁琐,但是使用范围更广,不光可以保存图片,还可以执行任意的oc代码。

大家酌情选择。