開発環境
- Xcode 14.3
- Swift 5.8
背景
WKWebViewを使っていると、JavaScriptとSwiftでやり取りをしたいことがあります。そんな時に調べていると、こういうサンプルコードを良く見ます。
final class SomeViewController: UIViewController { @IBOutlet weak var webView: WKWebView! { didSet { webView.configuration.userContentController.add(self, name: "EventName") } } } extension SomeViewController: WKScriptMessageHandler { func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { if message.name == "EventName" { // JS側で `webkit.messageHandlers.EventName.postMessage(message);` を呼ぶとここが呼ばれる。 } } }
課題
上記コードの
webView.configuration.userContentController.add(self, name: "EventName")
では self
を渡していますがこの self
によって循環参照が発生して、メモリリークします。そのため、SomeViewControllerの画面を閉じたとしてもメモリが解放されず、SomeViewControllerを開けば開くほどメモリを食いつぶしていきます。
解決方法1
使い終わった際にはちゃんと解放してあげましょう。
webView.configuration.userContentController.removeScriptMessageHandler(forName: "EventName")
JavaScriptとSwiftの連携が不要になったタイミングや、viewDidDisappearなどで呼んでおくと良さそうです。
解決方法2
解決方法1では、大量にJSのEvent登録をしたり、removeScriptMessageHandler
を呼ぶタイミングが難しいときに扱いが困難になります。そこで、渡した self
がweakになるように間に1つclassをかませてあげます。
class WKScriptMessageHandlerWithWeakReference: NSObject, WKScriptMessageHandler { private weak var delegate: WKScriptMessageHandler? init(delegate: WKScriptMessageHandler) { self.delegate = delegate super.init() } func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { delegate?.userContentController(userContentController, didReceive: message) } }
使う時はこんな感じ
wkWebView.configuration.userContentController.add(WKScriptMessageHandlerWithWeakReference(self), name: "EventName")
これによってわざわざ removeScriptMessageHandler
を呼ばなくても良くなります。
感想
昔のNotificationCenterやKVOを思い出しました。