WKWebview Prevent updateUIView - ios

When creating the WKWebView I pass the web page URL via #Binding webPageURL.
I also update the displayed website via webPageURL.
The problem here is that when I call a NavigationLink and navigate back from the NavigationLink, the updateUIView method is called and the WKWebView reloads the web page.
How do I prevent the WKWebView from calling the method updateUIView when I navigate back from the NavigationLink?
struct MainView:View {
#State private var isActive = false
#Binding var stateWebPageURL:String
var body: some View {
return VStack {
MyWKWebView(webPageURL:$stateWebPageURL)
NavigationLink(destination: SecoundDestination(),isActive: $isActive) {
Text("Do Something")
}
}
}
}
struct MyWKWebView: UIViewRepresentable {
#Binding var webPageURL:String
func updateUIView(_ uiView: WKWebView, context: Context) {
let pageURL = URL(string:webPageURL)
let urlRequest = URLRequest(url: pageURL!)
uiView.load(urlRequest)
}
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView(frame: .zero,configuration: webConfiguration)
let pageURL = URL(string:webPageURL)
let urlRequest = URLRequest(url: pageURL!)
webView.load(urlRequest)
return webView
}
func makeCoordinator() -> ContentController {
ContentController()
}
}

Provided code is not testable, so just by code reading, the idea is to make explicitly MyWKWebView equatable, as below
var body: some View {
return VStack {
MyWKWebView(webPageURL:$stateWebPageURL).equatable() // << here !!
and
struct MyWKWebView: UIViewRepresentable, Equatable {
static func == (lhs: MyWKWebView, rhs: MyWKWebView) -> Bool {
lhs.webPageURL == rhs.webPageURL
}
// ... other your code
SwiftUI should not update equal views, so should work.

Related

I don't want to reload webview when sheet dismiss

I'm trying to connect a GoBack button and a Forward button to a Webview. I've confirmed that it works fine in the webview (myWebView) which is set on the main view (myView).
In this webview, I implemented the ability to communicate(Java) with the webpage to open a sheet modal window(this is also web page).
However, the problem arises that the webview page return to goHome(reload to 1st page) whenever the modal window is dismissed (no problem when to open).
Isn't there any way to control this Reload Issue that occurs whenever the modal window dismiss?
If 'uiView.load(request)' is put something conditional on the updateUIView part, the goBack/Forward navigation buttons will fail.
struct myView: View {
#State var activeSheet: Bool = false
.....
var body: some View {
let webView = myWebView(web: nil, actSheet: $activeSheet, req: URLRequest(url: URL(string: "https://google.com")!))
VStack {
webView
}
.sheet(isPresented: $activeSheet) {
anotherView(urlToLoad: urlToOpenInSheet)
}
.onChange(of: urlToOpenInSheet) { newValue in
print("url changed to \(urlToOpenInSheet)!")
}
HStack {
Button(action: {
webView.goBack()
print("goBack: \(urlToOpenInSheet)")
}) {
Image(systemName: "arrow.backward")
.font(.system(size: 20))
.foregroundColor(.white)
.frame(alignment: .trailing)
}.frame(alignment: .topTrailing)
.padding(EdgeInsets(top: 5))
}
}
}
struct myWebView: UIViewRepresentable {
let request: URLRequest
var webview: WKWebView?
#Binding var activeSheet: Bool
init(web: WKWebView?, actSheet: Binding<Bool>, req: URLRequest) {
self.webview = WKWebView()
self._activeSheet = actSheet
self.request = req
}
....
....
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.load(request)
}
func goBack(){
webView?.goBack()
}
func goForward(){
webView?.goForward()
}
....
....
}

Reload web view contents when SwiftUI button is pressed

Aim
To display a web view inside a SwiftUI view
Reload the contents of the web view when a button (SwiftUI) is pressed.
My Attempt
I have made an attempt to do the above.
Pass a Publisher (CurrentValueSubject) to the UIViewRepresentable
Observe for any new values from the publisher
If the value is true refresh the web view
Question:
Is there a better way to do this? or is my approach reasonable?
Code:
WebView
import SwiftUI
import WebKit
import Combine
struct WebView: UIViewRepresentable {
let url: URL
let shouldRefresh: CurrentValueSubject<Bool, Never>
func makeUIView(context: Context) -> some UIView {
let configuration = WKWebViewConfiguration()
let webView = WKWebView(frame: .zero, configuration: configuration)
let request = URLRequest(url: url)
context.coordinator.navigation = webView.load(request)
context.coordinator.webView = webView
context.coordinator.observeChanges(for: shouldRefresh)
return webView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
print("update view called")
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
}
extension WebView {
class Coordinator {
var navigation: WKNavigation?
var webView: WKWebView?
private var cancellable: AnyCancellable?
#MainActor
func observeChanges(for shouldRefresh: CurrentValueSubject<Bool, Never>) {
cancellable = shouldRefresh
.filter { $0 == true }
.sink { [weak self] newValue in
self?.navigation = self?.webView?.reload()
}
}
}
}
ContentView
import SwiftUI
import Combine
struct ContentView: View {
#StateObject private var model = Model()
var body: some View {
VStack {
Button("Reload") {
print("reload pressed")
reload()
}
WebView(url: URL(string: "https://www.apple.com")!,
shouldRefresh: model.shouldRefresh)
}
}
func reload() {
model.shouldRefresh.send(true)
}
}
extension ContentView {
class Model: ObservableObject {
var shouldRefresh = CurrentValueSubject<Bool, Never>(false)
}
}

WKWebView scrolls to the top after keyboard opens

I have a WKWebView app, and in the url it loads it's got editables (inputs) to type in. But in the ios app when you tap an input that's at the bottom of the page, it opens the keyboard as expected, scrolls down on the page so you can see the input as expected, then it scrolls back to the top after half a second. So I tried it in the browser, but it didn't scroll back up after a split second. Does anyone know why this is happening only in my ios app?
Here's my code:
ContentView.swift:
struct ContentView: View {
var body: some View {
WebView(url: URL(string: "https://www.mywebsitenamethatiwontshare.com")!)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
WebView.swift:
struct WebView: UIViewRepresentable {
var url: URL
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ webView: WKWebView, context: Context) {
let request = URLRequest(url: url)
webView.load(request)
}
}
It is possible you might be causing a refresh of the view in your updateUIView function. Try removing the webView.load inside and try
struct WebView: UIViewRepresentable {
var url: URL
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
let request = URLRequest(url: url)
webView.load(request)
return webView
}
func updateUIView(_ webView: WKWebView, context: Context) {
// DO NOTHING
}
}

SwiftUI View init called multiple times

I am pretty new to SwiftUI. I have a very simple view. It's just a root view that contains a WKWebView wrapped in a UIViewRepresentable. My problem is, that the init method of the UIViewRepresentable is called 6 times when the view is opened. Which means the WKWebView is initialised 6 times and all my initialisation code (setting JS callbacks, ...) is called 6 times. I added print statements to the init functions of the root view MyWebView and the subview WebView (the UIViewRepresentable). The root view init is only called once, but the subview's init is called 6 times. Is this normal? Or am I doing something wrong?
struct MyWebView: View {
#ObservedObject private var viewModel = WebViewModel()
init() {
print("root init")
}
var body: some View {
VStack(alignment: .leading, spacing: 0, content: {
WebView(viewModel: viewModel)
})
.navigationBarTitle("\(viewModel.title)", displayMode: .inline)
.navigationBarBackButtonHidden(true)
} }
struct WebView: UIViewRepresentable {
var wkWebView: WKWebView!
init(viewModel: WebViewModel) {
print("webview init")
doWebViewInitialization(viewModel: viewModel)
}
func makeUIView(context: UIViewRepresentableContext<WebView>) -> WKWebView {
let request = URLRequest(url: URL(string: "https://www.google.com")!, cachePolicy: .returnCacheDataElseLoad)
wkWebView.load(request)
return wkWebView
}
}
I'm not getting your issue of multiple calls to the init method of the UIViewRepresentable.
I modified slightly your WebView, and this is how I tested my answer:
import SwiftUI
import Foundation
import WebKit
#main
struct TestSApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
MyWebView()
}.navigationViewStyle(StackNavigationViewStyle())
}
}
// for testing
class WebViewModel: ObservableObject {
#Published var title = ""
}
struct WebView: UIViewRepresentable {
let wkWebView = WKWebView()
init(viewModel: WebViewModel) {
print("\n-----> webview init")
// doWebViewInitialization(viewModel: viewModel)
}
func makeUIView(context: UIViewRepresentableContext<WebView>) -> WKWebView {
if let url = URL(string: "https://www.google.com") {
let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad)
wkWebView.load(request)
}
return wkWebView
}
func updateUIView(_ webview: WKWebView, context: UIViewRepresentableContext<WebView>) { }
}
struct MyWebView: View {
#ObservedObject private var viewModel = WebViewModel()
init() {
print("\n-----> root init")
}
var body: some View {
VStack(alignment: .leading, spacing: 0, content: {
WebView(viewModel: viewModel)
})
.navigationBarTitle("\(viewModel.title)", displayMode: .inline)
.navigationBarBackButtonHidden(true)
}
}
This leaves "doWebViewInitialization" with a possible problem spot.
You have to write your code assuming that the initializer of the View in SwiftUI will be called many times.
You write the initialization process in makeUIView(context:) in this case.
See:
https://developer.apple.com/documentation/swiftui/uiviewrepresentable/makeuiview(context:)
For example, I wrote the following code based on this answer. I added a toggle height button to this referenced code.
the -----> makeUIView log is only output once,
but the -----> webview init logs are output every time the toggle button is pressed.
import SwiftUI
import WebKit
struct ContentView: View {
var body: some View {
MyWebView()
}
}
class WebViewModel: ObservableObject {
#Published var title = ""
}
struct WebView: UIViewRepresentable {
let wkWebView = WKWebView()
init(viewModel: WebViewModel) {
print("\n-----> webview init")
}
func makeUIView(context: UIViewRepresentableContext<WebView>) -> WKWebView {
print("\n-----> makeUIView")
if let url = URL(string: "https://www.google.com") {
let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad)
wkWebView.load(request)
}
return wkWebView
}
func updateUIView(_ webview: WKWebView, context: UIViewRepresentableContext<WebView>) { }
}
struct MyWebView: View {
#State private var toggleHight = false
#ObservedObject private var viewModel = WebViewModel()
init() {
print("\n-----> root init")
}
var body: some View {
VStack {
WebView(
viewModel: viewModel
)
.frame(
height: { toggleHight ? 600 : 300 }()
)
Button(
"toggle",
action: {
toggleHight.toggle()
}
)
}
}
}
Furthermore, I realized after I wrote example code that WebView: UIViewRepresentable should not have an instance variable of wkWebView.
Please do it all(create instance and configuration) in makeUIView(context:), as shown below.
This is because instance variables are recreated every time the initializer is called.
import SwiftUI
import WebKit
struct ContentView: View {
var body: some View {
MyWebView()
}
}
class WebViewModel: ObservableObject {
#Published var title = ""
}
struct WebView: UIViewRepresentable {
init(viewModel: WebViewModel) {
print("\n-----> webview init")
}
func makeUIView(context: UIViewRepresentableContext<WebView>) -> WKWebView {
print("\n-----> makeUIView")
let wkWebView = WKWebView()
if let url = URL(string: "https://www.google.com") {
let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad)
wkWebView.load(request)
}
return wkWebView
}
func updateUIView(_ webview: WKWebView, context: UIViewRepresentableContext<WebView>) { }
}
struct MyWebView: View {
#State private var toggleHight = false
#ObservedObject private var viewModel = WebViewModel()
init() {
print("\n-----> root init")
}
var body: some View {
VStack {
WebView(
viewModel: viewModel
)
.frame(
height: { toggleHight ? 600 : 300 }()
)
Button(
"toggle",
action: {
toggleHight.toggle()
}
)
}
}
}
I struggled with this tight constraint when I was developing with UIViewControllerRepresentable. With the help of my colleagues, I managed to finish the code.
Your code has been called 6 times, so there may be some problem. but I cannot tell what the problem is from the code you provided.
It is common for init to be called multiple times in SwiftUI. We need to write code to deal with this. If your init is being called too often, you may want to look for the root cause. The code I referred to and the code I wrote are only once at startup.

