WKWebView goBack requiring two invocations - ios

I'm working on a iOS app that uses WKWebView, with custom navigation buttons. We're finding that following some links will require using our Back button twice, as the first invocation seems to just reload the current page. This does not happen in mobile Safari.
What could cause goBack() to need to be called twice to actually navigate back one page, while it works correctly in Safari? Are there changes I can make in the app to correct the issue?
Note: I cannot make changes to the web site's content or structure, so any fixes would have to be in the app.
Update:
It appears the site is using JavaScript for some of its content loads. The observeValue method is not invoked at all when this happens and is probably the culprit.
import UIKit
import WebKit
import SafariServices
class LandingPageViewController: UIViewController, SFSafariViewControllerDelegate, WKUIDelegate, WKNavigationDelegate {
#IBOutlet var urlNavigationItem: UINavigationItem!
#IBOutlet var backButton: UIBarButtonItem!
#IBOutlet var forwardButton: UIBarButtonItem!
#IBOutlet var reloadButton: UIBarButtonItem!
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
webView = WKWebView()
view.addSubview(webView)
webView.addObserver(self, forKeyPath: "loading", options: .new, context: nil)
let request = URLRequest(url: url) //url is defined elsewhere
webView.navigationDelegate = self
webView.load(request)
webView.allowsBackForwardNavigationGestures = true
webView.uiDelegate = self
backButton.isEnabled = false
forwardButton.isEnabled = false
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if(keyPath == "loading"){
backButton.isEnabled = webView.canGoBack
forwardButton.isEnabled = webView.canGoForward
}
}
#IBAction func back(_ sender: UIBarButtonItem) {
webView.goBack()
}
#IBAction func forward(_ sender: UIBarButtonItem){
webView.goForward()
}
#IBAction func reload(_ sender: UIBarButtonItem) {
let request = URLRequest(url: webView.url!)
webView.load(request)
}
func webView(_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
// Custom logic here
}
}

try with following delegate methods,
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
decisionHandler(.allow)
}
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: #escaping (WKNavigationResponsePolicy) -> Void) {
decisionHandler(.allow)
}
You have implemented one but not call decisionHandler(.allow)

I was facing the same issue. What worked for me was stopping the navigation and then go back.
func goBack() {
self.webView.stopLoading()
if (self.webView.canGoBack) {
self.webView.goBack()
return;
}
}

Related

Webview Back & Forward Button Not working (Swift, Xcode 14)

My webView Back & Forward buttons used to work but they are not anymore. I've looked everywhere I could for a solution but only found similar ones including here but nothing works for me or for Swift (at least to me). Below are my codes.I have Webkit Framework added, still not working when the buttons did work before even without adding the Framework. App Transport Security setting is set to YES for Arbitrary Load as before when the buttons were working. Did Apple change something? Please help. Thanks!!!
import UIKit
import WebKit
class SocialPlatformController: UIViewController, WKNavigationDelegate {
#IBOutlet weak var BackBtn: UIButton!
#IBOutlet weak var ForwardBtn: UIButton!
#IBOutlet weak var webView: WKWebView!
#IBAction func backToMainScreen(_ sender: Any) {
}
#IBAction func goBackBtnTap(_ sender: Any) {
if webView.canGoBack {
webView.goBack()
}
}
#IBAction func goForwardBtnTap(_ sender: Any) {
if webView.canGoForward {
webView.goForward()
}
}
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: #escaping (WKNavigationResponsePolicy) -> Swift.Void) {
BackBtn.isEnabled = webView.canGoBack
ForwardBtn.isEnabled = webView.canGoForward
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
HTTPCookieStorage.shared.setCookie(cookie)
}
}
decisionHandler(.allow)
}
override func viewDidLoad() {
super.viewDidLoad()
webView.navigationDelegate = self
let url:URL = URL(string:"https://www.facebook.com/groups/246328283932460")!
let urlRequest:URLRequest = URLRequest(url: url)
webView.load(urlRequest)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
}
Perhaps it's better to handle the Back and Forward button state in a navigation delegate handler that's fired after the navigation has been finished, for example:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!)
//successful load
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
updateButtons()
}
//failure load
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
updateButtons()
}
//back-forward button update
func updateButtons() {
backButton.isEnabled = webView.canGoBack
forwardbutton.isEnabled = webView.canGoForward
}
You can follow these delegates and methods. I think buttons are not getting updated/loaded as they should.

