Hi i have the following code to load a webpage in SwiftUI Xcode 12, everything load nicely, but the tel links and WhatsApp link doesn't work, what can I do?
this is my browser.swift file:
import Foundation
import SwiftUI
import WebKit
struct WebView: UIViewRepresentable{
var url: String
func makeUIView(context: Context) -> some WKWebView {
guard let url = URL(string: self.url) else {
return WKWebView()
}
let request = URLRequest(url: url)
let wkWebView = WKWebView()
wkWebView.load(request)
return wkWebView
}
func updateUIView(_ uiView: UIViewType, context: UIViewRepresentableContext<WebView>) {
}
}
this is my contentview.swift:
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack {
Color(UIColor(red: 0.89, green: 0.98, blue: 0.88, alpha: 1.00))
.ignoresSafeArea()
WebView(url: "https://dominio.com/app/ios/index.php?idE=17")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You need to implement the WKNavigationDelegate and WKUIDelegate
func makeUIView(context: Context) -> some WKWebView {
guard let url = URL(string: self.url) else {
return WKWebView()
}
let request = URLRequest(url: url)
let wkWebView = WKWebView()
wkWebView.navigationDelegate = context.coordinator //<<Add your delegates here
wkWebView.uiDelegate = context.coordinator //<<Add your delegates here
wkWebView.load(request)
return wkWebView
}
Add your coordinator function
func makeCoordinator() -> Coordinator {
Coordinator()
}
And at the Coordinator class
class Coordinator: NSObject, WKNavigationDelegate, WKUIDelegate {
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void)
{
if navigationAction.targetFrame == nil {
webView.load(navigationAction.request)
}
if navigationAction.request.url?.scheme == "tel" {
UIApplication.shared.openURL(navigationAction.request.url!)
decisionHandler(.cancel)
}
else if navigationAction.request.url?.scheme == "mailto" {
UIApplication.shared.openURL(navigationAction.request.url!)
decisionHandler(.cancel)
} else {
decisionHandler(.allow)
}
}
}
Make sure to test it on real devices
Related
While learning to use webview in my app I just added a webview and detected a memory leak.
I found a lot of demos on the Internet and tested them and all have this problem.
Here is my test code:
Instruments screenshot
import SwiftUI
import WebKit
struct SWKWebView: UIViewRepresentable {
#Binding var url: String?
func makeUIView(context: Context) -> WKWebView {
let webview = WKWebView()
webview.navigationDelegate = context.coordinator
return webview
}
func updateUIView(_ uiView: WKWebView, context: Context) {
if let url = url, let requetURL = URL(string: url) {
uiView.load(URLRequest(url: requetURL))
}
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
class Coordinator: NSObject,WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("document.title") { (result, error) in
print("didFinish:\(String(describing: result ?? ""))")
}
}
}
}
struct TTTest: View {
#State var url: String? = "https://www.google.com"
var body: some View {
SWKWebView(url: $url)
}
}
When I updated the IOS system to version 15.5, this problem was solved
I am building a view that uses WKWebView. To that end I am using UIViewRepresentable.
I would like to show the web page loading progress using a ProgressView. To that end I want to drive the progress UI using the WKWebView.estimatedProgress.
I am putting here the entire code. If you copy and paste this in a project you'll see that TestWContainer is stuck updating. I am trying to understand how to fix this, and I guess understanding the correct design pattern to follow in a situation like this to avoid endless view updates.
Here the code:
struct TestWContainer: View {
#State var url:URL?
#State var userSetUrl:URL?
#State var showLoader:Bool?
#State var estimatedProgress:Double?
var body: some View {
ZStack {
WebView(currentURL: $url, userSetURL: $userSetUrl, showLoader: $showLoader, estimatedProgress: $estimatedProgress)
if let estimatedProgress = estimatedProgress {
if estimatedProgress > 0 && estimatedProgress < 1 {
let _ = print("estimatedPogress: \(estimatedProgress)")
VStack(spacing:0) {
ProgressView(value: estimatedProgress, total: 1)
.frame(height: 3)
Spacer()
}
}
}
}
}
}
struct WebView: UIViewRepresentable {
#Binding var currentURL:URL?
#Binding var userSetURL:URL?
#Binding var showLoader:Bool?
#Binding var estimatedProgress:Double?
fileprivate let defaultURL:URL = URL(string: "https://www.google.com")!
class Coordinator: NSObject, WKNavigationDelegate {
var parent: WebView
var webViewNavigationSubscriber: AnyCancellable? = nil
init(_ webView: WebView) {
self.parent = webView
}
deinit {
webViewNavigationSubscriber?.cancel()
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
parent.showLoader = false
}
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
parent.showLoader = false
}
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
parent.showLoader = true
}
// This function is essential for intercepting every navigation in the webview
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
decisionHandler(.allow)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
dispatchPrecondition(condition: .onQueue(.main))
print("Observing keyPath: \(keyPath). change: \(change). object: \(object)")
guard let wv = object as? WKWebView else { return }
if keyPath == #keyPath(WKWebView.estimatedProgress) {
print("O: progress: \(wv.estimatedProgress)")
DispatchQueue.main.async {
self.parent.estimatedProgress = wv.estimatedProgress
}
}
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeUIView(context: Context) -> WKWebView {
let configuration = WKWebViewConfiguration()
let webView = WKWebView(frame: CGRect.zero, configuration: configuration)
webView.navigationDelegate = context.coordinator
webView.allowsBackForwardNavigationGestures = true
webView.scrollView.isScrollEnabled = true
webView.addObserver(context.coordinator, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: .new, context: nil)
print("setup: \(currentURL)")
load(url: userSetURL, in: webView)
return webView
}
fileprivate func load(url:URL?, in webView:WKWebView) {
if let url = url {
print("load url....: \(url)")
let req = URLRequest(url: url)
webView.load(req)
} else {
print("load url google case...")
let req = URLRequest(url: defaultURL)
webView.load(req)
}
}
func updateUIView(_ webView: WKWebView, context: Context) {
print("updateUIView: \(userSetURL)")
load(url: userSetURL, in: webView)
}
}
How can I change things so that I can drive a ProgressView with the built in WKWebView estimatedProgress property without getting stuck in a View update cycle?
You have a circular dependency -- you've defined estimatedProgress as #State and then sent it to the WebView as a #Binding. The WebView updates estimatedProgress, which then re-renders the view (since the state is updated). In WebView, you're calling load in updateUIView which is called every time the WebView re-renders with new input (ie one of its Bindings has changed).
The easiest fix is to just remove the load call from updateUIView. But, that would have the side effect of now updating the WebView in the case that you wanted to change the URL.
Another option is to store the state in an ObservableObject and only pass the trait that necessitates an update (I assume userSetURL) to the WebView:
class WebViewState : ObservableObject {
#Published var url:URL?
#Published var userSetUrl:URL?
#Published var showLoader:Bool?
#Published var estimatedProgress:Double?
}
struct TestWContainer: View {
#StateObject var webViewState = WebViewState()
var body: some View {
ZStack {
WebView(webViewState : webViewState, userSetURL: webViewState.userSetUrl)
if let estimatedProgress = webViewState.estimatedProgress {
if estimatedProgress > 0 && estimatedProgress < 1 {
let _ = print("estimatedPogress: \(estimatedProgress)")
VStack(spacing:0) {
ProgressView(value: estimatedProgress, total: 1)
.frame(height: 3)
Spacer()
}
}
}
}
}
}
struct WebView: UIViewRepresentable {
var webViewState : WebViewState
var userSetURL: URL?
fileprivate let defaultURL:URL = URL(string: "https://www.google.com")!
class Coordinator: NSObject, WKNavigationDelegate {
var parent: WebView
var webViewNavigationSubscriber: AnyCancellable? = nil
init(_ webView: WebView) {
self.parent = webView
}
deinit {
webViewNavigationSubscriber?.cancel()
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
parent.webViewState.showLoader = false
}
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
parent.webViewState.showLoader = false
}
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
parent.webViewState.showLoader = true
}
// This function is essential for intercepting every navigation in the webview
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
decisionHandler(.allow)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
dispatchPrecondition(condition: .onQueue(.main))
print("Observing keyPath: \(keyPath). change: \(change). object: \(object)")
guard let wv = object as? WKWebView else { return }
if keyPath == #keyPath(WKWebView.estimatedProgress) {
print("O: progress: \(wv.estimatedProgress)")
DispatchQueue.main.async {
self.parent.webViewState.estimatedProgress = wv.estimatedProgress
}
}
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeUIView(context: Context) -> WKWebView {
let configuration = WKWebViewConfiguration()
let webView = WKWebView(frame: CGRect.zero, configuration: configuration)
webView.navigationDelegate = context.coordinator
webView.allowsBackForwardNavigationGestures = true
webView.scrollView.isScrollEnabled = true
webView.addObserver(context.coordinator, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: .new, context: nil)
print("setup: \(webViewState.url)")
load(url: userSetURL, in: webView)
return webView
}
fileprivate func load(url:URL?, in webView:WKWebView) {
if let url = url {
print("load url....: \(url)")
let req = URLRequest(url: url)
webView.load(req)
} else {
print("load url google case...")
let req = URLRequest(url: defaultURL)
webView.load(req)
}
}
func updateUIView(_ webView: WKWebView, context: Context) {
print("updateUIView: \(userSetURL)")
load(url: userSetURL, in: webView)
}
}
You could also use a similar strategy with Combine to watch an updated property from within the WebView or it's coordinator, but that may be overkill for this situation.
I have a problem where my webView doesn't load on build but in preview it works as it should. What I mean by not loading is that the webView is just white.
What I first thought was that the simulated iphones network settings made it so it didn't allow URLRequests for some reason but I disputed this quickly when I temporarily changed the url to "https://google.com" and it loaded as it should.
Here is my code:
//
// ContentView.swift
// spotifystats
//
// Created by bappo on 2021-08-15.
//
import SwiftUI
import WebKit
struct SpotifyConstants {
static let CLIENT_ID = "***************"
static let SESSION_KEY = "spotifySessionKey"
static let REDIRECT_URI = "spotifystats://"
static let SCOPE = "user-read-email"
}
struct WebView : UIViewRepresentable {
let request: URLRequest
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.load(request)
}
}
struct ContentView: View {
#State var isLogginIn = false
let authURLFull = "https://accounts.spotify.com/authorize?response_type=token&client_id=" + SpotifyConstants.CLIENT_ID + "&scope=" + SpotifyConstants.SCOPE + "&redirect_uri=" + SpotifyConstants.REDIRECT_URI + "&show_dialog=false"
var body: some View {
Button("Spotify Login") {
isLogginIn = true
}
.padding()
.foregroundColor(.white)
.background(Color.green)
.clipShape(Capsule())
.sheet(isPresented: $isLogginIn) {
WebView(request: URLRequest(url: URL(string: authURLFull)! ))
}
}
}
extension ContentView {
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
RequestForCallbackURL(request: navigationAction.request)
}
func RequestForCallbackURL(request: URLRequest) {
let requestURLString = (request.url?.absoluteString)! as String
print(requestURLString)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I was wondering how I can display my current URL on WKWebView and display URL on change also. Currently I added a Text() but it does not display the URL at all. Any ideas how I can fix it?
WebView
import SwiftUI
import WebKit
struct WebView : UIViewRepresentable {
let request: URLRequest
private var webView: WKWebView?
init (request: URLRequest) {
self.webView = WKWebView()
self.request = request
}
func makeUIView(context: Context) -> WKWebView {
webView?.load(request)
return webView!
}
func updateUIView(_ uiView: WKWebView, context: Context) {
}
func URL() -> String {
return (webView?.url)?.absoluteString ?? ""
}
func goBack() {
webView?.goBack()
}
func goForward() {
webView?.goForward()
}
}
MainView
struct MainView: View {
var webView: WebView = WebView(request: URLRequest(url: URL(string: "https://www.google.com")!))
var body: some View {
VStack {
//...
Text(webView.URL())
webView
Button(action: { webView.goBack() }, label: { Text("Test") })
//...
}
}
To keep track of the URL of the WKWebView, you'll need to use a WKNavigationDelegate.
You can use a Coordinator in your UIViewRepresentable for the WKNavigationDelegate and an ObservableObject with a #Published value to communicate between the WebView and your parent view:
class NavigationState : ObservableObject {
#Published var url : URL?
}
struct WebView : UIViewRepresentable {
let request: URLRequest
var navigationState : NavigationState
func makeCoordinator() -> Coordinator {
return Coordinator()
}
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
context.coordinator.navigationState = navigationState
webView.navigationDelegate = context.coordinator
webView.load(request)
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
}
class Coordinator : NSObject, WKNavigationDelegate {
var navigationState : NavigationState?
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
navigationState?.url = webView.url
}
}
}
struct ContentView: View {
#StateObject var navigationState = NavigationState()
var body: some View {
VStack(){
Text(navigationState.url?.absoluteString ?? "(none)")
WebView(request: URLRequest(url: URL(string: "https://www.google.com")!), navigationState: navigationState)
}
}
}
Update, based on comments:
class NavigationState : NSObject, ObservableObject {
#Published var url : URL?
let webView = WKWebView()
}
extension NavigationState : WKNavigationDelegate {
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
self.url = webView.url
}
}
struct WebView : UIViewRepresentable {
let request: URLRequest
var navigationState : NavigationState
func makeUIView(context: Context) -> WKWebView {
let webView = navigationState.webView
webView.navigationDelegate = navigationState
webView.load(request)
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) { }
}
struct ContentView: View {
#StateObject var navigationState = NavigationState()
var body: some View {
VStack(){
Text(navigationState.url?.absoluteString ?? "(none)")
WebView(request: URLRequest(url: URL(string: "https://www.google.com")!), navigationState: navigationState)
HStack {
Button("Back") {
navigationState.webView.goBack()
}
Button("Forward") {
navigationState.webView.goForward()
}
}
}
}
}
I am currently using WKWebView to load an HTML file rather than an URL, and I'm looking for a way to dismiss the sheet when the user navigates to a specific URL.
struct WebView: UIViewRepresentable {
#Binding var text: String
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.loadHTMLString(text, baseURL: nil)
}
}
I am using this UIViewRepresentable to load the HTML string, and I am using this sheet to display the WebView:
.sheet(isPresented: $isSheetPresented, onDismiss: {
self.checkthreed()
}, content: {
WebView(text: $decodedString)
})
The user can navigate to 2 URLs:
example.com/failure
example.com/success
How can I dismiss the sheet when the user navigates to either of these URLs?
Here's a very basic very that you can adjust to fit your needs:
struct WebView: UIViewRepresentable {
#Binding var text: String
var closeFunction : (() -> Void)?
class Coordinator : NSObject, WKNavigationDelegate {
var closeFunction : (() -> Void)?
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
if let urlStr = navigationAction.request.url?.absoluteString {
if urlStr == "test" {
closeFunction?()
}
}
decisionHandler(.allow)
}
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.navigationDelegate = context.coordinator
context.coordinator.closeFunction = closeFunction
uiView.loadHTMLString(text, baseURL: nil)
}
}
struct ContentView: View {
#State var sheetPresented = true
var body: some View {
Text("Hi")
.sheet(isPresented: $sheetPresented, content: {
WebView(text: .constant("Test link<br>Test 2"),
closeFunction: {
sheetPresented = false
})
})
}
}
The WKWebView gets a WKNavigationDelegate attached to it where it can receive notifications about what URL is being loaded. You can see in my example that "test" triggers the close while "test2" does not.
The WKNavigationDelegate is part of a Coordinator for the UIViewRepresentable. Note that I made closeFunction an optional closure, so you have to make sure to set it, or nothing will happen. Another route to take would be to pass the Binding<Bool> for the sheet being presented and manipulate that directly.