UIWebView保存图片

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

在系统自带的Safari浏览器已经实现了该功能,但是iOS开发中我们如果调用UIWebView加载图片,会发现无法使用Safari保存图片的功能的。这就需要我们自己去实现。

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

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

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


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

实现原理:

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

实现代码:


@interface ViewController ()<UIGestureRecognizerDelegate,UIWebViewDelegate>
@property (weak, nonatomic) IBOutlet UIWebView *webView;
@end


@implementation ViewController
- (void)viewDidLoad
{
    self.webView.delegate = self;
    [self.webView  loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://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地址,如果不是就返回空值。

效果展示:

打开的网页如下所示:

image

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

image

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


方法二、遍历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<objs.length;i++){\
    objs[i].onclick=function(){\
    document.location.href=\"jscallbackoc://saveImage_*\"+this.src;\
    };\
    };\
    return objs.length;\
    };";
    
    [webView stringByEvaluatingJavaScriptFromString:jsGetImages];//注入js方法
    
    //注入自定义的js方法后别忘了调用 否则不会生效
    [webView stringByEvaluatingJavaScriptFromString:@"getImages()"];//调用js方法
    
    // 禁用用户选择
    [webView stringByEvaluatingJavaScriptFromString:@"document.documentElement.style.webkitUserSelect='none';"];
    
    // 禁用长按弹出框
    [webView stringByEvaluatingJavaScriptFromString:@"document.documentElement.style.webkitTouchCallout='none';"];
}

上面的方法是遍历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、验证结果

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

image

可以看到oc的方法-(void)saveImage:(NSString *)imageURL被执行了。

6、完整代码

#import "ViewController.h"
#import "NSObject+Extension.h"

@interface ViewController ()<UIGestureRecognizerDelegate,UIWebViewDelegate>
@property (weak, nonatomic) IBOutlet UIWebView *webView;
@end

@implementation ViewController

- (void)viewDidLoad
{
    self.webView.delegate = self;
    [self.webView  loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://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++){\
    objs[i].onclick=function(){\
    document.location.href=\"jscallbackoc://saveImage_*\"+this.src;\
    };\
    };\
    return objs.length;\
    };";
    
    [webView stringByEvaluatingJavaScriptFromString:jsGetImages];//注入js方法
    
    //注入自定义的js方法后别忘了调用 否则不会生效
    [webView stringByEvaluatingJavaScriptFromString:@"getImages()"];//调用js方法
    
    // 禁用用户选择
    [webView stringByEvaluatingJavaScriptFromString:@"document.documentElement.style.webkitUserSelect='none';"];
    
    // 禁用长按弹出框
    [webView 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:   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];
}

-(void)saveImage:(NSString *)imageURL
{
    NSLog(@"获取到图片地址:%@",imageURL);
}


@end


总结:

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

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

大家酌情选择。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容