How do I open phone from tel: url link from WebKit iOS app using Swift

I am creating an iPhone app and in part using WebKit to display a web page in the app. Currently, when I click a button that is displayed on the website it should open the phone app (with a tel: link) and dial the number. Unfortunately in the app, it does nothing, however, it works fine in chrome/ safari.
I have tried to scour the internet and attempted several suggestions but nothing seems to do anything so far.
import UIKit
import WebKit
class ShopViewController: UIViewController, WKNavigationDelegate, WKUIDelegate {
#IBOutlet weak var backButton: UIBarButtonItem!
#IBOutlet weak var shopWebKit: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
shopWebKit.navigationDelegate = self
shopWebKit.uiDelegate = self
// Do any additional setup after loading the view.
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear( animated )
let urlString:String = "https://www.somewebsite.com"
let url:URL = URL(string: urlString)!
let urlRequest:URLRequest = URLRequest(url: url)
shopWebKit.load(urlRequest)
}
#IBAction func backButtonTapped(_ sender: Any) {
if shopWebKit.canGoBack{
shopWebKit.goBack()
}
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
backButton.isEnabled = webView.canGoBack
}
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
if navigationAction.targetFrame == nil {
webView.load(navigationAction.request)
}
return nil
}
}
I am hoping to be able to click any tel: link button loaded in the WebKit and have it open the phone app and dial the number. Please help.
Try below code it's working fine swift5. first add 'navigationDelegate' to your WKWebView.
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: (WKNavigationActionPolicy) -> Void) {
switch navigationAction.request.url?.scheme {
case "tel":
UIApplication.shared.open(navigationAction.request.url!, options: [:], completionHandler: nil)
decisionHandler(.cancel)
break
default:
decisionHandler(.allow)
break
}
}
Note: try testing with actual device.
Here is just a code to open the phone app and make calls. Probably it can help u.
if let url = URL(string: "tel:+100000000"), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}

WKWebView issues with tel: links

