SwiftUI WKWebView detect url changing - ios

I'm a swift learner. I work with SwiftUI which is a struct, I have to implement a WKWebView and in that, a url is changing dynamically. I have to catch these changing urls, but solutions I have tried are not working.
For example: https://stackoverflow.com/a/48273950/10088243
I tried this code block but it is not working and it gives me some compiler errors:
import SwiftUI
import WebKit
struct ContentView: UIViewRepresentable, WKNavigationDelegate {
let request = URLRequest(url: URL(string: "https://apple.com")!)
func makeUIView(context: Context) -> WKWebView {
let preferences = WKPreferences()
preferences.javaScriptEnabled = true
preferences.javaScriptCanOpenWindowsAutomatically = true
let configuration = WKWebViewConfiguration()
configuration.preferences = preferences
let webView = WKWebView(frame: .zero, configuration: configuration)
webView.allowsBackForwardNavigationGestures = true
return webView
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
// 'override' can only be specified on class membe
if keyPath == #keyPath(WKWebView.url) {
print("### URL:", self.webView.url!)
}
if keyPath == #keyPath(WKWebView.estimatedProgress) {
// When page load finishes. Should work on each page reload.
if (self.webView.estimatedProgress == 1) {
print("### EP:", self.webView.estimatedProgress)
}
}
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.load(request)
}
func webViewDidFinishLoad(webView : WKWebView) {
print("Loaded: \(String(describing: webView.url))")
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print("Loaded: \(String(describing: webView.url))")
//progressView.isHidden = true
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
//progressView.isHidden = false
print("Loaded: \(String(describing: webView.url))")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I have a Non-class type 'ContentView' cannot conform to class protocol 'NSObjectProtocol' error at line struct ContentView...

You can simply create a ObservableObject model class of webview with the name "WebViewModel" like
class WebViewModel: ObservableObject {
#Published var link: String
#Published var didFinishLoading: Bool = false
init (link: String) {
self.link = link
}
}
and also import
import WebKit
import Combine
and then copy this code snippets
struct SwiftUIWebView: UIViewRepresentable {
#ObservedObject var viewModel: WebViewModel
let webView = WKWebView()
func makeUIView(context: UIViewRepresentableContext<SwiftUIWebView>) -> WKWebView {
self.webView.navigationDelegate = context.coordinator
if let url = URL(string: viewModel.link) {
self.webView.load(URLRequest(url: url))
}
return self.webView
}
func updateUIView(_ uiView: WKWebView, context: UIViewRepresentableContext<SwiftUIWebView>) {
return
}
class Coordinator: NSObject, WKNavigationDelegate {
private var viewModel: WebViewModel
init(_ viewModel: WebViewModel) {
self.viewModel = viewModel
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
//print("WebView: navigation finished")
self.viewModel.didFinishLoading = true
}
}
func makeCoordinator() -> SwiftUIWebView.Coordinator {
Coordinator(viewModel)
}
}
struct SwiftUIWebView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIWebView(viewModel: WebViewModel(link: "https://google.com"))
//WebView(request: URLRequest(url: URL(string: "https://www.apple.com")!))
}
}
and in you view
struct AnyView: View {
#ObservedObject var model = WebViewModel(link: "https://www.wikipedia.org/")
var body: some View {
NavigationView {
SwiftUIWebView(viewModel: model)
if model.didFinishLoading {
//do your stuff
}
}
}}
so in this way you can get the others delegates response.

you use this to delegates of WKNavigationProtocol to perform(e.g to allow or cancel URL Loading) your action
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
if let host = navigationAction.request.url?.host {
if host.contains("facebook.com") {
decisionHandler(.cancel)
return
}
}
decisionHandler(.allow)
}

Use the following delegate function of WKWebView:
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
// Suppose you don't want your user to go a restricted site
if let host = navigationAction.request.url?.host {
if host == "restricted.com" {
decisionHandler(.cancel)
return
}
}
decisionHandler(.allow)
}
You can read this article from Medium which shows a better way of intercepting every network call or Url changing and obtaining upcoming Url related data. It also shows how to implement WebView in SwiftUI, interfacing with JavaScript functions and loading a local .html file from iOS project

Simple, Just use this delegate method
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print(webView.url?.absoluteString)
}

