How do I refresh a WKWebView via #StateObject stored in another class after altering a property on an #ObservedObject? [duplicate] - swift

This question already has an answer here:
How to make SwiftUI WebView dynamically load an URL?
(1 answer)
Closed 2 days ago.
This post was edited and submitted for review yesterday.
I have a basic model like this"
public final class WebSite: ObservableObject {
#Published
var URL: String
// Let us please assume that this will be a valid URL
}
And a webview. It uses the site url to render a UIViewRepresentable (my site view must be a class bc of the WKWebView protocol):
class WebSiteWebView: WKWebView {
#ObservedObject
var webSite: WebSite
init() { /* initialization. Not relevant here*/ }
loadUrl(url: String) {
self.load(URLRequest(url: url))
}
render() -> some View {
let representable = WebSiteWebView.Representable(webView: self)
do {
try representable.webView.loadUrl(url: self.webSite.url)
} catch { ... }
}
}
Now, I have a parent ContentView as follows:
struct ContentView: View {
#StateObject var webSite: WebSite = WebSite(URL: "stackoverflow.com")
var body: some View {
WebSiteView(webSite: webSite)
}
}
and a WebSiteView that displays a web view:
struct WebSiteView: View {
#ObservedObject
var webSite: WebSite
var body: some View {
let webView: WebSiteWebView = WebView(webSite: webSite)
VStack {
webView.render()
Button("Switch URL") {
webSite.URL = "google.com"
}
}
}
}
The UIViewRepresentable impl for completeness
extension WebSiteWebView {
struct Representable: UIViewRepresentable {
let webView: WebSiteWebView
func makeUIView(context: Context) -> WebSiteWebView {
self.webView
}
func updateUIView(_ uiView: WebSiteWebView, context: Context) {}
}
}
In a nutshell, this is what's going on:
The parent ContentView initializes a #StateObject that contains a published String named URL.
The ContentView has a child view named WebSiteView that takes the web site class as an #ObservedObject
That child view loads the URL from the #ObservedObject
That child view also has a button that changes the URL
I would expect that after the button is pressed, the WebSiteView would re-render, since it declares the webSite as an #ObservedObject. And, it does (technically). webView.render() is called with the new URL but the WKWebView does not refresh. Does anyone know why this is happening?

things are a bit backwards, it needs to be like this:
struct WebSiteWebView: UIViewRepresentable {
let url: URL
func makeUIView(context: Context) -> WKWebView {
WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
if uiView.url != url {
uiView.load(url)
}
}
}
Basically the WebSiteWebView struct can be recreated many times and your job is to update the WKWebView object with any change to the properties. updateUIView is called the first time and also any time the url changes.

Related

How do I inject CSS/JS in a WKWebView using SwiftUI?