I've looked though a lot of the answers for this question but they seem to be outdated now and none of the solutions are working for me and just give lots of errors.
I'm just gettign into xcode and apps and am loading a local html file "index.html" into a WKWebView. This is fine, loads fine, displays as it should, BUT I have some tel: links on the page which don't work, I tap on them and nothing. I am using fraework7 for my html files and have added the "external" class, which works in safari etc.
UPDATE: Using the following code I can click on a link and it will try and call, but then gives a fatal error
import UIKit
import WebKit
class ViewController: UIViewController, WKNavigationDelegate{
#IBOutlet weak var webview: WKWebView!
override func viewDidLoad() {
webview.navigationDelegate = self
super.viewDidLoad()
let htmlpath = Bundle.main.path(forResource: "index", ofType: "html")
let url = URL(fileURLWithPath: htmlpath!)
let request = URLRequest(url: url)
webview.load(request)
webview.scrollView.bounces = false
webview.configuration.dataDetectorTypes = .phoneNumber
// Do any additional setup after loading the view, typically from a nib.
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Swift.Void) {
if navigationAction.request.url?.scheme == "tel" {
UIApplication.shared.openURL(navigationAction.request.url!)
decisionHandler(.cancel)
}
decisionHandler(.allow)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
and html looks like:
<tr>
<td>Police / Fire / Ambulance</td>
<td>Emergency Services</td>
<td>999</td>
</tr>
obviously part of a larger table but this is the example.
Any pointers would be greatly appreciated as I've been going round in circles for ages.
Managed to find a solution finally:
import UIKit
import WebKit
class ViewController: UIViewController, WKNavigationDelegate, WKUIDelegate{
#IBOutlet weak var webView: WKWebView!
override func viewDidLoad() {
webView.navigationDelegate = self
webView.uiDelegate = self
super.viewDidLoad()
let htmlpath = Bundle.main.path(forResource: "index", ofType: "html")
let url2 = URL(fileURLWithPath: htmlpath!)
let request = URLRequest(url: url2)
webView.load(request)
webView.scrollView.bounces = false
//webview.configuration.dataDetectorTypes = .phoneNumber
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
if navigationAction.request.url?.scheme == "tel" {
UIApplication.shared.openURL(navigationAction.request.url!)
decisionHandler(.cancel)
} else {
decisionHandler(.allow)
}
}
Using this seems to pick up the tel: links fine, an answer I found on here previously didn't have the else statement so fired decisionHandler more than once which created the error, the else statement fixed that and now seems fine.
It's not just tel: links that aren't handled out of the box by WKWebView. Other links like mailto: and facetime: aren't handled either. For a complete list of these kinds of special links, see Apple's documentation.
You should decide which of Apple's special links you want handled (see the link above), and override the navigation handler in a manner similar to the following:
webView.navigationDelegate = self
...
extension ViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
guard let url = navigationAction.request.url else {
decisionHandler(.allow)
return
}
if ["tel", "sms", "facetime"].contains(url.scheme) && UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
decisionHandler(.cancel)
} else {
decisionHandler(.allow)
}
}
}
The only reason I can think of why Apple didn't include this in the first place is because when you try launching one of these links in Mobile Safari, it shows you a dialog such as the following:
This website has been blocked from automatically starting a call.
[Ignore] [Allow Call]
Thus, they may expect you want to implement something similar in your app instead of just launching the call.
Note however, that each type of link behaves differently:
tel: dialog that shows the phone number with call/cancel buttons.
sms: launches the Messages app.
mailto: launches the Mail app.
So if you do want to have some kind of modal that allows the user to cancel if they didn't mean to perform the action, depending on which actions you support it might already have similar behavior (e.g. tel:).
Just use WKNavigationDelegate and dataDetectorTypes for WKWebView configuration like below:
webView.navigationDelegate = self
webView.configuration.dataDetectorTypes = [.link, .phoneNumber]
extension PDFWebViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
if let requestUrl = navigationAction.request.url, requestUrl.scheme == "tel" {
UIApplication.shared.open(requestUrl, options: [:], completionHandler: nil)
decisionHandler(.cancel)
} else {
decisionHandler(.allow)
}
}
}
While suggested answers work fine - I would recommend to use white-list approach instead of black-list. We know that WKWebView can only handle http/https links so we can gracefully handle the error of opening any other kind of link.
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
self.handleNavigationError(error)
}
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
self.handleNavigationError(error)
}
private func handleNavigationError(_ error: Error) {
if let failedUrl = (error as NSError).userInfo[NSURLErrorFailingURLErrorKey] as? URL, failedUrlScheme = failedUrl.scheme?.lowercased(), !["http", "https"].contains(failedUrlScheme) {
UIApplication.shared.open(failedUrl, completionHandler: nil)
} else {
// handle other errors if needed
}
}
In this case you will support all the types of external app links like tel:, sms:, faceTime:, itms-services: etc.

WKWebView links not working