You can use key/value observation to detect changes to the url property of the WKWebView.
Here is a simple example of wrapping a WKWebView in a UIViewRepresentable.
Note that because we are modifying a property, the UIViewRepresentable is a final class rather than a struct.
import Combine
import SwiftUI
import WebKit
final class WebView: UIViewRepresentable {
#Published var url: URL? = nil {
didSet {
if url != nil {
willChange.send(url)
}
}
}
private let view = WKWebView()
private var urlChangedObservation: NSKeyValueObservation?
private let willChange = PassthroughSubject<URL?, Never>()
func makeUIView(context: Context) -> WKWebView {
return makeWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
}
func display(_ html: String) {
self.view.loadHTMLString(html, baseURL: nil)
}
public func load(_ url: String) -> WebView {
let link = URL(string: url)!
let request = URLRequest(url: link)
self.view.load(request)
return self
}
func makeWebView() -> WKWebView {
self.urlChangedObservation = self.view.observe(\WKWebView.url, options: .new) { view, change in
if let url = view.url {
self.url = url
}
}
return self.view
}
}
You can then listen to the url modified notification in the onReceive() of the container holding the WebView:
.onReceive(self.webview.$url) { url in
if let url = url {
}
}

I came here trying a fast way to get a working sample in SwiftUI to get an HTML response from an web auth service. (in the specific the new DropBox awful auth schema using an URI... we do no see this details, but call-backs and code should be explanatory enough. (JSOn comes from my web server specified in URI) )
in our Swift UI part:
struct ContentView: View {
#State private var showingSheet = false
private var webCallBack: WebCallBack = nil
let webView = WKWebView(frame: .zero)
#State private var auth_code = ""
var body: some View {
VStack{
Text("\(auth_code)")
.font(.system(size: 50))
Button("Show Auth web form") {
self.showingSheet = true
}
.sheet(isPresented: $showingSheet) {
WebView( webView: webView, webCallBack: { (d: Dict?) in
print("\n", d)
auth_code = (d?["auth_code"] as? String) ?? "!!"
showingSheet = false
} )
}
}
}
}
Our implementation:
typealias WebCallBack = ( (Dict?)->() )?
class MyWKDelegate: NSObject, WKNavigationDelegate{
private var webCallBack : WebCallBack = nil
init(webCallBack: WebCallBack) {
self.webCallBack = webCallBack
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print("End loading")
webView.evaluateJavaScript("document.body.innerHTML", completionHandler: { result, error in
if let html = result as? String {
//print(html)
// we are here also at first call, i.e. web view with user / password. Custiomize as needed.
if let d = dictFromJSONWith(string: html){
//print(d)
self.webCallBack?(d)
}
}
})
}
}
struct WebView: UIViewRepresentable {
let webView: WKWebView
let delegate: MyWKDelegate
internal init(webView: WKWebView, webCallBack: WebCallBack) {
self.webView = webView
self.delegate = MyWKDelegate(webCallBack: webCallBack)
webView.navigationDelegate = delegate
let urlStr = DB_URL.replacingOccurrences(of: "APP_KEY", with: APP_KEY).replacingOccurrences(of: "REDIRECT_URI", with: REDIRECT_URI)
print(urlStr)
if let url = URL(string: urlStr){
webView.load(URLRequest(url: url))
}
}
func makeUIView(context: Context) -> WKWebView {
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) { }
}
Some accessory code to make life easier:
typealias Dict = [String : Any]
typealias Dicts = [Dict]
func dictFromJSONWith(data: Data?)->Dict? {
guard let data = data else {
return nil
}
if let dict = try? JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions() ){
return dict as? Dict
}
return nil
}
func dictFromJSONWith(string: String?)->Dict?{
guard let data = string?.data(using: .utf8) else{
return nil
}
return dictFromJSONWith(data: data)
}

I have found a very good solution to my question. I will post it here. Maybe someone wants to see it and might be useful to them.
observe.observation = uiView.observe(\WKWebView.url, options: .new) { view, change in
if let url = view.url {
// do something with your url
}
}

Related

Access Denied (Errorcode=1020) on WKWebview

