【SwiftUI】WKWebViewをSwiftUIで使う

# はじめに
SwiftUIも便利になってきましたが、WebViewはいまだに対応されていません。
今回はSwiftUIでWKWebViewを使用する方法を紹介します。

# やりたいこと
- カスタムユーザーエージェントを設定
- WKWebViewConfigurationを使ってJavaScriptを実行
- URLを監視

この辺りができれば、UIKitで使用する時と同等のことができるのではないでしょうか

# 参考
ベースはこちらのライブラリです。
`WKWebViewConfiguration`の設定ができなかったのでPRを投げてみました。

https://github.com/danielsaidi/WebViewKit

# 実装
```swift
#if os(iOS)
typealias WebViewRepresentable = UIViewRepresentable
#elseif os(macOS)
typealias WebViewRepresentable = NSViewRepresentable
#endif

#if os(iOS) || os(macOS)
import SwiftUI
import WebKit

public struct WebView: WebViewRepresentable {
    
    // MARK: - Initializers
    public init(
        url: URL? = nil,
        configuration: WKWebViewConfiguration? = nil,
        webView: @escaping (WKWebView) -> Void = { _ in }) {
        self.url = url
        self.configuration = configuration
        self.webView = webView
    }

    // MARK: - Properties
    
    private let url: URL?
    private let configuration: WKWebViewConfiguration?
    private let webView: (WKWebView) -> Void

    // MARK: - Functions
    
    #if os(iOS)
    public func makeUIView(context: Context) -> WKWebView {
        makeView()
    }
    
    public func updateUIView(_ uiView: WKWebView, context: Context) {}
    #endif
    
    #if os(macOS)
    public func makeNSView(context: Context) -> WKWebView {
        makeView()
    }
    
    public func updateNSView(_ view: WKWebView, context: Context) {}
    #endif
}

private extension WebView {

    func makeWebView() -> WKWebView {
        guard let configuration = self.configuration else { return WKWebView() }
        return WKWebView(frame: .zero, configuration: configuration)
    }
    
    func makeView() -> WKWebView {
        let view = makeWebView()
        webView(view)
        tryLoad(url, into: view)
        return view
    }
    
    func tryLoad(_ url: URL?, into view: WKWebView) {
        guard let url = url else { return }
        view.load(URLRequest(url: url))
    }
}
#endif
```

# 使い方
### 基本的な使い方
```diff_swift:ContentView
import SwiftUI

struct ContentView: View {
    @State var isPresentedWebView: Bool = false

    private let url = "https://gs.statcounter.com/detect"
    var body: some View {
        VStack {
            Button {
                isPresentedWebView = true
            } label: {
                Text("Show WebView")
            }
        }
        .sheet(isPresented: $isPresentedWebView) {
+           WebView(url: URL(string: url))
        }
    }
}
```

### カスタムユーザーエージェントの設定
```diff_swift:ContentView
import SwiftUI

struct ContentView: View {
    @StateObject var viewModel = ViewModel()

    @State var isPresentedWebView: Bool = false

    private let url = "https://taishin-miyamoto.com"
    var body: some View {
        VStack {
            Button {
                isPresentedWebView = true
            } label: {
                Text("Show WebView")
            }
        }
        .sheet(isPresented: $isPresentedWebView) {
+           WebView(url: URL(string: url)) { webView in
+               let customUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3864.0 Safari/537.36"
+               webView.customUserAgent = customUserAgent
+           }
        }
    }
}
```

### WKWebViewConfigurationを使ってJavaScriptを実行
JavaScriptは[こちら](https://mushikago.com/i/?p=8385)をサンプルとして使いました
```diff_swift:ContentView
import SwiftUI

struct ContentView: View {
    @StateObject var viewModel = ViewModel()

    @State var isPresentedWebView: Bool = false

    private let url = "https://taishin-miyamoto.com"
    var body: some View {
        VStack {
            Button {
                isPresentedWebView = true
            } label: {
                Text("Show WebView")
            }
        }
        .sheet(isPresented: $isPresentedWebView) {
+           WebView(url: URL(string: url), configuration: viewModel.configuration)
        }
    }
}
```
```diff_swift:ViewModel
import WebKit

final class ViewModel: ObservableObject {
    var configuration: WKWebViewConfiguration {
        let disableSelectionScriptString = "document.documentElement.style.webkitUserSelect='none';"
        let disableCalloutScriptString = "document.documentElement.style.webkitTouchCallout='none';"

        let disableSelectionScript = WKUserScript(source: disableSelectionScriptString, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
        let disableCalloutScript = WKUserScript(source: disableCalloutScriptString, injectionTime: .atDocumentEnd, forMainFrameOnly: true)

        let userContentController = WKUserContentController()
        userContentController.addUserScript(disableSelectionScript)
        userContentController.addUserScript(disableCalloutScript)

        let configuration = WKWebViewConfiguration()
        configuration.userContentController = userContentController
        configuration.ignoresViewportScaleLimits = false

        return configuration
    }
}
```

### URLの監視
Combineを使って監視してみたいと思います。
```diff_swift:ContentView
import SwiftUI

struct ContentView: View {
    @StateObject var viewModel = ViewModel()

    @State var isPresentedWebView: Bool = false

    private let url = "https://taishin-miyamoto.com"
    var body: some View {
        VStack {
            Button {
                isPresentedWebView = true
            } label: {
                Text("Show WebView")
            }
        }
        .sheet(isPresented: $isPresentedWebView) {
            WebView(url: URL(string: url)) { webView in
                viewModel.observeURL(webView: webView)
            }
        }
    }
}
```
```swift:ViewModel
import Combine
import WebKit

final class ViewModel: ObservableObject {
    private var cancellables = Set<AnyCancellable>()

    func observeURL(webView: WKWebView) {
        webView.publisher(for: \.url, options: .new)
            .sink { url in
                guard let url else { return }
                print(url)
            }
            .store(in: &cancellables)
    }
}
```

# おわり
フルSwiftUIで実装できるのはいつになるのでしょうか。。。