I'm new to SwiftUI and trying to inject some custom CSS/JS into a page loaded with WKWebView:
import SwiftUI
import WebKit
struct WebView: UIViewRepresentable {
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ webView: WKWebView, context: Context) {
let request = URLRequest(url: URL(string: "https://example.com")!)
WKWebsiteDataStore.default().removeData(ofTypes: [WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache], modifiedSince: Date(timeIntervalSince1970: 0), completionHandler:{ })
webView.load(request)
webView.configuration.userContentController.addUserScript(WKUserScript( source: "alert('debug')", injectionTime: .atDocumentEnd, forMainFrameOnly: true))
}
}
Which is load like this:
struct ContentView: View {
var body: some View {
WebView()
}
}
Sadly, the code doesn't seem to actually inject anything. I've tried running it before webView.load as well. Having been googling quite a bit, I only see examples done in UIKit and unfortunately, I'm too inexperienced to wrap UIKit in a way that I can use with SwiftUI.
Any guidance would be greatly appreciated.
First of all try to avoid including business code in your views whenever you can. You may use two functions in the Webkit API if you want to include/inject JS to the webview content: EvaluateJS and AddUserScript. You may use "AddUserScript" before the "load" starts. Also please not that "alert" function in JS, would not work in current Mobile Safari. You should have see the text colors to appear in blue with the script below.
Result:
import SwiftUI
import WebKit
struct ContentView: View {
var body: some View {
VStack {
CustomWebview()
}
.padding()
}
}
struct SwiftUIWebView: UIViewRepresentable {
typealias UIViewType = WKWebView
let webView: WKWebView
func makeUIView(context: Context) -> WKWebView {
webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
}
}
final class SwiftUIWebViewModel: ObservableObject {
#Published var addressStr = "https://www.stackoverflow.com"
let webView: WKWebView
init() {
webView = WKWebView(frame: .zero)
loadUrl()
}
func loadUrl() {
guard let url = URL(string: addressStr) else {
return
}
webView.configuration.userContentController.addUserScript(WKUserScript( source: """
window.userscr ="hey this is prior injection";
""", injectionTime: .atDocumentStart, forMainFrameOnly: false))
webView.load(URLRequest(url: url))
// You will have the chance in 8 seconds to open Safari debugger if needed. PS: Also put a breakpoint to injectJS function.
DispatchQueue.main.asyncAfter(deadline: .now() + 8.0) {
self.injectJS()
}
}
func injectJS () {
webView.evaluateJavaScript("""
window.temp = "hey here!";
document.getElementById("content").style.color = "blue";
""")
}
}
struct CustomWebview: View {
#StateObject private var model = SwiftUIWebViewModel()
var body: some View {
VStack {
SwiftUIWebView(webView: model.webView)
.padding()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Argument passed to call that takes no arguments on swift for Webview

I am building a webview on xcode with swift and the error
Argument passed to call that takes no arguments on swift for Webview
I am building a webview on xcode with swift and the error Argument passed to call that takes no arguments on swift for Webview
//
// ContentView.swift
// GenuineApp
//
// Created by Kennysoft-Macbook on 11/10/22.
//
import SwiftUI
import WebKit
struct ContentView: View {
#State private var showWebView = false
private let urlstring: String = "https://www.genuineict.com"
var body: some View {
VStack(spacing: 40) {
WebView(url: URL(string: urlstring)!).frame(height: 500.0)
.cornerRadius(10)
}
}
}
struct WebView: UIViewRepresentable {
var url:URL
func makeUIView(context: Context) -> some UIView {
return WKWebView()
}
func updateUIView(_ uiView: UIViewType, context: Context) {
let request = URLRequest(url: url)
UIView.load(request) Here is the error Argument passed to call that takes no arguments on swift for Webview
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
This looks good but it is not working on the latest xcode is there anyone using xcode with swift in 2022
Am building a webview on xcode with swift and the error Argument passed to call that takes no arguments on swift for Webview
The UIView.load() method you are calling doesn't take any arguments, so XCode is correct in their assessment. You can control-click on the .load method and go to the documentation to verify this.
What you probably wanted to do is pass the URLRequest to the WKWebView that you're creating. By making the return value of makeUIView specific (so not using some to make it opaque), the updateUIView method will also recognize it as the incoming uiView. You can then call the correct WKWebView.load(_:) on it:
struct WebView: UIViewRepresentable {
var url: URL
func makeUIView(context: Context) -> WKWebView {
WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
let request = URLRequest(url: url)
uiView.load(URLRequest(url: url))
}
}

How to avoid retain cycle in custom SwiftUI view closure

I'm trying to create a wrapper around a WKWebView for SwiftUI, and I'm not sure how to prevent a memory leak.
I've created an ObservableObject which handles controlling the WebView, and a custom view that displays it.
The web view needs to be able to communicate with JavaScript, so I've added an onAction() method which gets called when the WebView gets a javascript event.
public class WebViewStore: ObservableObject {
var webView: WKWebView = WKWebView()
// List of event handlers that will be called from the WebView
var eventHandlers: [String: () -> Void] = [:]
deinit {
// This is never called once an action has been set with the view,
// and the content view is destroyed
print("deinit")
}
// All the WebView code, including custom JavaScript message handlers, custom scheme handlers, etc...
func reloadWebView() { }
}
View wrapper:
struct WebView: NSViewRepresentable {
let store: WebViewStore
func makeNSView(context: Context) -> WKWebView {
return store.webView
}
func updateNSView(_ view: WKWebView, context: Context) {}
}
extension WebView {
/// Action event called from the WebView
public func onAction(name: String, action: #escaping () -> Void) -> WebView {
self.store.eventHandlers[name] = action
return self
}
}
Usage that creates the retain cycle:
struct ContentView: View {
#StateObject var store = WebViewStore()
var body: some View {
VStack {
// Example of interacting with the WebView
Button("Reload") {
store.reloadWebView()
}
WebView(store: store)
// This action closure captures the WebViewStore, causing the retain cycle.
.onAction(name: "javascriptMessage") {
print("Event!")
}
}
}
}
Is there a way to prevent the retain cycle from happening, or is there a different SwiftUI pattern that can to handle this use case? I can't use [weak self] because the view is a struct. I need to be able to receive events from the WebView and send them to SwiftUI and vice versa.

Sheet and alert keeps refreshing my page for WKWebView SwiftUI

I am currently using WKWebView and I added a sheet and an alert inside my wkwebview. However, every time I close the sheet or close the alarm, My WkWebView keeps navigating(loading) back to the origin domain that I specified (for this case, google.com). I an wondering how I can fix this issue.
WebView
import SwiftUI
import WebKit
struct WebView : UIViewRepresentable {
let request: URLRequest
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.load(request)
}
}
Main View
struct MainView: View {
var webView: WebView = WebView(request: URLRequest(url: URL(string: "https://www.google.com")!))
var body: some View {
VStack(){
AlertView()
webView
SheetScreenView(webView: $webView)
}
}
AlertView()
...
Button(action: {
}, label: {}..alert(isPresented: $isBookmarked) {
Alert(title: Text(" \(webView.getURL())"), dismissButton: .default(Text("Ok")))
}
...
SheetScreenView
....
#State private var showingSheet = false
var body: some View {
Button("Show Sheet") {
showingSheet.toggle()
}
.sheet(isPresented: $showingSheet) {
SheetView()
}
}
...
SheetView
struct SheetView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Button("Press to dismiss") {
presentationMode.wrappedValue.dismiss()
}
.font(.title)
.padding()
.background(Color.black)
}
}
Your issue can be reproduced even without the sheet and alert by just rotating the device. The problem is that in updateView (which is going to get called often), you call load on the URLRequest. This is going to get compounded by the fact that you're storing a view in a var which is going to get recreated on every new render of MainView, since Views in SwiftUI are transient.
The simplest way to avoid this if your URL isn't going to change is to just call load in makeUIView:
struct WebView : UIViewRepresentable {
var request: URLRequest
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
webView.load(request)
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
}
}
struct ContentView: View {
var body: some View {
WebView(request: URLRequest(url:URL(string: "https://www.google.com")!))
}
}
If your URL may change, you need a way to compare the previous state with the new one. This seems like the shortest (but certainly not only) way to do this:
struct WebView : UIViewRepresentable {
var url: URL
#State private var prevURL : URL?
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
if (prevURL != url) {
let request = URLRequest(url: url)
uiView.load(request)
DispatchQueue.main.async { prevURL = url }
}
}
}
struct ContentView: View {
var body: some View {
WebView(url: URL(string: "https://www.google.com")!)
}
}

A link with swift webview application target _blank is not working

I'm a beginner, sorry I did webview with swift, but a link with target _blank doesn't work
I've created a UIViewRepresentable class for the WKWebView from UIKit. The UIViewRepresentable class can be used to create and manage views(UIView) from UIKit in SwiftUI. There are two views in the project,
ContentView lists the urls from string array. Selecting an url navigates to the Detail view.
The Detail view shows the web page for the selected url using WKWebView.
Hope this helps. Here's the code.
import SwiftUI
import UIKit
import WebKit
struct ContentView: View {
var urls: [String] = ["https://www.stackoverflow.com", "https://www.yahoo.com"]
#State private var hideStatusBar = false
var body: some View {
NavigationView {
List {
ForEach(urls, id: \.self) { url in
VStack {
NavigationLink(destination: DetailView(url: url)) {
Text(url)
}
}
}
}
.navigationBarTitle("Main")
}
}
}
struct DetailView: View {
var url: String = ""
var body: some View {
VStack {
Webview(url: url)
Spacer()
}
.navigationBarHidden(true)
}
}
struct Webview: UIViewRepresentable {
var url: String
typealias UIViewType = WKWebView
func makeUIView(context: UIViewRepresentableContext<Webview>) -> WKWebView {
let wkWebView = WKWebView()
guard let url = URL(string: self.url) else {
return wkWebView
}
let request = URLRequest(url: url)
wkWebView.load(request)
return wkWebView
}
func updateUIView(_ uiView: WKWebView, context: UIViewRepresentableContext<Webview>) {
}
}