iOS web view
Toolchain-checked integration example — validate lifecycle in your app
import Combine
import Foundation
import SwiftUI
import WebKit
@MainActor
private final class PrioSmithFeedbackNavigator: ObservableObject {
weak var webView: WKWebView?
@discardableResult
func goBack() -> Bool {
guard let webView, webView.canGoBack else { return false }
webView.goBack()
return true
}
}
struct PrioSmithFeedbackView: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.openURL) private var openURL
@StateObject private var navigator = PrioSmithFeedbackNavigator()
var body: some View {
PrioSmithFeedbackWebView(
navigator: navigator,
openExternalURL: { target in
_ = openURL(target)
}
)
.navigationBarBackButtonHidden()
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button {
if !navigator.goBack() {
dismiss()
}
} label: {
Label("Back", systemImage: "chevron.backward")
}
.accessibilityIdentifier("priosmith-feedback-back")
}
}
}
}
private struct PrioSmithFeedbackWebView: UIViewRepresentable {
private let portalURL = URL(string: "https://app.priosmith.example/p/PROJECT_SLUG/board")!
let navigator: PrioSmithFeedbackNavigator
let openExternalURL: (URL) -> Void
func makeCoordinator() -> Coordinator {
Coordinator(
portalURL: portalURL,
navigator: navigator,
openExternalURL: openExternalURL
)
}
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView(frame: .zero)
navigator.webView = webView
webView.navigationDelegate = context.coordinator
webView.allowsBackForwardNavigationGestures = true
webView.load(URLRequest(url: portalURL))
return webView
}
func updateUIView(_ webView: WKWebView, context: Context) {}
static func dismantleUIView(_ webView: WKWebView, coordinator: Coordinator) {
coordinator.detach(webView)
webView.navigationDelegate = nil
}
@MainActor
final class Coordinator: NSObject, WKNavigationDelegate {
private let portalURL: URL
private let navigator: PrioSmithFeedbackNavigator
private let openExternalURL: (URL) -> Void
init(
portalURL: URL,
navigator: PrioSmithFeedbackNavigator,
openExternalURL: @escaping (URL) -> Void
) {
self.portalURL = portalURL
self.navigator = navigator
self.openExternalURL = openExternalURL
}
func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping @MainActor @Sendable (WKNavigationActionPolicy) -> Void
) {
if navigationAction.targetFrame?.isMainFrame == false {
decisionHandler(.allow)
return
}
guard let target = navigationAction.request.url else {
decisionHandler(.cancel)
return
}
if hasSameOrigin(target, portalURL) {
decisionHandler(.allow)
return
}
openExternalURL(target)
decisionHandler(.cancel)
}
func detach(_ webView: WKWebView) {
if navigator.webView === webView {
navigator.webView = nil
}
}
private func hasSameOrigin(_ target: URL, _ expected: URL) -> Bool {
guard
let targetScheme = target.scheme?.lowercased(),
let expectedScheme = expected.scheme?.lowercased(),
let targetHost = target.host?.lowercased(),
let expectedHost = expected.host?.lowercased()
else {
return false
}
return targetScheme == expectedScheme &&
targetHost == expectedHost &&
effectivePort(target) == effectivePort(expected)
}
private func effectivePort(_ url: URL) -> Int? {
if let port = url.port { return port }
switch url.scheme?.lowercased() {
case "http": return 80
case "https": return 443
default: return nil
}
}
}
}