9
3

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 3 years have 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はサーバエンジニアだけでなく、ちゃんとスマホエンジニアを巻き込んで設計しましょう。

参考サイト

9
3
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
9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?