WKWebViewをSwiftUIでラップしたら挙動不審になった。
挙動不審な様子
WKWebView
をUIViewRepresentable
でラップするベストプラクティスがわかりません。
SwiftUIの描画更新サイクルとWKWebViewの持つ描画更新サイクルがうまく噛み合わないので、ある特定のページをロードするだけの利用ではなく、ユーザーがロードするページを操作するような場合は描画更新が無限ループして挙動不審になりました。どうすればいいのか。
構成
.
├── Extensions
│ └── String+Extension.swift
├── BrowserApp.swift
└── View
├── NetSurfingView.swift
├── SearchBar.swift
├── ToolBar.swift
└── WebContentView.swift
WebContentView.swift
import SwiftUI
import WebKit
import Combine
struct WebContentView: UIViewRepresentable {
enum Action {
case none
case goBack
case goForward
case refresh
case search(String)
}
@Binding private var action: Action
@Binding private var canGoBack: Bool
@Binding private var canGoForward: Bool
@Binding private var estimatedProgress: Double
@Binding private var progressOpacity: Double
let webView: WKWebView
init(
action: Binding<Action>,
canGoBack: Binding<Bool>,
canGoForward: Binding<Bool>,
estimatedProgress: Binding<Double>,
progressOpacity: Binding<Double>
) {
self._action = action
self._canGoBack = canGoBack
self._canGoForward = canGoForward
self._estimatedProgress = estimatedProgress
self._progressOpacity = progressOpacity
webView = WKWebView()
webView.allowsBackForwardNavigationGestures = true
webView.allowsLinkPreview = false
NSLog("🌟🐮")
}
func makeUIView(context: Context) -> WKWebView {
NSLog("🌟🐝")
webView.uiDelegate = context.coordinator
webView.navigationDelegate = context.coordinator
return webView
}
func makeCoordinator() -> WebViewCoordinator {
return WebViewCoordinator(contentView: self)
}
func updateUIView(_ webView: WKWebView, context: Context) {
NSLog("🌟🐷")
switch action {
case .none:
break
case .goBack:
if webView.canGoBack {
webView.goBack()
}
case .goForward:
if webView.canGoForward {
webView.goForward()
}
case .refresh:
webView.reload()
case .search(let searchText):
context.coordinator.search(text: searchText)
}
action = .none
}
final class WebViewCoordinator: UIViewController {
let contentView: WebContentView
private var cancellables = Set<AnyCancellable>()
init(contentView: WebContentView) {
self.contentView = contentView
super.init(nibName: nil, bundle: nil)
contentView.webView
.publisher(for: \.estimatedProgress)
.sink { value in
contentView.estimatedProgress = value
}
.store(in: &cancellables)
contentView.webView
.publisher(for: \.isLoading)
.sink { value in
if value {
contentView.estimatedProgress = 0
contentView.progressOpacity = 1
} else {
contentView.progressOpacity = 0
}
}
.store(in: &cancellables)
contentView.webView
.publisher(for: \.canGoBack)
.assign(to: \.canGoBack, on: contentView)
.store(in: &cancellables)
contentView.webView
.publisher(for: \.canGoForward)
.assign(to: \.canGoForward, on: contentView)
.store(in: &cancellables)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func openURL(urlString: String) {
if let url = URL(string: urlString) {
contentView.webView.load(URLRequest(url: url))
}
}
func search(text: String) {
if text.isEmpty {
openURL(urlString: "https://www.google.com")
} else if text.match(pattern: #"^[a-zA-Z]+://"#) {
openURL(urlString: text)
} else if let encoded = text.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) {
let urlString = "https://www.google.com/search?q=\(encoded)"
openURL(urlString: urlString)
}
}
}
}
// MARK: - WKUIDelegate
extension WebContentView.WebViewCoordinator: WKUIDelegate {
// Alert
func webView(
_ webView: WKWebView,
runJavaScriptAlertPanelWithMessage message: String,
initiatedByFrame frame: WKFrameInfo
) async {
return await withCheckedContinuation { continuation in
let alertController = UIAlertController(title: nil,
message: message,
preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default) { _ in
continuation.resume()
}
alertController.addAction(okAction)
self.present(alertController, animated: true, completion: nil)
}
}
// Confirm
func webView(
_ webView: WKWebView,
runJavaScriptConfirmPanelWithMessage message: String,
initiatedByFrame frame: WKFrameInfo
) async -> Bool {
return await withCheckedContinuation { continuation in
let alertController = UIAlertController(title: nil,
message: message,
preferredStyle: .alert)
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { _ in
continuation.resume(returning: false)
}
alertController.addAction(cancelAction)
let okAction = UIAlertAction(title: "OK", style: .default) { _ in
continuation.resume(returning: true)
}
alertController.addAction(okAction)
self.present(alertController, animated: true, completion: nil)
}
}
// Prompt
func webView(
_ webView: WKWebView,
runJavaScriptTextInputPanelWithPrompt prompt: String,
defaultText: String?, initiatedByFrame frame: WKFrameInfo
) async -> String? {
return await withCheckedContinuation { continuation in
let alertController = UIAlertController(title: nil,
message: prompt,
preferredStyle: .alert)
alertController.addTextField { textField in
textField.text = defaultText
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { _ in
continuation.resume(returning: nil)
}
alertController.addAction(cancelAction)
let okAction = UIAlertAction(title: "OK", style: .default) { _ in
if let result = alertController.textFields?.first?.text {
continuation.resume(returning: result)
} else {
continuation.resume(returning: "")
}
}
alertController.addAction(okAction)
self.present(alertController, animated: true, completion: nil)
}
}
}
// MARK: - WKNavigationDelegate
extension WebContentView.WebViewCoordinator: WKNavigationDelegate {
func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction
) async -> WKNavigationActionPolicy {
guard let requestURL = navigationAction.request.url else {
return .cancel
}
switch requestURL.scheme {
case "http", "https":
return .allow
default:
UIApplication.shared.open(requestURL, options: [:]) { result in
NSLog("🌟🐙 \(result)")
}
return .cancel
}
}
}
その他のコード
MinBrowserApp.swift
import SwiftUI
@main
struct MinBrowserApp: App {
var body: some Scene {
WindowGroup {
NetSurfingView()
}
}
}
NetSurfingView.swift
import SwiftUI
struct NetSurfingView: View {
@State private var inputText: String = ""
@State private var action: WebContentView.Action = .none
@State private var canGoBack: Bool = false
@State private var canGoForward: Bool = false
@State private var estimatedProgress = 0.0
@State private var progressOpacity = 1.0
var body: some View {
VStack(spacing: 0) {
SearchBar(inputText: $inputText)
.onSubmit {
action = .search(inputText)
}
ProgressView(value: estimatedProgress)
.opacity(progressOpacity)
WebContentView(action: $action,
canGoBack: $canGoBack,
canGoForward: $canGoForward,
estimatedProgress: $estimatedProgress,
progressOpacity: $progressOpacity)
ToolBar(action: $action,
canGoBack: $canGoBack,
canGoForward: $canGoForward)
}
.onOpenURL(perform: { url in
if let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let queryItem = components.queryItems?.first(where: { $0.name == "url" }),
let queryURL = queryItem.value {
NSLog("🌟 \(queryURL)")
action = .search(queryURL)
}
})
}
}
SearchBar.swift
import SwiftUI
struct SearchBar: View {
@Binding var inputText: String
var body: some View {
ZStack {
Rectangle()
.foregroundColor(Color("SearchBar"))
HStack(spacing: 4) {
Image(systemName: "magnifyingglass")
TextField("Search ..", text: $inputText)
}
.foregroundColor(Color.gray)
.padding(.leading, 8)
}
.frame(height: 36)
.cornerRadius(10)
.padding(.vertical, 8)
.padding(.horizontal, 16)
}
}
ToolBar.swift
struct ToolBar: View {
@Binding var action: WebContentView.Action
@Binding var canGoBack: Bool
@Binding var canGoForward: Bool
var body: some View {
VStack(spacing: 0) {
Divider()
.background(Color("ToolBarBorder"))
HStack {
Button {
action = .goBack
} label: {
Image(systemName: "chevron.backward")
.imageScale(.large)
.frame(width: 40, height: 40, alignment: .center)
}
.disabled(!canGoBack)
Button {
action = .goForward
} label: {
Image(systemName: "chevron.forward")
.imageScale(.large)
.frame(width: 40, height: 40, alignment: .center)
}
.disabled(!canGoForward)
Spacer()
}
.padding(.vertical, 8)
.padding(.horizontal, 16)
.background(Color("ToolBar"))
}
}
}
String+Extension.swift
import Foundation
extension String {
func match(pattern: String) -> Bool {
let matchRange = self.range(of: pattern, options: .regularExpression)
return matchRange != nil
}
}
Author And Source
この問題について(WKWebViewをSwiftUIでラップしたら挙動不審になった。), 我々は、より多くの情報をここで見つけました https://zenn.dev/kyome/articles/0e7ec77d73167b著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Collection and Share based on the CC protocol