Subclass UIWebView to Append Every Link (in Swift) - ios

I was wondering how I could subclass UIWebView to append every link the user goes to with a string at the end like "=Human". To be clear, if the user goes to Yahoo.com and then clicks on any link it will still have the "=Human" at the end.

I could subclass UIWebView
But the docs say:
A WKWebView object displays interactive web content, such as for an
in-app browser.
For new development, employ this class instead of the older UIWebView
class.
This works for me:
import UIKit
import WebKit
class WebViewController: UIViewController,
WKNavigationDelegate {
var webView: WKWebView!
override func loadView() {
let webView = WKWebView()
webView.navigationDelegate = self
view = webView
let defaultUrl = NSURL(
string: "https://www.yahoo.com"
)
if let validDefaultUrl = defaultUrl {
let yahooRequest = NSURLRequest(URL: validDefaultUrl)
webView.loadRequest(yahooRequest)
}
}
//MARK: - WKNavigationDelegate methods:
func webView(webView: WKWebView,
decidePolicyForNavigationAction navigationAction: WKNavigationAction,
decisionHandler: (WKNavigationActionPolicy) -> Void)
{
print("[me]: In WKWebView delegate method")
if navigationAction.navigationType == .LinkActivated {
print("[me]: User clicked on a link.")
let request = navigationAction.request
if let url = request.URL,
newURL = NSURL(string: url.absoluteString + "=User")
{
print("[me]: New url is valid.")
print("[me]: New url is \(newURL.absoluteString)")
let newRequest = NSMutableURLRequest(URL: newURL)
decisionHandler(.Cancel)
webView.loadRequest(newRequest)
}
}
decisionHandler(.Allow)
}
}
Edit: Actually, the code does not enter the if navigationAction.navigationType == .LinkActivated block every time a link is clicked for some reason. Sometimes I see just the output:
[me]: In WKWebView delegate method
and I fail to see the output:
[me]: User clicked on a link.

Related

Write Unit Tests to validate WKWebView load request

I have a WKWebView to loads a basic url request.
extension ViewController: WKUIDelegate, WKScriptMessageHandler, WKNavigationDelegate {
func webView(
_ webView: WKWebView,
createWebViewWith configuration: WKWebViewConfiguration,
for navigationAction: WKNavigationAction,
windowFeatures: WKWindowFeatures
) -> WKWebView? {
if navigationAction.targetFrame == nil, let url = navigationAction.request.url {
if url.description.lowercased().range(of: "http://") != nil ||
url.description.lowercased().range(of: "https://") != nil ||
url.description.lowercased().range(of: "mailto:") != nil {
UIApplication.shared.open(url)
}
}
return nil
}
func openSite() {
guard let myUrl = URL(string: "https://www.myurl.com") else { return }
let request = URLRequest(url: myUrl)
webView?.load(request)
self.webView.sizeToFit()
}
Now I want to write a unit test to verify webview correctly load the request. I have followed this approach https://stackoverflow.com/a/63827560/627667 and created a mock navigation action.
func test_AllowsCorrectURL() {
let action = MockNavigationAction()
action.mockedRequest = URLRequest(url: URL(string: "https://www.myurl")!)
let allowExpectation = expectation(description: "Allows action")
viewController.webView(WKWebView(), decidePolicyFor: action) { policy in
XCTAssertEqual(policy, .allow)
allowExpectation.fulfill()
}
waitForExpectations(timeout: 1.0)
}
However on this line viewController.webView(WKWebView(), decidePolicyFor: action) I am getting below error.
Cannot call value of non-function type 'WKWebView?'
Swift version 5. How to get rid of this error? Your suggestions highly appreciated.
I'm almost sure that this is related to you adding implementation for another method of WKNavigationDelegate to your vc. It has few of them containing decidePolicyFor parameter label and if you will omit the implementation for the one that you are trying to call in here you won't be able to do so.
The reason of that (to my understanding) is that these are methods marked as optional on the protocol so your class doesn't need to implement them and if compiler won't find implementation of the method that you try to call it will complete with the error.
Since you shared a concrete example, you're basing your solution on check if your vc is having a method with exactly this signature:
extension ViewController: WKNavigationDelegate {
func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
// ...
}
}

