iOSのUIWebViewでHTTPステータスをチェックする


UIWebViewは簡単にWebページを表示できるのですが、
HTTPステータスをチェックしてiOS側で制御を行う機能がありません。

自分でNSURLConnectionを使ってリクエストして、UIWebViewに読みこませればできないこともないのですが、
loadRequestで改めて読み直すと2重リクエストになってしまったり、loadDataを使ってしまうと、ブラウザ機能の戻る/進むを使えなくなってしまいます。

いろいろ調査していて、URLローディングシステムを利用すれば、ステータスをチェックして制御を行えることがわかりました。
[URL Loading System Programming Guide](https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/URLLoadingSystem/URLLoadingSystem.html)

![The URL loading system class hierarchy](https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/URLLoadingSystem/Concepts/art/nsobject_hierarchy.gif)

上のProtocol Supportの2つのクラスを使えば、HTTPステータスのチェックが可能になります。
また、URLProtocolClientのURLProtocol:didFailWithError:を呼び出せば、UIWebViewDelegateのwebView:didFailLoadWithError:が呼び出されるので、エラーハンドリング用のメソッドも特別に追加する必要もありません。

最終的に下のような感じになりました。

- `canInitWithRequest:`でYESを返すと、自分で実装したURLProtocolのなかで処理
- 処理するURLは、画面遷移ページのみ(拡張子あり/なしで判別しているので、URL RoutingをサポートしているWeb Frameworkを使う想定)
- リクエストループを防止する仕組みを追加(リクエストヘッダーを追加する等)
- `initWithRequest:cachedResponse:client:`で初期化時に、ループ防止用のリクエストヘッダーを追加
- `startLoading`で実際にリクエストを行いHTTPステータスをチェック


```MyURLProtocol.h
#import <Foundation/Foundation.h>

@interface MyURLProtocol : NSURLProtocol

@end
```

```MyURLProtocol.m
#import "MyURLProtocol.h"

# ループを防ぐためにHTTPリクエストヘッダーに追加する
static NSString *const MyWebViewResponseCheckHeader = @"X-iOS-WebView-Response-Check";

@interface MyURLProtocol ()

@property (strong, nonatomic, readwrite) NSURLRequest *request;

@end

@implementation MyURLProtocol

/*!
    クラスロード時に、URL Loading Systemに登録してしまう
    UIWebView/NSURLConnectionなどの通信は、このクラスを利用するようになる
 */
+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [NSURLProtocol registerClass:[self class]];
    });
}

/*!
    HTTPステータスをチェックするアプリのホスト名を渡す
    @result アプリホスト名
 */
+ (NSString *)appHost
{
    __strong static NSString *host = nil;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        host = [[NSURL URLWithString:APP_URL_BASE] host];
    });
    return host;
}

#pragma mark - URL Path judgment.

/*!
    URLProtocol内でチェック対象のホスト
 */
+ (NSArray *)canInitHost
{
    __strong static NSArray *hosts = nil;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        hosts = @[[self appHost]];
    });

    return hosts;
}

/*!
    URLProtocol内でチェック対象外のパス
 */
+ (NSArray *)cannotInitPathComponentsForHost:(NSString *)host
{
    __strong static NSDictionary *pathComponentsDict = nil;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        pathComponentsDict = @{
            [self appHost]: @[@"api"],
        };
    });

    return pathComponentsDict[host];
}

#pragma mark - URLProtocol.

/*!
    WebViewでページ遷移するものだけフィルターしてNSURLProtocolで処理する
    UIWebViewのリクエストのみ処理する
    css/js/画像(data:base64;も含む)/APIリクエストは処理しない
 */
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
    NSLog(@"url: %@", request.URL);
    NSLog(@"requesth headers: %@", request.allHTTPHeaderFields);
    NSString *scheme = request.URL.scheme;
    NSString *pathExtension = request.URL.pathExtension;
    NSArray *pathComponents = request.URL.pathComponents;

    // MARK: NSURLProtocolで処理済みならスルー
    if ([request valueForHTTPHeaderField:MyWebViewResponseCheckHeader]) {
        return NO;
    }

    // MARK: schme=http + extension=''なら画面遷移かAPIリクエスト
    if ([scheme isEqualToString:@"http"]
        && [[self canInitHost] containsObject:request.URL.host]
        && pathExtension.length <= 0) {
        // MARK: APIリクエストは、WebViewを使わずにリクエストしていてステータス取得可能なのでスルー
        if ([[self cannotInitPathComponentsForHost:request.URL.host] firstObjectCommonWithArray:pathComponents]) {
            return NO;
        }

        // Web request
        return YES;
    }
    return NO;
}

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
    return request;
}

- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client
{
    self = [super initWithRequest:request cachedResponse:cachedResponse client:client];
    if (self) {
        NSMutableURLRequest *mutableRequest = [request mutableCopy];
        // Loop guard.
        [mutableRequest setValue:@"1" forHTTPHeaderField:MyWebViewResponseCheckHeader];

        self.request = (NSURLRequest *)mutableRequest;
    }
    return self;
}

- (void)startLoading
{
    NSLog(@"request: %@", self.request);
    NSLog(@"request.headers: %@", self.request.allHTTPHeaderFields);

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [NSURLConnection
     sendAsynchronousRequest:self.request
     queue:queue
     completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
        if (error) {
            NSLog(@"reponse error: %@", error);
            // NSURLProtocolClientのdidFailWithErrorを呼び出すと
            // UIWebViewDelegateのwebView:didFailLoadWithError:が呼ばれる(こっちはいつもどおり)
            [self.client URLProtocol:self didFailWithError:error];
            return;
        }

        NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;

        // MARK: UIWebViewの代わりにステータスコードをチェックする
        if (httpResponse.statusCode >= 400) {
            NSLog(@"status error: %d", httpResponse.statusCode);
            // 独自エラーオブジェクトを作成して、NSURLProtocolClientのdidFailWithErrorを呼び出す
            // 同じくUIWebViewDelegateのwebView:didFailLoadWithError:が呼ばれる
            NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"エラーっぽい"};
            NSError *httpStatusError = [NSError errorWithDomain:MyErrorDomain code:MyHTTPResponseError userInfo:userInfo];
            [self.client URLProtocol:self didFailWithError:httpStatusError];
            return;
        }

        // 正常終了
        [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
        [self.client URLProtocol:self didLoadData:data];
        [self.client URLProtocolDidFinishLoading:self];
    }];
}

- (void)stopLoading
{
    NSLog(@"request: %@", self.request);
    NSLog(@"request.headers: %@", self.request.allHTTPHeaderFields);
}

@end
```

##まとめ

WebViewアプリはめんどい…。これだけは、Android羨ましいっす

##参考

- http://blog.livedoor.jp/faulist/archives/1762587.html
- https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/URLLoadingSystem/URLLoadingSystem.html