This is most likely a new question as I have not seen any other question related.
I am trying to connect to a cybersource payment endpoint using WKWebview on SwiftUI
I get the error
I have tried to set cookie but still won't work
WebView
public struct HTMLView: UIViewRepresentable {
var url: String
var javascriptString: String
#Binding var webViewUIModel: UIModel
public init(url: String, javascriptString: String = "", webViewUIModel: Binding<UIModel>) {
self.url = url
self.javascriptString = javascriptString
self._webViewUIModel = webViewUIModel
}
public func makeUIView(context: Context) -> WKWebView {
// let webView = WKWebView()
let webConfiguration = WKWebViewConfiguration()
let store = WKWebsiteDataStore.default()
webConfiguration.websiteDataStore = store
webConfiguration.preferences.javaScriptCanOpenWindowsAutomatically = true
webConfiguration.defaultWebpagePreferences.allowsContentJavaScript = true
let webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.navigationDelegate = context.coordinator
webView.loadHTMLString(url, baseURL: nil)
return webView
}
public func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.loadHTMLString(url, baseURL: nil)
}
public func makeCoordinator() -> WebViewCordinator {
.init(javascriptString: javascriptString) {
print("HTMLView: Start")
} didHaveError: { e in
print("HTMLView: Error")
} didFinish: {
print("HTMLView: Finish")
}
}
}
Cordinator
public class WebViewCordinator: NSObject, WKNavigationDelegate, WKUIDelegate {
var javascriptString: String
var didStart: () -> Void
var didHaveError: (Error) -> Void
var didFinish: () -> Void
public init(javascriptString: String = "", didStart: #escaping () -> Void, didHaveError: #escaping (Error) -> Void, didFinish: #escaping () -> Void) {
self.javascriptString = javascriptString
self.didStart = didStart
self.didHaveError = didHaveError
self.didFinish = didFinish
}
public func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: #escaping (WKNavigationResponsePolicy) -> Void) {
guard let response = navigationResponse.response as? HTTPURLResponse,
let url = navigationResponse.response.url else {
decisionHandler(.cancel)
return
}
if let headerFields = response.allHeaderFields as? [String: String] {
let cookies = HTTPCookie.cookies(withResponseHeaderFields: headerFields, for: url)
cookies.forEach { cookie in
print("Cookie \(cookie) \(cookie.domain)")
webView.configuration.websiteDataStore.httpCookieStore.setCookie(cookie)
}
}
decisionHandler(.allow)
}
public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
print("Error \(error)")
didHaveError(error)
}
public func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
print("HTMLViewCOMMIT \( webView.url)")
}
public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print( "HTMLViewFINISH \( String(describing: webView.url))")
webView.evaluateJavaScript("document.body.innerHTML") { value, error in
print("HTMLFinishValue \(value)")
print("Errot\(error)")
}
didFinish()
}
}
How can I resolve this?

Pull to refresh in SwiftUI WebView