why I can’t open a App Store link inside a web view?

I'm getting always this error :
WebPageProxy::didFailProvisionalLoadForFrame: frameID=3, domain=WebKitErrorDomain, code=102
Normal links are working but the AppStore one is not working
what I want is the Link to open the AppStore I can't do it locally because the web is loaded from a Qualtrics web.
I try it adding the navigationAction function but that doesn't work, what I'm guessing is that maybe the request is taking some time and i need a way of load that data on an async way but to be honest i really dont know
import SwiftUI
import WebKit
struct WebView: UIViewRepresentable {
let html = """
Appstore link dont open</span></span><br />
Normal link </span></span><br />
"""
var loadStatusChanged: ((Bool, Error?) -> Void)? = nil
func makeCoordinator() -> WebView.Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> WKWebView {
let view = WKWebView()
view.navigationDelegate = context.coordinator
view.loadHTMLString(html, baseURL: nil)
return view
}
func updateUIView(_ uiView: WKWebView, context: Context) {
}
class Coordinator: NSObject, WKNavigationDelegate {
let parent: WebView
init(_ parent: WebView) {
self.parent = parent
}
}
}
struct ContentView: View {
var body: some View {
WebView()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Some links on tapping them might activate actions / redirect with url schemes that are non HTTPs like
_blank to open a new tab
mailto to launch the mail application
some other deep link techniques familiar to device OSs
I believe the app store link uses a combination of the above and WKWebView cannot handle non HTTPs schemes.
What you can do is to listen to URLs that fail using WKNavigationDelegate and handle them accordingly
I am not using SwiftUI but I think you can get the picture.
Set up using the same HTML as you with both the links
class ViewController: UIViewController, WKNavigationDelegate
{
override func viewDidAppear(_ animated: Bool)
{
super.viewDidAppear(animated)
let html = """
Appstore link dont open</span></span><br />
Normal link </span></span><br />
"""
let webview = WKWebView()
webview.frame = view.bounds
webview.navigationDelegate = self
view.addSubview(webview)
webview.loadHTMLString(html, baseURL: nil)
}
}
Then I implement these WKNavigationDelegate functions
decidePolicyFor navigationAction (documentation link) to allow even urls that do not follow the HTTPs scheme to be allowed to be processed
this navigation fail delegate function webView didFailProvisionalNavigation and check if iOS can handle the open in a new tab, mail, deep link etc so in your case it would open the app store
You could also implement the same logic as point 2 in this
WKNavigationDelegate function just in case
// MARK: WKNavigationDelegates
func webView(_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: #escaping (WKNavigationActionPolicy) -> Void)
{
decisionHandler(.allow)
}
func webView(_ webView: WKWebView,
didFailProvisionalNavigation navigation: WKNavigation!,
withError error: Error)
{
manageFailedNavigation(webView,
didFail: navigation,
withError: error)
}
func webView(_ webView: WKWebView,
didFail navigation: WKNavigation!,
withError error: Error)
{
manageFailedNavigation(webView,
didFail: navigation,
withError: error)
}
private func manageFailedNavigation(_ webView: WKWebView,
didFail navigation: WKNavigation!,
withError error: Error)
{
// Check if this failed because of mailto, _blank, deep links etc
// I have commented out how to check for a specific case like open in a new tab,
// you can try to handle each case as you wish
if error.localizedDescription
== "Redirection to URL with a scheme that is not HTTP(S)"
//let url = webView.url, url.description.lowercased().range(of: "blank") != nil
{
// Convert error to NSError so we can access the url
let nsError = error as NSError
// Get the url from the error
// This key could change in future iOS releases
if let failedURL = nsError.userInfo["NSErrorFailingURLKey"] as? URL
{
// Check if the action can be handled by iOS
if UIApplication.shared.canOpenURL(failedURL)
{
// Request iOS to open handle the link
UIApplication.shared.open(failedURL, options: [:],
completionHandler: nil)
}
}
}
}
Give this a go and check if this fixes your issue. On my side, both links seem to work fine:

Tracking navigation in WKWebView doesn't work for button click

I am trying to catch a button click in my web view. Button "action" if I can say so, triggers certain url... I am not really able to catch this moment through WKNavigationDelegate. webview is set like this:
lazy var advertWebKitView: WKWebView = {
let webConfiguration = WKWebViewConfiguration()
let preferences = WKPreferences()
preferences.javaScriptEnabled = true
webConfiguration.preferences = preferences
let webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.navigationDelegate = self
webView.uiDelegate = self
webView.translatesAutoresizingMaskIntoConstraints = false
return webView
}()
I am trying to catch that button click like this:
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
decisionHandler(.allow)
guard let urlAsString = navigationAction.request.url?.absoluteString.lowercased() else {
return
}
if urlAsString.range(of: "...") != nil {
}
}
but this method doesn't trigger when button is clicked.
On android, it works and its done like this:
#Override
public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) {
if (url.equals("...")) {
finish();
}
//...
super.doUpdateVisitedHistory(view, url, isReload);
}
what would be equal method and a way to track navigation correctly through wkwebview?
EDIT:
if there is some info needed about the webpage structure or so, please ask for details and I will add that too. I just didn't know which info may be relevant or helpful.
The button html looks like this:
<a class="lc-button lc-button--light" href="/close">OK</a>
Also one interesting thing is, that I have tried to use my code on other sites and navigation delegate methods were triggered correctly. Which leads me that problem is with our site and its html code or something about how the page is loaded after button is clicked, maybe...
I am using this myself and it works, it is slightly different than what you are trying until now but the right equivalent to the Android method.
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
if let requestURL = navigationAction.request.url, requestURL.pathComponents.contains("...") {
//Do your things
decisionHandler(.cancel)
return
}
decisionHandler(.allow)
}
Use RedirectTo instead of LinkTo in react router for redirecting to /close
First set delegate in viewDidLoad,
self.webView.navigationDelegate = self
You can catch where your button is navigating you to by implementing
WKNavigationDelegate function, didFinish navigation
extension YourViewController: WKNavigationDelegate{
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
if let currentURL = webView.url{
if self.urlString == currentURL.absoluteString{
print(self.urlString)
}
}
}
}

