はじめに
株式会社ピー・アール・オーのアドベントカレンダー4日目です。
今まで使えたUIWebView、いよいよ2020年12月で利用できなくなります。つい最近までUIWebViewからWKWebViewへの移行対応をやりましたが、WebAPIと一緒に返ってきたSession Cookieを使ってWebページを表示していて、WKWebViewに移行するとNSHttpCookieStorageからCookieを自動的に取れなくて、その対応についての調査は結構時間かかりました。
そこで、WKWebViewに移行するとき、Session Cookieをセットする方法の記事があったらいいなと思って、この記事を書きました。
WKProcessPoolを用意する
さて、一番最初に用意しないといけないのは、WKProcessPoolのインスタンスです。これを利用して、WKWebViewを作成時に使われる設定値に設定します。複数WebViewで同じドメインのWebページを表示したい時に、全てのWebViewが同じWKProcessPoolのインスタンスを参照しないといけないです。それぞれのWebViewが独自のWKProcessPoolを持つと、WebViewにCookieをセットするとき、コンフリクトが発生する可能性があって、Cookieをセット失敗になるので、WKProcessPoolインスタンスを作るとき、常にアプリの同じ所を参照するようにします。
/// WKWebViewUtil.m
static WKProcessPool *processPool = nil;
+ (WKProcessPool *)sharedProcessPool {
static dispatch_once_t once;
dispatch_once(&once, ^{
processPool = [[WKProcessPool alloc] init];
});
return processPool;
}
WKWebViewを初期化する
次はWebViewの初期化です。
WebViewが初期化される前に用意したWKProcessPoolのインスタンスをセットしないといけないので、今回のWebView作成はStroyboardではなく、全てコードで生成します。
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
configuration.processPool = [WKWebViewUtil sharedProcessPool];
WKWebView *webView = [[WKWebView alloc] initWithFrame:frame configuration:configuration];
WKWebViewにCookieを渡す
WKWebViewの初期化が終わったので、次は作られたWebViewにCookieをセットします。
UIWebViewと違い、WKWebViewは自分自身の内部に持っていたWKHTTPCookieStoreの方でCookieを管理していたため、手動でNSHTTPCookieStorageに保存したSession CookieをWKHTTPCookieStoreに渡さないといけません。
WKWebViewにCookieをセットする処理は非同期のため、Webページ表示用に必要なCookieを全てセット完了後Webページを表示する必要があるため、全てのCookieをWKWebViewにセット完了のチェックが必要になります。
NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (NSHTTPCookie *cookie in cookieStorage.cookies) {
// Cookieをセットする
[webView.configuration.websiteDataStore.httpCookieStore] setCookie:cookie completionHandler:^{
// Cookieセット処理は非同期になるので、ここで全部のCookieがセット完了チェックが必要
}];
}
これだけだと、連続的に同じWebViewのインスタンスを作るとき、WebViewにCookieセット完了のハンドラーが呼ばれないことがあって、WebViewにCookieセット完了したかどうかは不明な状態になるので、WKHTTPCookieStore setCookie: completionHandler:
を呼んだ後、下記の処理を追加して、強制的にCookieセット完了後ハンドラーが呼ばれるようにする必要があります。
// Cookieセット完了後必ずcompletionHandlerが呼ばれるように追加する処理
[[webView.configuration.websiteDataStore] fetchDataRecordsOfTypes:[NSSet<NSString *> setWithObject:WKWebsiteDataTypeCookies] completionHandler:^(NSArray<WKWebsiteDataRecord *> *records) {}];
初期化したWKWebViewを親のViewに追加する
WebViewが用意されたとで、次は画面上に表示するため、親のViewに追加する必要があります。ViewControllerのviewDidLoad
に作成したWebViewを追加します。
[self.view addSubview:webview];
NSURLRequestのヘッダーに同じCookieをセットする
Webページを表示する用のWebViewは準備できましたが、しかし今の状態はまだ画面が正しく表示できません。
画面表示する前に、URLRequestのヘッダーにもWebViewにセットしたCookieをセットしないといけません。
NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:url];
// WebViewにセットしたCookieをリクエストヘッダーにもセットする
NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
NSDictionary *headerFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookieStorage.cookies];
[urlRequest setAllHTTPHeaderFields:headerFields];
// WebViewを表示する
[webView loadRequest:urlRequest];
これで、UIWebViewと同じように、WebAPIと一緒に返ってきたSession Cookieを利用して、WKWebViewも認証必要なWebページ画面が正しく表示できます。
完成版のコード
/// WKWebViewUitl.m
static WKProcessPool *processPool = nil;
+ (WKProcessPool *)sharedProcessPool {
static dispatch_once_t once;
dispatch_once(&once, ^{
processPool = [[WKProcessPool alloc] init];
});
return processPool;
}
+ (void)initWebViewWithFrame:(CGRect)frame
completionHandler:(void (^)(WKWebView *))handler {
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
configuration.processPool = [WKWebViewUtil sharedProcessPool];
WKWebView *webView = [[WKWebView alloc] initWithFrame:frame configuration:configuration];
NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
if ([cookieStorage.cookies count] <= 0) {
// Cookieは存在しない場合、そのまま返す
handler(webView);
}
__block long setCookieCount = 0;
for (NSHTTPCookie *cookie in cookieStorage.cookies) {
// Cookieをセットする
[webView.configuration.websiteDataStore.httpCookieStore] setCookie:cookie completionHandler:^{
setCookieCount += 1;
if (setCookieCount == [cookieStorage.cookies count]) {
handle(webView);
}
}];
[[webView.configuration.websiteDataStore] fetchDataRecordsOfTypes:[NSSet<NSString *> setWithObject:WKWebsiteDataTypeCookies] completionHandler:^(NSArray<WKWebsiteDataRecord *> *records) {}];
}
}
/// ViewController.m
@property (strong, nonatomic) WKWebView *webView;
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
[WKWebViewUtil initWebViewWithFrame:self.view.bounds completionHandler:^(WKWebView *)webView {
weakSelf.webView = webView;
[weakSelf.view addSubview: weakSelf.webView];
NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:url];
// WebViewにセットしたCookieをリクエストヘッダーにもセットする
NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
NSDictionary *headerFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookieStorage.cookies];
[urlRequest setAllHTTPHeaderFields:headerFields];
// WebViewを表示する
[weakSelf.webView loadRequest:urlRequest];
}];
}
最後に
- 今回WKWebViewにSession Cookieを渡して、Webページを正しく表示するために結構大変でした。やっぱり今後はWebAPIで返って来たSession Cookieを使ってWebページを表示するのはやめましょう。
- たとえまだ使えるだとしても、レガシーな設計やコードは放っておくと、いずれ使えなくなるので、今後技術的な負債に繋がる可能性は大いにあるから、可能なら早めに修正する方向で持っていきましょう。
- スマホアプリと連携するWebサービス・WebAPIはサーバエンジニアだけでなく、ちゃんとスマホエンジニアを巻き込んで設計しましょう。