I'm looking for the best approach to implement "pull to refresh" mechanism to SwiftUI WebView.
This is the closest solution I could wrote, however the refreshing process is not ended (activity indicator is present and nothing else is happening)
Those two extensions I've taken from this post https://stackoverflow.com/a/36256504
import SwiftUI
import WebKit
struct WebView: UIViewRepresentable {
let webView: WKWebView
init() {
webView = WKWebView(frame: .zero)
webView.load(URLRequest(url: URL(string: "https://cnn.com")!))
webView.setPullToRefresh(type: .embed)
}
func makeUIView(context: Context) -> WKWebView {
webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {}
}
extension WKWebView {
var refreshControl: UIRefreshControl? { (scrollView.getAllSubviews() as [UIRefreshControl]).first }
enum PullToRefreshType {
case none
case embed
}
func setPullToRefresh(type: PullToRefreshType) {
(scrollView.getAllSubviews() as [UIRefreshControl]).forEach { $0.removeFromSuperview() }
switch type {
case .none: break
case .embed: _setPullToRefresh(target: self, selector: #selector(webViewPullToRefreshHandler(source:)))
}
}
private func _setPullToRefresh(target: Any, selector: Selector) {
let refreshControl = UIRefreshControl()
refreshControl.addTarget(target, action: selector, for: .valueChanged)
scrollView.addSubview(refreshControl)
}
#objc func webViewPullToRefreshHandler(source: UIRefreshControl) {
guard let url = self.url else { source.endRefreshing(); return }
load(URLRequest(url: url))
}
}
extension UIView {
class func getAllSubviews<T: UIView>(from parenView: UIView) -> [T] {
return parenView.subviews.flatMap { subView -> [T] in
var result = getAllSubviews(from: subView) as [T]
if let view = subView as? T { result.append(view) }
return result
}
}
func getAllSubviews<T: UIView>() -> [T] { return UIView.getAllSubviews(from: self) as [T] }
}
OK, here how I solved this problem.
Checked on iPhone 14 Pro, iOS 15 & 16,
Swift 5
import SwiftUI
import WebKit
struct HomeView: UIViewRepresentable {
var webView = WebView()
func makeUIView(context: Context) -> WKWebView {
webView.webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {}
}
class WebView: NSObject {
var webView: WKWebView!
override init() {
super.init()
webView = WKWebView(frame: .zero)
webView.load(URLRequest(url: URL(string: "https://cnn.com")!))
webView.setPullToRefresh()
webView.navigationDelegate = self
}
}
extension WebView: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.refreshControl?.endRefreshing()
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
webView.refreshControl?.endRefreshing()
}
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
webView.refreshControl?.endRefreshing()
}
}
extension WKWebView {
var refreshControl: UIRefreshControl? {
(scrollView.getAllSubviews() as [UIRefreshControl]).first
}
func setPullToRefresh() {
(scrollView.getAllSubviews() as [UIRefreshControl]).forEach {
$0.removeFromSuperview()
}
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(webViewPullToRefreshHandler(source:)), for: .valueChanged)
scrollView.addSubview(refreshControl)
}
#objc func webViewPullToRefreshHandler(source: UIRefreshControl) {
guard let url = self.url else {
source.endRefreshing()
return
}
load(URLRequest(url: url))
}
}
extension UIView {
class func getAllSubviews<T: UIView>(from parentView: UIView) -> [T] {
return parentView.subviews.flatMap { subView -> [T] in
var result = getAllSubviews(from: subView) as [T]
if let view = subView as? T {
result.append(view)
}
return result
}
}
func getAllSubviews<T: UIView>() -> [T] {
return UIView.getAllSubviews(from: self) as [T]
}
}

Trying to implement the ability to launch multiple wkwebview instances to emulate tabbing on a browser