I have build a Swift iOS app with the only purpose to display a website with the help of WKWebView.
This works fine, but pressing on links (e.g. on an [mailto:] button) doesn't work because it can't open anything!
Does anybody have a solution for this?
I have read a lot about solving this, but I don't know where to start.
Thanks for the help
[UPDATE: ]
The code below shows the solution
import UIKit
import WebKit
class ViewController: UIViewController, WKNavigationDelegate {
#IBOutlet var containerView : UIView! = nil
var webView: WKWebView?
override func loadView() {
super.loadView()
self.webView = WKWebView()
self.view = self.webView!
}
override func viewDidLoad() {
super.viewDidLoad()
webView?.navigationDelegate = self
let url = NSURL(string:"https://google.de")
let req = NSURLRequest (URL: url!)
self.webView!.loadRequest(req)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func prefersStatusBarHidden() -> Bool {
return true
}
func webView(webView: WKWebView, decidePolicyForNavigationAction navigationAction: WKNavigationAction, decisionHandler: (WKNavigationActionPolicy) -> Void) {
let url = navigationAction.request.URL?.absoluteString
let url_elements = url!.componentsSeparatedByString(":")
switch url_elements[0] {
case "mailto":
openCustomApp("mailto://", additional_info: url_elements[1])
decisionHandler(.Cancel)
default:
}
decisionHandler(.Allow)
}
func openCustomApp(urlScheme:String, additional_info:String){
if let requestUrl:NSURL = NSURL(string:"\(urlScheme)"+"\(additional_info)") {
let application:UIApplication = UIApplication.sharedApplication()
if application.canOpenURL(requestUrl) {
application.openURL(requestUrl)
}
}
}
}
You have to implement the following method (part of WebKit), which can be used to parse such URLs:
func webView(webView: WKWebView, decidePolicyForNavigationAction navigationAction: WKNavigationAction, decisionHandler: (WKNavigationActionPolicy) -> Void)
The idea is to write if-statements to check for certain URL types before passing the decision. More info here:
GitHub example
SO thread
Awesome tutorials for beginners with WKWebView at hackingwithswift
Keep in mind that some of the syntax for delegates has changed slightly in Swift 3 - something to keep in mind when using tutorials. If you have already found this but you ran into an error - post your code.
EDIT: just an update for others who stumble upon this question. Keep in mind that you need the right formatting for mailto e.g. mailto://tes#test.com (as mentioned by OhadM in the comments of the OP)

Create a WKWebView in a UIView

I'm trying to add a WKWebView inside of a UIView ,
The code seems to work fine ( the page is loaded the bounds is good the delegate print the message with no error)
but the View (called ViewForStuffInWeb ) stay blank, nothing in it
can someone explain why ?
import UIKit
import WebKit
class ViewController: UIViewController ,WKNavigationDelegate {
#IBOutlet var ViewForStuffInWeb: UIView!
var webView = WKWebView()
let request = "https://www.google.fr"
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.automaticallyAdjustsScrollViewInsets = false
let config = WKWebViewConfiguration()
webView = WKWebView(frame: ViewForStuffInWeb.bounds ,configuration : config)
webView.navigationDelegate = self
ViewForStuffInWeb = webView
println("webview : frame :\(webView.frame) , bounds : \(webView.bounds)")
requesting(request)
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
func requesting (request :String) {
if let url = NSURL(string: request) {
webView.loadRequest(NSURLRequest(URL: url))
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func webView(webView: WKWebView, decidePolicyForNavigationAction navigationAction: WKNavigationAction, decisionHandler: (WKNavigationActionPolicy) -> Void) {
println("decide policy action")
decisionHandler(WKNavigationActionPolicy.Allow)
}
func webView(webView: WKWebView, decidePolicyForNavigationResponse navigationResponse: WKNavigationResponse, decisionHandler: (WKNavigationResponsePolicy) -> Void) {
println("decide policy response")
decisionHandler(WKNavigationResponsePolicy.Allow)
}
func webView(webView: WKWebView, didCommitNavigation navigation: WKNavigation!) {
println("commit navigation")
}
func webView(webView: WKWebView, didFailNavigation navigation: WKNavigation!, withError error: NSError) {
println("fail in didFailNavigation \(error)")
}
func webView(webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: NSError) {
println("fail in didFailProvisionalNavigation \(error)")
}
func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!) {
println("finish navigation")
}
thanks for reading
You never add the WKWebView to the view hierarchy. In your viewDidAppear, which code should really move to viewDidLoad, you probably want to replace:
ViewForStuffInWeb = webView
with:
ViewForStuffInWeb.addSubview(webView)
Although it is unclear what ViewForStuffInWeb actually is. The above will only work if that view exists in your nib and if it is connected.

Resources