【问题标题】:iOS WebView remote html with local image files带有本地图像文件的 iOS WebView 远程 html
【发布时间】:2011-07-31 03:28:44
【问题描述】:

以前有人问过类似的问题,但我一直找不到解决方案。

这是我的情况 - 我的 UIWebView 加载了一个远程 html 页面。网页中使用的图像在构建时是已知的。为了让页面加载更快,我想在iOS应用中打包图片文件,运行时替换掉。

[请注意,html 是远程的。我总是得到从本地加载 html 和图像文件的答案 - 我已经这样做了]

我得到的最接近的建议是在 html 页面和 iOS 应用程序中使用自定义 url 方案,例如 myapp://images/img.png,用 NSURLProtocol 子类拦截 myapp:// URL 并替换图像带有本地图像。理论上听起来不错,但我还没有找到一个完整的代码示例来证明这一点。

我有 Java 背景。我可以使用自定义内容提供程序轻松地为 Android 做到这一点。我确信 iOS/Objective-C 必须存在类似的解决方案。我没有足够的 Objective-C 经验,无法在短时间内自己解决。

任何帮助将不胜感激。

【问题讨论】:

    标签: iphone html image uiwebview nsurlprotocol


    【解决方案1】:

    好的,这里是一个示例,如何继承 NSURLProtocol 并传递一个已经在包中的图像 (image1.png)。下面是子类的标题、实现以及如何在 viewController(不完整的代码)和本地 html 文件(可以很容易地与远程文件交换)中使用它的示例。我已经调用了自定义协议:myapp://,您可以在底部的 html 文件中看到。

    感谢您的提问!我自己问了很长时间,弄清楚这一点所花费的时间每一秒都是值得的。

    编辑: 如果有人让我的代码在当前 iOS 版本下运行有困难,请查看 sjs 的答案。当我回答这个问题时,它正在工作。他指出了一些有用的补充并纠正了一些问题,所以也给他一些支持。

    这是它在我的模拟器中的样子:

    MyCustomURLProtocol.h

    @interface MyCustomURLProtocol : NSURLProtocol
    {
        NSURLRequest *request;
    }
    
    @property (nonatomic, retain) NSURLRequest *request;
    
    @end
    

    MyCustomURLProtocol.m

    #import "MyCustomURLProtocol.h"
    
    @implementation MyCustomURLProtocol
    
    @synthesize request;
    
    + (BOOL)canInitWithRequest:(NSURLRequest*)theRequest
    {
        if ([theRequest.URL.scheme caseInsensitiveCompare:@"myapp"] == NSOrderedSame) {
            return YES;
        }
        return NO;
    }
    
    + (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest*)theRequest
    {
        return theRequest;
    }
    
    - (void)startLoading
    {
        NSLog(@"%@", request.URL);
        NSURLResponse *response = [[NSURLResponse alloc] initWithURL:[request URL] 
                                                            MIMEType:@"image/png" 
                                               expectedContentLength:-1 
                                                    textEncodingName:nil];
    
        NSString *imagePath = [[NSBundle mainBundle] pathForResource:@"image1" ofType:@"png"];  
        NSData *data = [NSData dataWithContentsOfFile:imagePath];
    
        [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
        [[self client] URLProtocol:self didLoadData:data];
        [[self client] URLProtocolDidFinishLoading:self];
        [response release];
    }
    
    - (void)stopLoading
    {
        NSLog(@"something went wrong!");
    }
    
    @end
    

    MyCustomProtocolViewController.h

    @interface MyCustomProtocolViewController : UIViewController {
        UIWebView *webView;
    }
    
    @property (nonatomic, retain) UIWebView *webView;
    
    @end
    

    MyCustomProtocolViewController.m

    ...
    
    @implementation MyCustomProtocolViewController
    
    @synthesize webView;
    
    - (void)awakeFromNib
    {
        self.webView = [[[UIWebView alloc] initWithFrame:CGRectMake(20, 20, 280, 420)] autorelease];
        [self.view addSubview:webView];
    }
    
    - (void)viewDidLoad
    {   
        // ----> IMPORTANT!!! :) <----
        [NSURLProtocol registerClass:[MyCustomURLProtocol class]];
    
        NSString * localHtmlFilePath = [[NSBundle mainBundle] pathForResource:@"file" ofType:@"html"];
    
        NSString * localHtmlFileURL = [NSString stringWithFormat:@"file://%@", localHtmlFilePath];
    
        [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:localHtmlFileURL]]];
    
        NSString *html = [NSString stringWithContentsOfFile:localHtmlFilePath encoding:NSUTF8StringEncoding error:nil]; 
    
        [webView loadHTMLString:html baseURL:nil];
    }
    

    file.html

    <html>
    <body>
        <h1>we are loading a custom protocol</h1>
        <b>image?</b><br/>
        <img src="myapp://image1.png" />
    <body>
    </html>
    

    【讨论】:

    • 太棒了!看起来正是我想要的。我会试试这个,让你知道。
    • 为什么request被声明为子类的属性? NSURLProtocol 已经有一个request 属性,所以你应该只使用self.request。在上面的代码中request 总是nil
    • @sjs 好点,我不能告诉你我在介绍该属性时的想法,但是没有造成任何伤害,因为该示例当时运行良好。不,请求不是零,这是 NSURLProtocol 的注册子类。查看静态 registerClass 方法的文档。
    • @NickWeaver 我已阅读文档。 request 从未被分配,而是nil。我正在使用您的代码,为了使其正常工作,我必须使用属性self.request。今天试试这个代码,它不起作用。即使它确实有效,一个未使用的 ivar 也应该被删除。如果您阅读文档,您还会看到 -[NSURLProtocol stopLoading] 不是错误条件。您不应该记录“出了点问题!”当这是成功请求周期的常规部分时。
    • @Krutarth Patel 不直接。您必须在 html 中添加一个按钮/链接,以触发加载某个 URL。然后,您将不得不在 startLoading 或 UIWebViewDelegate 方法之一中拦截它,例如 - webViewDidStartLoad:.
    【解决方案2】:

    NSURLProtocolUIWebView 的一个不错的选择,但是直到现在 WKWebView 仍然不支持它。对于WKWebView,我们可以构建一个本地HTTP服务器来处理本地文件请求,GCDWebServer很适合这个:

    self.webServer = [[GCDWebServer alloc] init];
    
    [self.webServer addDefaultHandlerForMethod:@"GET"
                                  requestClass:[GCDWebServerRequest class]
                                  processBlock:
     ^GCDWebServerResponse *(GCDWebServerRequest *request)
    {
        NSString *fp = request.URL.path;
    
        if([[NSFileManager defaultManager] fileExistsAtPath:fp]){
            NSData *dt = [NSData dataWithContentsOfFile:fp];
    
            NSString *ct = nil;
            NSString *ext = request.URL.pathExtension;
    
            BOOL (^IsExtInSide)(NSArray<NSString *> *) = ^(NSArray<NSString *> *pool){
                NSUInteger index = [pool indexOfObjectWithOptions:NSEnumerationConcurrent
                                                      passingTest:^BOOL(NSString *obj, NSUInteger idx, BOOL *stop) {
                                                          return [ext caseInsensitiveCompare:obj] == NSOrderedSame;
                                                      }];
                BOOL b = (index != NSNotFound);
                return b;
            };
    
            if(IsExtInSide(@[@"jpg", @"jpeg"])){
                ct = @"image/jpeg";
            }else if(IsExtInSide(@[@"png"])){
                ct = @"image/png";
            }
            //else if(...) // other exts
    
            return [GCDWebServerDataResponse responseWithData:dt contentType:ct];
    
        }else{
            return [GCDWebServerResponse responseWithStatusCode:404];
        }
    
    }];
    
    [self.webServer startWithPort:LocalFileServerPort bonjourName:nil];
    

    指定本地文件的文件路径时,添加本地服务器前缀:

    NSString *fp = [[NSBundle mainBundle] pathForResource:@"picture" ofType:@"jpg" inDirectory:@"www"];
    NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://127.0.0.1:%d%@", LocalFileServerPort, fp]];
    NSString *str = url.absoluteString;
    [self.webViewController executeJavascript:[NSString stringWithFormat:@"updateLocalImage('%@')", str]];
    

    【讨论】:

      【解决方案3】:

      希望我能正确理解您的问题:

      1) 加载远程网页...和

      2) 用 app/build 中的文件替换某些远程资产

      对吗?


      好吧,我正在做的事情如下(由于 Mobile Safari 上的缓存限制为 5MB,我将它用于视频,但我认为任何其他 DOM 内容都应该同样有效):


      • 创建一个带有样式标签的本地(使用 Xcode 编译)HTML 页面,用于替换应用内/构建内容,设置为隐藏,例如:

      <div style="display: none;">
      <div id="video">
          <video width="614" controls webkit-playsinline>
                  <source src="myvideo.mp4">
          </video>
      </div>
      </div> 
      


      • 在同一个文件中提供一个内容 div,例如

      <div id="content"></div>
      


      •(此处使用 jQuery)从远程服务器加载实际内容并将本地(Xcode 导入的资产)附加到目标 div,例如

      <script src="jquery.js"></script>
      <script>
          $(document).ready(function(){
              $("#content").load("http://www.yourserver.com/index-test.html", function(){
                     $("#video").appendTo($(this).find("#destination"));           
              });
      
          });
      </script>
      


      • 将 www 文件(index.html / jquery.js / 等...使用根级别进行测试)放入项目并连接到目标


      • 远程 HTML 文件(此处位于 yourserver.com/index-test.html)具有

      <base href="http://www.yourserver.com/">
      


      • 以及目标 div,例如

      <div id="destination"></div>
      


      • 最后在您的 Xcode 项目中,将本地 HTML 加载到 Web 视图中

      self.myWebView = [[UIWebView alloc]init];
      
      NSURL *baseURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
      NSString *path = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"];
      NSString *content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
      [self.myWebView loadHTMLString:content baseURL:baseURL];
      

      对我来说是一种享受,最好与 https://github.com/rnapier/RNCachingURLProtocol 结合使用,用于离线缓存。希望这可以帮助。 F

      【讨论】:

        【解决方案4】:

        Nick Weaver 的想法是正确的,但他的答案中的代码不起作用。它也打破了一些命名约定,永远不要使用 NS 前缀命名您自己的类,并遵循标识符名称中的首字母缩写词(例如 URL)大写的约定。为了便于理解,我会坚持使用他的名字。

        这些变化是微妙但重要的:丢失未分配的request ivar,而是参考NSURLProtocol 提供的实际请求,它工作正常。

        NSURLProtocolCustom.h

        @interface NSURLProtocolCustom : NSURLProtocol
        @end
        

        NSURLProtocolCustom.m

        #import "NSURLProtocolCustom.h"
        
        @implementation NSURLProtocolCustom
        
        + (BOOL)canInitWithRequest:(NSURLRequest*)theRequest
        {
            if ([theRequest.URL.scheme caseInsensitiveCompare:@"myapp"] == NSOrderedSame) {
                return YES;
            }
            return NO;
        }
        
        + (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest*)theRequest
        {
            return theRequest;
        }
        
        - (void)startLoading
        {
            NSLog(@"%@", self.request.URL);
            NSURLResponse *response = [[NSURLResponse alloc] initWithURL:self.request.URL 
                                                                MIMEType:@"image/png" 
                                                   expectedContentLength:-1 
                                                        textEncodingName:nil];
        
            NSString *imagePath = [[NSBundle mainBundle] pathForResource:@"image1" ofType:@"png"];  
            NSData *data = [NSData dataWithContentsOfFile:imagePath];
        
            [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
            [[self client] URLProtocol:self didLoadData:data];
            [[self client] URLProtocolDidFinishLoading:self];
            [response release];
        }
        
        - (void)stopLoading
        {
            NSLog(@"request cancelled. stop loading the response, if possible");
        }
        
        @end
        

        Nick 代码的问题在于NSURLProtocol 的子类不需要存储请求。 NSURLProtocol 已经有请求,可以通过-[NSURLProtocol request] 方法或同名属性访问。由于从未分配过原始代码中的request ivar,因此始终为nil(如果已分配,则应该已在某处发布)。该代码不能也不起作用。

        其次,我建议在创建响应之前读取文件数据并将[data length] 作为预期的内容长度而不是-1。

        最后,-[NSURLProtocol stopLoading] 不一定是错误,它只是意味着您应该在可能的情况下停止响应工作。用户可能已取消它。

        【讨论】:

        • 感谢这次重要的改进!我按照你说的做了,效果很好。
        • 有没有办法让您的应用程序提供移动 Safari 或其他应用程序的 UIWebView 中加载的网页所请求的图像?这种方法行得通吗?
        • @Danny 我不这么认为。我的理解是不能拦截 http 或 https 请求。
        • 这个解决方案似乎停止与 ios8 一起工作。 cacheStoragePolicies 是否随着 iOS8 的变化而变化?
        【解决方案5】:

        诀窍是为现有 HTML 提供显式的基本 URL。

        将 HTML 加载到 NSString 中,使用 UIWebView 的 loadHTMLString: baseURL: 和 URL 作为基础。要将 HTML 加载到字符串中,可以使用 [NSString stringWithContentsOfURL],但这是一种同步方法,连接速度较慢时会冻结设备。使用异步请求加载 HTML 也是可能的,但涉及更多。阅读NSURLConnection

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-10-04
          • 1970-01-01
          • 2011-02-08
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多