I'm trying to make a very barebones mobile browser to practice swiftui and wkwebview (WKwebview is wrapped in a UIViewRepresentable). However, when trying to implement multiple tabs for the browser I hit an odd error. The default webview works but when pressing the button to add new tabs, I notice that the new webview never actually has it's makeuiview() function run. The new tab just displays the last page that was loaded by the first webview but remains static and can't receive navigation. I'm not sure what would cause a uiviewrepresentable to not run it's makeuiview method. There's more code to this but I posted what I thought mattered most.
Content View
(Where the logic for adding the new tab resides) Refer to Tablist.newTab() function
import SwiftUI
import CoreData
class TabList: ObservableObject {
#Published var tabs: [BrowserPage] = [BrowserPage()]
func newTab(){
tabs.append(BrowserPage())
}
}
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#StateObject var tablist: TabList = TabList()
#State var selection = 0
#State var showTabs: Bool = false
var body: some View {
VStack {
tablist.tabs[selection]
Button("next") {
withAnimation {
selection = (selection + 1) % tablist.tabs.count
}
}
Divider()
Button("add") {
withAnimation {
tablist.newTab()
selection = tablist.tabs.count - 1
}
}
Divider()
Text("\(selection)")
Text("\(tablist.tabs.count)")
}
.frame( maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.sheet(isPresented: $showTabs, content: {
})
}
}
WebviewModel Class
import Foundation
import Combine
class WebViewModel: ObservableObject{
var webViewNavigationPublisher = PassthroughSubject<WebViewNavigation, Never>()
var showWebTitle = PassthroughSubject<String, Never>()
var showLoader = PassthroughSubject<Bool, Never>()
var valuePublisher = PassthroughSubject<String, Never>()
var url: String = "https://www.google.com"
#Published var urltoEdit: String = "https://www.google.com"
}
enum WebViewNavigation {
case backward, forward, reload, load
}
WebsiteView (UIViewRepresentable for WKWebview)
import Foundation
import UIKit
import SwiftUI
import Combine
import WebKit
protocol WebViewHandlerDelegate {
func receivedJsonValueFromWebView(value: [String: Any?])
func receivedStringValueFromWebView(value: String)
}
struct WebView: UIViewRepresentable, WebViewHandlerDelegate {
func receivedJsonValueFromWebView(value: [String : Any?]) {
print("JSON value received from web is: \(value)")
}
func receivedStringValueFromWebView(value: String) {
print("String value received from web is: \(value)")
}
#ObservedObject var viewModel: WebViewModel
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> WKWebView {
let preferences = WKPreferences()
let configuration = WKWebViewConfiguration()
configuration.userContentController.add(self.makeCoordinator(), name: "iOSNative")
configuration.preferences = preferences
let webView = WKWebView(frame: CGRect.zero, configuration: configuration)
webView.navigationDelegate = context.coordinator
//remove after debugging...
webView.allowsBackForwardNavigationGestures = true
webView.scrollView.isScrollEnabled = true
webView.load(URLRequest(url: URL(string: viewModel.url)!))
print("UUUUUUUUUUUUUUUUUUUUU " + viewModel.url)
return webView
}
func updateUIView(_ webView: WKWebView, context: Context) {
}
class Coordinator : NSObject, WKNavigationDelegate {
var parent: WebView
var valueSubscriber: AnyCancellable? = nil
var webViewNavigationSubscriber: AnyCancellable? = nil
var delegate: WebViewHandlerDelegate?
init(_ uiWebView: WebView) {
self.parent = uiWebView
}
deinit {
valueSubscriber?.cancel()
webViewNavigationSubscriber?.cancel()
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// Get the title of loaded webcontent
webView.evaluateJavaScript("document.title") { (response, error) in
if let error = error {
print("Error getting title")
print(error.localizedDescription)
}
guard let title = response as? String else {
return
}
self.parent.viewModel.showWebTitle.send(title)
}
valueSubscriber = parent.viewModel.valuePublisher.receive(on: RunLoop.main).sink(receiveValue: { value in
let javascriptFunction = "valueGotFromIOS(\(value));"
webView.evaluateJavaScript(javascriptFunction) { (response, error) in
if let error = error {
print("Error calling javascript:valueGotFromIOS()")
print(error.localizedDescription)
} else {
print("Called javascript:valueGotFromIOS()")
}
}
})
// Page loaded so no need to show loader anymore
self.parent.viewModel.showLoader.send(false)
}
/* Here I implemented most of the WKWebView's delegate functions so that you can know them and
can use them in different necessary purposes */
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
// Hides loader
parent.viewModel.showLoader.send(false)
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
// Hides loader
parent.viewModel.showLoader.send(false)
}
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
self.parent.viewModel.urltoEdit = webView.url!.absoluteString
// Shows loader
parent.viewModel.showLoader.send(true)
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
// Shows loader
parent.viewModel.showLoader.send(true)
self.webViewNavigationSubscriber = self.parent.viewModel.webViewNavigationPublisher.receive(on: RunLoop.main).sink(receiveValue: { navigation in
switch navigation {
case .backward:
if webView.canGoBack {
webView.goBack()
}
case .forward:
if webView.canGoForward {
webView.goForward()
}
case .reload:
webView.reload()
case .load:
webView.load(URLRequest(url: URL(string: self.parent.viewModel.url)!))
print("IIIIIIIIIIIIIIIIIIIII " + self.parent.viewModel.url)
}
})
}
// This function is essential for intercepting every navigation in the webview
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
// Suppose you don't want your user to go a restricted site
// Here you can get many information about new url from 'navigationAction.request.description'
decisionHandler(.allow)
}
}
}
// MARK: - Extensions
extension WebView.Coordinator: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
// Make sure that your passed delegate is called
if message.name == "iOSNative" {
if let body = message.body as? [String: Any?] {
delegate?.receivedJsonValueFromWebView(value: body)
} else if let body = message.body as? String {
delegate?.receivedStringValueFromWebView(value: body)
}
}
}
}
BrowserPage View
import SwiftUI
struct BrowserPage: View, Identifiable{
let id: UUID = UUID()
#ObservedObject var viewModel = WebViewModel()
var body: some View {
VStack{
URLBarView(viewModel: viewModel)
HStack{
WebView(viewModel: viewModel)
}
BottomNavBarView(viewModel: viewModel)
.padding(.bottom, 25)
}
}
}

