How to pass function to webpage using swift? - ios

I have a situation where I need to capture an event from a button click in a WebPage.
this code is on the webpage on a button click.
okCoolClick = () => {
/*eslint-disable* /
console.log("okay cool ", window.okCool);
if (window.okCool) {
okCool.performClick();
} else {
window.open("some-url");
}
};
Now the Android team is done with the problem, with below code.
commonWebView.addJavascriptInterface(object : Any() {
#JavascriptInterface
fun performClick() {
val ownerId = arguments?.getInt(OWNER_ID)
if (ownerId == -1) {
context?.startActivity<RightNavigationActivity>()
activity?.finishAffinity()
} else {
context?.startActivity<HomeItemListActivity>(
Pair(HomeItemListActivity.INTENT_EXTRA_COMPONENT,
AppConstants.HOME_SUB_LIST_CLUB_OUTLET_LIST),
Pair(HomeItemListActivity.INTENT_EXTRA_CLUB_ID, ownerId),
Pair(HomeItemListActivity.INDUSTRY_ID, -1)
)
activity?.finish()
}
}
}, "okCool")
So, Now I'm left with finding the solution in iOS,
How can I achieve this on my iOS project? Any help or node toward the correct direction would be great.

In ViewDidLoad() add this
let config = WKWebViewConfiguration()
let contentController = WKUserContentController()
contentController.add(self, name: "function")
config.userContentController = contentController
webView = WKWebView(frame: .zero, configuration: config)
add two methods from WKNavigationDelegate delegate:
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: #escaping (WKNavigationResponsePolicy) -> Void) {
return decisionHandler(.allow)
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void)
{
if navigationAction.navigationType == WKNavigationType.linkActivated, let url = navigationAction.request.url{
DeepLink.deepLink(url.absoluteString)
}
return decisionHandler(.allow)
}
Finally add the most important method to get callback from WKScriptMessageHandler delegate :
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage){
let dataFromWeb = message.body //Data Passed from webview
}
Pass data from html like this
var myObj = {"name":"John", "age":30, "car":null};
window.webkit.messageHandlers.function.postMessage(myObj);
"function" name should be same in the above statement and when u add userContentController as a config for your webview.

You can use this WebViewJavascriptBridge for sending messages between Obj-C/Swift and JavaScript.
Also you can do it without using any Third Party read this.

Related

Swift 5 Disable Redirection from WKWebView to different Apps

I have a WKWebView inside my app. The problem is that when for example going on https://stockx.com/de-de and the user has the StockX app, the user is redirected to the app.
Is there any way to disable this redirection in genreal for my webView? I couldn't find anything on this... Not too sure if you need code but here is my webView:
lazy var webView: WKWebView = {
let webConfiguration = WKWebViewConfiguration()
let webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.uiDelegate = self
webView.translatesAutoresizingMaskIntoConstraints = false
return webView
}()
Apparently there is this function for an older Swift version:
func webView(webView: WKWebView, decidePolicyForNavigationAction navigationAction: WKNavigationAction, decisionHandler: ((WKNavigationActionPolicy) -> Void)) {
print("webView:\(webView) decidePolicyForNavigationAction:\(navigationAction) decisionHandler:\(decisionHandler)")
let app = UIApplication.sharedApplication()
let url = navigationAction.request.URL
let myScheme: NSString = "https"
if (url!.scheme == myScheme) && app.canOpenURL(url!) {
print("redirect detected..")
// intercepting redirect, do whatever you want
app.openURL(url!) // open the original url
decisionHandler(.Cancel)
return
}
decisionHandler(.Allow)
}
But this is no longer working in Swift 5. Any idea how to to this in Swift 5?
So what is happening here it's web app initializing universal link. What you can theoretically do to block this:
Reference to this answer
func webView(webView: WKWebView, decidePolicyForNavigationAction navigationAction: WKNavigationAction, decisionHandler: ((WKNavigationActionPolicy) -> Void)) {
print("webView:\(webView) decidePolicyForNavigationAction:\(navigationAction) decisionHandler:\(decisionHandler)")
let app = UIApplication.sharedApplication()
let url = navigationAction.request.URL
let myScheme: NSString = "https"
if (url!.scheme == myScheme) && app.canOpenURL(url!) {
print("redirect detected..")
// intercepting redirect, do whatever you want
app.openURL(url!) // open the original url
decisionHandler(.Cancel)
return
}
decisionHandler(.Allow)
}

How to detect AVplayer and get url of current video from WKWebView?

I'm using below code to extract url from UIWebView: it is working fine but, this same code using for WKWebView it's not working anymore. Can anyone help me? The video playing in WKWebView is Inlineplacyback not in fullscreen.
My code is :
NotificationCenter.default.addObserver(self, selector: #selector(self.playerItemBecameCurrent(_:)), name: NSNotification.Name("AVPlayerItemBecameCurrentNotification"), object: nil)
#objc func playerItemBecameCurrent(_ sender : NSNotification){
let playerItem: AVPlayerItem? = sender.object as? AVPlayerItem
if playerItem == nil {
print("player item nil")
return
}
// Break down the AVPlayerItem to get to the path
let asset = playerItem?.asset as? AVURLAsset
let url: URL? = asset?.url
let path = url?.absoluteString
print(path!,"video url")
}
Response URL :
https://r2---sn-po4g5uxa-5hql.googlevideo.com/videoplayback?txp=5531432&sparams=clen%2Cdur%2Cei%2Cgir%2Cid%2Cinitcwndbps%2Cip%2Cipbits%2Citag%2Clmt%2Cmime%2Cmm%2Cmn%2Cms%2Cmv%2Cpcm2%2Cpl%2Cratebypass%2Crequiressl%2Csource%2Cexpire&ip=103.37.181.55&ratebypass=yes&id=o-AM9UWIaxopyYZX4gikGuswG8EMi3dhH_PPBMIqY5cbXj&expire=1554400796&c=MWEB&fvip=4&initcwndbps=481250&ipbits=0&mime=video%2Fmp4&dur=60.093&lmt=1554142002789460&key=yt6&mt=1554379078&itag=18&source=youtube&gir=yes&requiressl=yes&signature=6C68366FC249958BB8E95A5D88074FF8BCB99745.DA113E66DD0B46863BAE52DAA3CAB31FD141F0E5&clen=2708520&mm=31%2C29&mn=sn-po4g5uxa-5hql%2Csn-cvh7knek&ei=vPGlXPOWHIWD8QOO1KBo&ms=au%2Crdu&pcm2=no&pl=24&mv=m&cpn=I9d32bNmeq3kf0jn&cver=2.20190403&ptk=youtube_none&pltype=contentugc
It's video URL not Webpage URL so, please help me how to get this.
Thanks.
This is kind of a hack, but the only way I found to accomplish this.
First set yourself as WKWebView navigation delegate:
self.webView?.navigationDelegate = self
Now listen to all navigation changes, and save the requested url:
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
if let urlStr = navigationAction.request.url?.absoluteString {
//Save presented URL
//Full path can be accessed via self.webview.url
}
decisionHandler(.allow)
}
Now you only need to know when does the new screen become visible, and use the URL you saved (To know the video URL of the new visible screen).
You can do this via listening to UIWindowDidBecomeVisibleNotification notification:
NotificationCenter.default.addObserver(self, selector: #selector(windowDidBecomeVisibleNotification(notif:)), name: NSNotification.Name("UIWindowDidBecomeVisibleNotification"), object: nil)
Then check if the navigation window is not your window, and that means a new screen did open:
#objc func windowDidBecomeVisibleNotification(notif: Notification) {
if let isWindow = notif.object as? UIWindow {
if (isWindow !== self.view.window) {
print("New window did open, check what is the currect URL")
}
}
}
You can try to inject JS in your WKWebView like shown here: https://paulofierro.com/blog/2015/10/12/listening-for-video-playback-within-a-wkwebview
retrieves the full URL from the request property of the navigation action in the webView(_:decidePolicyFor:decisionHandler:) method of WKNavigationDelegate.
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
if let urlStr = navigationAction.request.url?.absoluteString {
//urlStr is your URL
}
decisionHandler(.allow)
}
also don't forgot to conform protocol
webView.navigationDelegate = self
Using Swift
You can get the html content from url of webview using code below
let docString = webView.stringByEvaluatingJavaScriptFromString("document.documentElement.outerHTML")
This case you'll get the whole html content,
Then look for href links inside the html string
let regex = try! NSRegularExpression(pattern: "<a[^>]+href=\"(.*?)\"[^>]*>")
let range = NSMakeRange(0, docString.characters.count)
let matches = regex.matches(in: docString, range: range)
for match in matches {
let htmlLessString = (docString as NSString).substring(with: match.rangeAt(1))
print(htmlLessString)
}
Check if it is youtube url using
Regular expression: "#https?://(www.)?youtube.com/.[^\s.,"\']+#i"
Another way to achive this
Think outside the box!
You can make an api call to get the url. That seems pretty easy using the web languages like php, .net etc.
The code for getting all urls inside a webpage in PHP (Use whatever language that is okey for you)
$url="http://wwww.somewhere.com";
$data=file_get_contents($url);
$data = strip_tags($data,"<a>");
$d = preg_split("/<\/a>/",$data);
foreach ( $d as $k=>$u ){
if( strpos($u, "<a href=") !== FALSE ){
$u = preg_replace("/.*<a\s+href=\"/sm","",$u);
$u = preg_replace("/\".*/","",$u);
print $u."\n";
}
}
To check one by one if it is youtube url.
$sText = "Check out my latest video here http://www.youtube.com/?123";
preg_match_all('#https?://(www\.)?youtube.com/.[^\s.,"\']+#i', $sText, $aMatches);
var_dump($aMatches);
If you wanted to check if the sample apps are using the same method, get a web debugging proxy and dig on it
Many of the above explanations are taken from other sites.
I Hope it sum up to your need!
Happy coding!
Try this in your ViewController, to add an URL Observer on WKWebView:
override func loadView() {
let webConfig = WKWebViewConfiguration()
webView = WKWebView(frame: .zero, configuration: webConfig)
webView.addObserver(self, forKeyPath: "URL", options: .new, context: nil)
view = webView
}
Overriding the observeValue to get url request:
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == #keyPath(WKWebView.url) {
let urlRequest:String = webView.url?.absoluteString ?? ""
print(urlRequest)
}
}
Finally... deinit the Observer:
deinit { webView.removeObserver(self, forKeyPath: "URL") }
In WKWebView we need add the configuration for Inlineplacyback is true in WKWebViewConfiguration. If the configuration is set in WKWebView its automatically takes to full screen view.
Below code for reference:
class WebViewController: UIViewController {
lazy var webView: WKWebView! = {
let webView = WKWebView(frame: .zero, configuration: configuration)
webView.translatesAutoresizingMaskIntoConstraints = false
webView.uiDelegate = self
webView.navigationDelegate = self
let request = URLRequest(url: .init(string: "https://developer.apple.com/videos/play/wwdc2020/10188/")!)
webView.load(request)
return webView
}()
lazy var configuration: WKWebViewConfiguration! = {
let configuration = WKWebViewConfiguration()
configuration.allowsInlineMediaPlayback = true
configuration.mediaTypesRequiringUserActionForPlayback = .audio
configuration.allowsPictureInPictureMediaPlayback = true
return configuration
}()
override func loadView() {
super.loadView()
self.view.backgroundColor = .white
self.view.addSubview(self.webView)
// Constraint
NSLayoutConstraint.activate([
self.webView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
self.webView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor),
self.webView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor),
self.webView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor),
])
}
}
extension WebViewController: WKUIDelegate{
}
extension WebViewController: WKNavigationDelegate{
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse) async -> WKNavigationResponsePolicy {
debugPrint("---------------------------- decidePolicyFor navigationResponse")
return .allow
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: #escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) {
debugPrint("---------------------------- decidePolicyFor navigationAction")
decisionHandler(.allow, .init())
}
}
Click here to see sample output video

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 goBack requiring two invocations

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;
}
}

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)

Resources