Problem with navigation in WKWebView Swift

I am using WKWebView for my WebViewController in my app and I spot one problem with one URL
"http://www.viator.com/tours/San-Francisco/San-Francisco-Vista-Grande-Helicopter-Tour/d651-3538VISTAGRANDE?eap=brand-subbrand-75523&aid=vba75523en" there is no navigation through the web site when I tap some other event page nothing happens. Does anyone have any ideas about how to deal with that?
If you're talking about links opening in a new window, indeed, they don't create a new navigation action. But you can still intercept them, and load the request manually.
If your webview does not have a custom uiDelegate already, assign it to your view controller or something else, and then do something like this:
extension SomeViewController: WKUIDelegate {
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
// Intercept target=_blank links
guard
navigationAction.targetFrame == nil,
let url = navigationAction.request.url else {
return nil
}
let request = URLRequest(url: url)
webView.load(request)
return nil
}
}

Disable Youtube app redirect from WKWebview in swift

I developed simple web browser with WKWebView in Swift.
When I click Youtube link, Youtube app is auto launched.
I hope to play Youtube video inside my web browser.
I don't need to launch Youtube app.
Please help me.
Here are my sample code.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let webViewConfiguration = WKWebViewConfiguration()
webViewConfiguration.allowsInlineMediaPlayback = true
wkWebView.configuration.allowsInlineMediaPlayback = true
wkWebView.navigationDelegate = self
let myURL = URL(string: "https://www.google.com")
let youtubeRequest = URLRequest(url: myURL!)
wkWebView.load(youtubeRequest)
}
Yes, you can do this.
First you need to set up a WKNavigationDelegate in your webview, then in webview:decidePolicyFor method cancel any link activated youtube requests, and force it to load within your webview.
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
if navigationAction.navigationType == .linkActivated && navigationAction.request.url?.host?.hasSuffix("youtube.com") {
decisionHandler(.cancel)
DispatchQueue.main.async {
webView.load(navigationAction.request)
}
} else {
decisionHandler(.allow)
}
}
Unfortunately you can't do that programatically. Following are the steps to achieve something like you want manually.
Open youtube app from Home screen
Tap on your profile picture on the upper right corner.
Go to Settings and tap on Google app Settings
Check the Safari Option.

Resources