Load an Activity Indicator while website is loading in SwiftUI

I have a basic list that displays a webview. I want to add an activity indicator that shows while the webpage is loading. this is the code that I've created.
import SwiftUI
import WebKit
struct WebView: UIViewRepresentable {
var url: String
// makeUIView func
func makeUIView(context: Context) -> WKWebView {
guard let url = URL(string: self.url) else {
return WKWebView()
}
let request = URLRequest(url: url)
let wkWebView = WKWebView()
wkWebView.load(request)
return wkWebView
}
// updateUIView func
func updateUIView(_ uiView: WKWebView, context: Context) {
}
}
struct WebView_Preview: PreviewProvider {
static var previews: some View {
WebView(url: "https://www.google.com")
}
}
Thank you!
Here's something that should do what you want. The WebView has a delegate in the Coordinator class. It changes a binding, to which the ContentView can react appropriately. Currently it's just a Text displaying the raw value of the state, but it can be replaced with an activity indicator of some sorts.
struct ContentView: View {
#State var urlString = ""
#State var workState = WebView.WorkState.initial
var body: some View {
VStack(spacing: 20) {
WebView(urlString: self.$urlString, workState: self.$workState)
Button("Play") {
self.urlString = "https://www.example.com/"
}
Text("Current work = " + self.workState.rawValue)
}
}
}
struct WebView: UIViewRepresentable {
enum WorkState: String {
case initial
case done
case working
case errorOccurred
}
#Binding var urlString: String
#Binding var workState: WorkState
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
webView.navigationDelegate = context.coordinator
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
switch self.workState {
case .initial:
if let url = URL(string: self.urlString) {
uiView.load(URLRequest(url: url))
}
default:
break
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, WKNavigationDelegate {
var parent: WebView
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
self.parent.workState = .working
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
self.parent.workState = .errorOccurred
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
self.parent.workState = .done
}
init(_ parent: WebView) {
self.parent = parent
}
}
}
You will need to add an Coordinator to your UIViewRepresentable.
I think the answers for this question give you the right ideas.
SwiftUI WKWebView detect url changing

SwiftUI how to wrap WKWebView to make it autosizing after html load

I have WKWebView placed in List cell and want this cell to be autosized to height of content that was loaded into WKWebView. I have tried such code but it doesn't work
struct WebView: UIViewRepresentable {
let url: URL?
let html: String?
let didFail: (WKNavigation) -> Void
let didFinish: (WKNavigation) -> Void
#Binding var contentHeight : CGFloat
init(html: String, contentHeight: Binding<CGFloat> = .constant(0), didFail: #escaping (WKNavigation) -> Void = { _ in }, didFinish: #escaping (WKNavigation) -> Void = { _ in }) {
self.url = nil
self.html = html
self.didFinish = didFinish
self.didFail = didFail
self._contentHeight = contentHeight
}
init(url: URL, contentHeight: Binding<CGFloat> = .constant(0), didFail: #escaping (WKNavigation) -> Void = { _ in }, didFinish: #escaping (WKNavigation) -> Void = { _ in }) {
self.url = url
self.html = nil
self.didFinish = didFinish
self.didFail = didFail
self._contentHeight = contentHeight
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
webView.navigationDelegate = context.coordinator
webView.uiDelegate = context.coordinator
if let url = url {
let request = URLRequest(url: url)
webView.load(request)
} else if let html = html {
webView.loadHTMLString(html, baseURL: nil)
}
return webView
}
func updateUIView(_ webView: WKWebView, context: Context) {
}
class Coordinator : NSObject, WKNavigationDelegate, WKUIDelegate {
private let parent: WebView
init(_ parent: WebView) {
self.parent = parent
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
parent.didFail(navigation)
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("document.readyState", completionHandler: { (ready, error) in
if ready != nil {
webView.evaluateJavaScript("document.documentElement.scrollHeight", completionHandler: { (height, error) in
if let contentHeight = height as? CGFloat {
self.parent.contentHeight = contentHeight
}
})
}
})
parent.didFinish(navigation)
}
}
}
Even setting .frame(height: contentHeight) via #State does not work.

Resources