UIWebViewは簡単にWebページを表示できるのですが、
HTTPステータスをチェックしてiOS側で制御を行う機能がありません。
自分でNSURLConnectionを使ってリクエストして、UIWebViewに読みこませればできないこともないのですが、
loadRequestで改めて読み直すと2重リクエストになってしまったり、loadDataを使ってしまうと、ブラウザ機能の戻る/進むを使えなくなってしまいます。
いろいろ調査していて、URLローディングシステムを利用すれば、ステータスをチェックして制御を行えることがわかりました。
URL Loading System Programming Guide
上のProtocol Supportの2つのクラスを使えば、HTTPステータスのチェックが可能になります。
また、URLProtocolClientのURLProtocol:didFailWithError:を呼び出せば、UIWebViewDelegateのwebView:didFailLoadWithError:が呼び出されるので、エラーハンドリング用のメソッドも特別に追加する必要もありません。
最終的に下のような感じになりました。
-
canInitWithRequest:
でYESを返すと、自分で実装したURLProtocolのなかで処理 - 処理するURLは、画面遷移ページのみ(拡張子あり/なしで判別しているので、URL RoutingをサポートしているWeb Frameworkを使う想定)
- リクエストループを防止する仕組みを追加(リクエストヘッダーを追加する等)
-
initWithRequest:cachedResponse:client:
で初期化時に、ループ防止用のリクエストヘッダーを追加 -
startLoading
で実際にリクエストを行いHTTPステータスをチェック
#import <Foundation/Foundation.h>
@interface MyURLProtocol : NSURLProtocol
@end
#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羨ましいっす
##参考