96
93

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2013-01-20

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

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

いろいろ調査していて、URLローディングシステムを利用すれば、ステータスをチェックして制御を行えることがわかりました。
URL Loading System Programming Guide

The URL loading system class hierarchy

上の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羨ましいっす

##参考

96
93
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
96
93

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?