search
LoginSignup
3
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

はじめに

株式会社ピー・アール・オーのアドベントカレンダー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はサーバエンジニアだけでなく、ちゃんとスマホエンジニアを巻き込んで設計しましょう。

参考サイト

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
What you can do with signing up
3
Help us understand the problem. What are the problem?