SwiftUI: WebView doesn't load specific page

I try to load a url in my WebView, it works well with "https://www.google.com", but it doesn't work with "https://www.sl.se".
It shows "Not Found, HTTP Error 404: The requested resource is not found."
The weird thing is that, if I google "sl.se" in Google page, click the sl.se, the the "www.sl.se" can be load the WebView. Does anyone know the reason?
ContentView.swift
// ContentView.swift
struct ContentView: View {
#State private var shouldRefresh = false
var body: some View {
VStack{
Button(action: {
self.shouldRefresh.toggle()
}){
Text("Reload")
}
WebView(url: nil, reload: $shouldRefresh)
}
}
}
WebView.Swift
// WebView.swift
import WebKit
import SwiftUI
struct WebView: UIViewRepresentable{
var url: URL? // optional, if absent, one of below search servers used
#Binding var reload: Bool
private let urls = [URL(string: "https://google.com/")!, URL(string: "https://www.sl.se")!]
private let webview = WKWebView()
fileprivate func loadRequest(in webView: WKWebView) {
if let url = url {
webView.load(URLRequest(url: url))
} else {
let index = Int(Date().timeIntervalSince1970) % 2
print("load: \(urls[index].absoluteString)")
webView.load(URLRequest(url: urls[index]))
}
}
func makeUIView(context: UIViewRepresentableContext<WebView>) -> WKWebView {
loadRequest(in: webview)
return webview
}
func updateUIView(_ uiView: WKWebView, context: UIViewRepresentableContext<WebView>) {
if reload {
loadRequest(in: uiView)
DispatchQueue.main.async {
self.reload = false // must be async
}
}
}
}
There is nothing wrong with your code. You just need to remove the www from your URL. try https://sl.se and It'll work.

Resources