skip to content
私的歌詞倉庫

WKWebViewをSwiftUIで使ってログイン成功時に呼ばれるイベントハンドラを作成する

/ 8 min read

Updated:
Table of Contents

はじめに

最近SwiftUIに再入門したのですが、色々忘れていて勉強中です🧏

そんなSwiftUIでブラウザ(WKWebView)を使って、以下のような処理を実装しました。

  • モーダル遷移(sheet)でWKWebViewを表示
  • ログイン成功時に
    • 特定のCookieを取得
    • 今開いているWKWebViewを閉じる

というような処理です。@EnvironmentObject を使ってsheetのトグルを共有すれば簡単に解決しそうですが、グローバルに状態を登録すると管理が複雑になるのでできればミニマムに実装したいな〜というのがありました(ブラウザ開閉の粒度でグローバルに登録したら収集がつかなくなりそうという思い…)。

そこで今回はSwiftUIのonAppear のようなイベントハンドラで呼び出せるように、ログイン成功時に呼ばれるonAuthorizedを自作してみます。

LoginWebView(url: URL(string: "ログインするURL")!)
.onAuthorized(action: {
// 色々処理する
})

実装

先にコード全体を載せておきます。

public struct LoginWebView: UIViewRepresentable {
public typealias AuthorizedHandler = (() -> Void)
public var authorizedHandler: AuthorizedHandler?
public let url: URL
private let webView = WKWebView()
public init(url: URL) {
self.url = url
}
public func makeUIView(context: Context) -> WKWebView {
webView.navigationDelegate = context.coordinator
let request = URLRequest(url: url)
webView.load(request)
return webView
}
public func updateUIView(_ uiView: WKWebView, context: Context) { }
public func makeCoordinator() -> Coordinator {
return Coordinator(parent: self)
}
public func onAuthorized(action: @escaping AuthorizedHandler) -> LoginWebView {
var view = self
view.authorizedHandler = action
return view
}
}
extension LoginWebView {
public class Coordinator: NSObject, WKNavigationDelegate {
var parent: LoginWebView
init(parent: LoginWebView) {
self.parent = parent
}
public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
guard let url = webView.url else {
return
}
if url.absoluteString == "ログイン成功時に遷移するURL" {
webView.configuration.websiteDataStore.httpCookieStore.getAllCookies { cookies in
for cookie in cookies {
if cookie.name == "取得したいCookieの名前" {
// ここでCookieを取得する
// SwiftUI側で渡されるonAuthorizedのクロージャを実行
self.parent.authorizedHandler?()
}
}
}
}
}
}
}

今回はログイン画面の名前をLoginWebViewとします。

WKWebViewのパーツがSwiftUIにないのでUIViewのWKWebViewをラップします。そのために、UIViewRepresentableを継承してSwiftUIで扱えるようにしています。

LoginWebViewのメンバ変数はコメント通りです。

/// 毎回型を書くのは面倒なのでtypealiasを宣言
public typealias AuthorizedHandler = (() -> Void)
/// onAuthorizedのクロージャを登録する
public var authorizedHandler: AuthorizedHandler?
/// 開きたいURL
public let url: URL
/// SwiftUIで表示するためにWKWebView
private let webView = WKWebView()
public init(url: URL) {
self.url = url
}

UIViewRepresentableプロトコルを継承したクラスは実装が必須のメソッドがいくつかあるのでそれぞれ実装します。

makeUIView(context: Context) -> WKWebView

public func makeUIView(context: Context) -> WKWebView {
webView.navigationDelegate = context.coordinator
let request = URLRequest(url: url)
webView.load(request)
return webView
}

makeUIViewで初期状態のWKWebViewをセットし、返します。contextはUIViewの情報を持っていて、SwiftUIとUIKitの連携を勝手にいい感じにするようです(多分)。

context.coordinatorには後述するmakeCoordinatorメソッドで返すインスタンスが入ってます。このインスタンスの中にWKNavigationDelegateの処理を入れてCookieなどの制御をしています。

updateUIView(_ uiView: WKWebView, context: Context)

public func updateUIView(_ uiView: WKWebView, context: Context) { }

特にSwiftUI側からなにか変更を加えたいということもないので空っぽです。@State とかで開きたいURLを定義して、動的に変更したいとかがあるなら、ここの実装が必要になってくるかもです。

makeCoordinator() -> Coordinator

public func makeCoordinator() -> Coordinator {
return Coordinator(parent: self)
}

WKNavigationDelegateに準拠したCoordinatorを返すようにしました。ここで返した値がContextのcoordinatorに入ってくるようでした。

onAuthorized(action: @escaping AuthorizedHandler) -> LoginWebView

public func onAuthorized(action: @escaping AuthorizedHandler) -> LoginWebView {
var view = self
view.authorizedHandler = action
return view
}

ここが今回自作するイベントハンドラの処理になります。actionはメソッド外に定義したメンバ変数(authorizedHandler)に代入するので@escaping をつけます。

一旦selfをコピーしないとactionを代入できません(structなので)。

public class Coordinator: NSObject, WKNavigationDelegate {
var parent: LoginWebView
init(parent: LoginWebView) {
self.parent = parent
}
public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
guard let url = webView.url else {
return
}
if url.absoluteString == "ログイン成功時に遷移するURL" {
webView.configuration.websiteDataStore.httpCookieStore.getAllCookies { cookies in
for cookie in cookies {
if cookie.name == "取得したいCookieの名前" {
// ここでCookieを取得する処理
// SwiftUI側で渡されるonAuthorizedのクロージャを実行
self.parent.authorizedHandler?()
}
}
}
}
}
}

Coordinatorクラス

WKNavigationDelegateに準拠したクラスを作ります。この中でCookieを取得する処理などを書きます。メンバ変数で親のViewであるLoginWebViewを定義します(イベントハンドラだけだったらわざわざ親Viewを渡す必要もない気がする)。ここからイベントハンドラを持ってきます。

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!)

public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
guard let url = webView.url else {
return
}
if url.absoluteString == "ログイン成功時に遷移するURL" {
webView.configuration.websiteDataStore.httpCookieStore.getAllCookies { cookies in
for cookie in cookies {
if cookie.name == "取得したいCookieの名前" {
// ここでCookieを取得する処理
// SwiftUI側で渡されるonAuthorizedのクロージャを実行
self.parent.authorizedHandler?()
}
}
}
}
}

webViewメソッド内でブラウザの状態を監視します。今開いているWKWebViewのURLをurl.absoluteString で取得し、ログイン成功時に遷移するURLとマッチしたら、Cookieを取ってイベントハンドラを実行します。取得したCookieはKeychainなどに保存すると良さそうです。

終わりに

WKWebViewをSwiftUIで使いつつ、自作のイベントハンドラを実行するようにしました。これによって、メッソドチェーンでイベント処理を書くことができるためSwiftUIっぽい書き方になりました。

今回は決め打ちのURLに遷移したら~という処理でしたが、SwiftUI側でURLの処理を分けたいときは(() -> Void)((URL) -> Void) にすることで出来ます。

UIViewRepresentableやイベントハンドラを作る方法はなんとなく理解できてきました。これでUIKitとSwiftUIで色々出来そうです!

参考文献