How to get url from webview whenever user move to other pages - swift

How can I get a URL from webView whenever the URL is changed?
I want to change the button color when current URL is changed. So I need to check the current URL all the time.
And I also would like to get URL value as a String.
I tried the below code but it doesn't work at all.
NotificationCenter.default.addObserver(self, selector: #selector(urlChecker), name: NSNotification.Name.NSURLCredentialStorageChanged, object: webView)

you can use:
1. Solution
UIWebViewDelegate
https://developer.apple.com/reference/uikit/uiwebviewdelegate/1617945-webview
optional func webView(_ webView: UIWebView,
shouldStartLoadWith request: URLRequest,
navigationType: UIWebViewNavigationType) -> Bool
UIWebViewNavigationType:
https://developer.apple.com/reference/uikit/uiwebviewnavigationtype
don't forget to return true
case linkClicked
User tapped a link.
case formSubmitted
User submitted a form.
case backForward
User tapped the back or forward button.
case reload
User tapped the reload button.
case formResubmitted
User resubmitted a form.
case other
Some other action occurred.
2. Solution
Inject Javascript JavaScript MessageHandler
(credit to Vasily Bodnarchuk)
Solution is here: https://stackoverflow.com/a/40730365/1930509
Swift 3 example.
Description
The script is inserted into page which will displayed in WKWebView.
This script will return the page URL (but you can write another
JavaScript code). This means that the script event is generated on the
web page, but it will be handled in our function:
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {...}
Full Code example
import UIKit
import WebKit
class ViewController: UIViewController, WKNavigationDelegate {
var webView = WKWebView()
let getUrlAtDocumentStartScript = "GetUrlAtDocumentStart"
let getUrlAtDocumentEndScript = "GetUrlAtDocumentEnd"
override func viewDidLoad() {
super.viewDidLoad()
let config = WKWebViewConfiguration()
config.addScript(script: WKUserScript.getUrlScript(scriptName: getUrlAtDocumentStartScript),
scriptHandlerName:getUrlAtDocumentStartScript, scriptMessageHandler:
self, injectionTime: .atDocumentStart)
config.addScript(script: WKUserScript.getUrlScript(scriptName: getUrlAtDocumentEndScript),
scriptHandlerName:getUrlAtDocumentEndScript, scriptMessageHandler:
self, injectionTime: .atDocumentEnd)
webView = WKWebView(frame: UIScreen.main.bounds, configuration: config)
webView.navigationDelegate = self
view.addSubview(webView)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
webView.loadUrl(string: "http://apple.com")
}
}
extension ViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
switch message.name {
case getUrlAtDocumentStartScript:
print("start: \(message.body)")
case getUrlAtDocumentEndScript:
print("end: \(message.body)")
default:
break;
}
}
}
extension WKUserScript {
class func getUrlScript(scriptName: String) -> String {
return "webkit.messageHandlers.\(scriptName).postMessage(document.URL)"
}
}
extension WKWebView {
func loadUrl(string: String) {
if let url = URL(string: string) {
load(URLRequest(url: url))
}
}
}
extension WKWebViewConfiguration {
func addScript(script: String, scriptHandlerName:String, scriptMessageHandler: WKScriptMessageHandler,injectionTime:WKUserScriptInjectionTime) {
let userScript = WKUserScript(source: script, injectionTime: injectionTime, forMainFrameOnly: false)
userContentController.addUserScript(userScript)
userContentController.add(scriptMessageHandler, name: scriptHandlerName)
}
}
Info.plist
add in your Info.plist transport security setting
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
Result
Resources ##
Document Object Properties and Methods

Related

Am unable to receive HTTP responses with UIViewController set as URLSessionDelegate

Wed 5/18 Additional Info added at Step 5
I am able to create a URLSesion, build a request with a file to upload and successfully call it from my app. On my server side, the proper script is called, uploaded file is saved, etc,. However, I am not receiving the HTTP responses, data, etc.
Actually had this working without the delegate, when the HTTP response functions were within the task itself. But am now trying to expand functionality and am missing something while trying implement the delegate.
The trimmed code is below, and it all works, with the exception of setting up UIViewController as the URLSession delegate. Just trying to figure out why my UIViewController is not receiving the HTTP responses.
Below is the code for:
UIViewController
Class which creates the upload session (UploadService)
Extension for
UIViewController which I want to use to process the responses
How the previous task looked, when it worked. Before I tried to implement the delegate.
Used print to confirm that my UIViewConroller is the delegate, yet it still receives no HTTP response, data, or error messages
UIViewController
class UploadInv : UIViewController {
var xFile : XFile?
...create UI....
let uploadService = UploadService()
lazy var uploadSession: URLSession = {
let configuration = URLSessionConfiguration.default
return URLSession(configuration: configuration, delegate: self, delegateQueue: .main)
}()
override func viewWillAppear(_ animated: Bool) {
...
}
override func viewDidLoad() {
super.viewDidLoad()
uploadService.uploadSession = uploadSession
... code the lays out all buttons, labels, etc...
}
#objc func buttonAction(sender: UIButton!) {
guard let theButton = sender else { return}
let myTag = theButton.tag
switch myTag {
//button to start upload
case ButtType.up.rawValue:
uploadService.start(upFile: xFile!, script: "uploadOrig.pl", upLoadInvClass: self)
uploadService.task?.resume()
//button to select file to upload
case ButtType.file.rawValue:
... file xFile with file info
}
}
UploadService
class UploadService {
var uploadSession : URLSession!
var task: URLSessionUploadTask?
func start(upFile: XFile, script: String, upLoadInvClass: UploadInv) {
var request = upFile.makeUrlReq(upFile: upFile, script: script)
task = uploadSession.uploadTask(with: request, from: request.httpBody! )
print("\(uploadSession.delegate)")
task?.resume()
}
}
extension
extension UploadInv: UIDocumentPickerDelegate, URLSessionDelegate {
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
... file xFile info for upload ....
... http request created ....
}
// Below are the three simple functions which I would handle
// responses the server, but these never seem to get called.
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let err = error {
print("Error: \(err.localizedDescription)")
}
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: (URLSession.ResponseDisposition) -> Void) {
print("didReceive response")
completionHandler(URLSession.ResponseDisposition.allow)
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
print("didReceive data")
if let responseText = String(data: data, encoding: .utf8) {
print(responseText)
}
}
}
Pre-Delegate model which worked
class UploadService {
var uploadSession = URLSession.shared
func start(upFile: XFile, script: String, upLoadInvClass: UploadInv) {
var request = upFile.makeUrlReq(upFile: upFile, script: script)
uploadSession.uploadTask(with: request, from: request.httpBody )
{ (data, response, error) in
if let response = response {
upLoadInvClass.upResp(resp: response)
}
if let error = error {
upLoadInvClass.upErr(error: error)
}
if let data = data {
upLoadInvClass.upData(data: data)
}
}.resume()
}
}
Step 5:
task = uploadSession.uploadTask(with: request, from: request.httpBody! )
print("\(uploadSession.delegate)")
task?.resume()
For other newbies also stuck on this, it turns out there's more than one delegate to look at. There are:
URLSessionTaskDelegate, URLSessionDataDelegate, URLSessionDownloadDelegate, and more. So obviously I was using the wrong one, might have been fell trap to "autocomplete." Nevertheless, I have to make sure I read more documentation on the subject.
Thanks to Scott who "passively/aggressively" gave me the answer, here, while still allowing me to "think." I mean that as a compliment. He told me to add the line:
assert(uploadSession.delegate! is URLSessionDataDelegate)

Support URL schemes in macOS application

There are several (old) questions on this subject, but none of the solutions worked for me, so here's the question:
How to add a URL scheme, so it will be possible to open my app via the browser?
I did the following:
Added the required info to the info.plist:
I Added those functions:
func applicationWillFinishLaunching(_ notification: Notification) {
NSAppleEventManager.shared().setEventHandler(self, andSelector: #selector(handleEvent(_:with:)), forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL))
}
#objc func handleEvent(_ event: NSAppleEventDescriptor, with replyEvent: NSAppleEventDescriptor) {
NSLog("at handleEvent")
}
I also tried to add this function:
func application(_ application: NSApplication, open urls: [URL]) {
for url in urls {
NSLog("url:\(url)")
}
}
None of the above worked. I have a webpage that redirect with MyAppLogin://test but nothing happens. It doesn't matter if the app is open or closed (I want it to work in both cases)
Any idea what's the problem here?
Edit: Two more details:
The app is sandboxed
I'm running it via Xcode (so the installation is not at the 'Applications' folder)
One year later, this is how I made it work:
Add this to your AppDelegate:
func applicationWillFinishLaunching(_ notification: Notification) {
let appleEventManager: NSAppleEventManager = NSAppleEventManager.shared()
appleEventManager.setEventHandler(self, andSelector: #selector(handleGetURLEvent(_:withReplyEvent:)), forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL))
}
Add this, to parse the URL and do whatever you want:
#objc func handleGetURLEvent(_ event: NSAppleEventDescriptor, withReplyEvent: NSAppleEventDescriptor) {
if let urlString = event.forKeyword(AEKeyword(keyDirectObject))?.stringValue {
let url = URL(string: urlString)
guard url != nil, let scheme = url!.scheme else {
//some error
return
}
if scheme.caseInsensitiveCompare("yourSchemeUrl") == .orderedSame {
//your code
}
}
}

Swift Load Website to Scrape Code Without Loading View | WebKit

I have an array of Google News article urls. Google News article urls redirect immediately to real urls, ie: CNBC.com/.... I am trying to pull out the real, redirected url. I thought I could loop through the list and load the Google News link in a WebView, then call webView.url in a DispatchQueue after 1 second to get the real url, but this doesn't work.
How could you fetch a list of redirected urls quickly?
Here's my code you could use to reproduce the problem:
let webView = WKWebView()
let myList = [URL(string: "https://news.google.com/articles/CAIiEDthIxbgofssGWTpXgeJXzwqGQgEKhAIACoHCAow2Nb3CjDivdcCMJ_d7gU?hl=en-US&gl=US&ceid=US%3Aen"), URL(string: "https://news.google.com/articles/CAIiEP5m1nAOPt-LIA4IWMOdB3MqGQgEKhAIACoHCAowocv1CjCSptoCMPrTpgU?hl=en-US&gl=US&ceid=US%3Aen")]
for url in myList {
guard let link = url else {continue}
self.webView.loadUrl(string: link.absoluteString)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
let redirectedLink = self.webView.url
print("HERE redirected url: ", redirectedLink) // this does not work
}
}
There are two problems with your attempt:
1) You're using one and the same web view in the loop and since nothing inside the loop blocks until the web view has finished loading, you just end up cancelling the previous request with every loop pass.
2) Even if you did block inside the loop, accessing the URL after a second won't work reliably since the navigation could easily take longer than that.
What I would recommend doing is to continue using a single web view (to save resources) but to use its navigation delegate interface for resolving the URLs one by one.
This is a crude example to give you a basic idea:
import UIKit
import WebKit
#objc class RedirectResolver: NSObject, WKNavigationDelegate {
private var urls: [URL]
private var resolvedURLs = [URL]()
private let completion: ([URL]) -> Void
private let webView = WKWebView()
init(urls: [URL], completion: #escaping ([URL]) -> Void) {
self.urls = urls
self.completion = completion
super.init()
webView.navigationDelegate = self
}
func start() {
resolveNext()
}
private func resolveNext() {
guard let url = urls.popLast() else {
completion(resolvedURLs)
return
}
let request = URLRequest(url: url)
webView.load(request)
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
resolvedURLs.append(webView.url!)
resolveNext()
}
}
class ViewController: UIViewController {
private var resolver: RedirectResolver!
override func viewDidLoad() {
super.viewDidLoad()
resolver = RedirectResolver(
urls: [URL(string: "https://news.google.com/articles/CAIiEDthIxbgofssGWTpXgeJXzwqGQgEKhAIACoHCAow2Nb3CjDivdcCMJ_d7gU?hl=en-US&gl=US&ceid=US%3Aen")!, URL(string: "https://news.google.com/articles/CAIiEP5m1nAOPt-LIA4IWMOdB3MqGQgEKhAIACoHCAowocv1CjCSptoCMPrTpgU?hl=en-US&gl=US&ceid=US%3Aen")!],
completion: { urls in
print(urls)
})
resolver.start()
}
}
This outputs the following resolved URLs:
[https://amp.cnn.com/cnn/2020/04/09/politics/trump-coronavirus-tests/index.html, https://www.cnbc.com/amp/2020/04/10/asia-markets-coronavirus-china-inflation-data-currencies-in-focus.html]
One other thing to note is that the redirection of those URLs in particular seems to rely on JavaScript which means you indeed need a web view. Otherwise kicking off URLRequests manually and observing the responses would have been enough.

Unable to inject JS into WKWebView in Swift/Cocoa/NextStep / Push user selection on web page in WKWebView to Swift / Cocoa

I'm working with an MacOS app which needs to use the WKUserScript capability to send a message from the webpage back to the MacOS app. I'm working with the article https://medium.com/capital-one-tech/javascript-manipulation-on-ios-using-webkit-2b1115e7e405 which shows this working in iOS and works just fine.
However I've been struggling for several weeks to try to get it to work in my MacOS. Here is my example of his code which complies fine and runs but does not successfully print the message found in the handler userContentController()
import Cocoa
import WebKit
class ViewController: NSViewController, WKNavigationDelegate {
#IBOutlet weak var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
let userContentController = WKUserContentController()
// Add script message handlers that, when run, will make the function
// window.webkit.messageHandlers.test.postMessage() available in all frames.
userContentController.add(self, name: "test")
// Inject JavaScript into the webpage. You can specify when your script will be injected and for
// which frames–all frames or the main frame only.
let scriptSource = "window.webkit.messageHandlers.test.postMessage(`Hello, world!`);"
let userScript = WKUserScript(source: scriptSource, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
userContentController.addUserScript(userScript)
// let config = WKWebViewConfiguration()
// config.userContentController = userContentController
// let webView = WKWebView(frame: .zero, configuration: config)
webView.navigationDelegate = self
webView.configuration.userContentController = userContentController
// Make sure in Info.plist you set `NSAllowsArbitraryLoads` to `YES` to load
// URLs with an HTTP connection. You can run a local server easily with services
// such as MAMP.
let htmlStr = "<html><body>Hello world - nojs</body></html>"
webView.loadHTMLString(htmlStr, baseURL: nil)
}
}
extension ViewController: WKScriptMessageHandler {
// Capture postMessage() calls inside loaded JavaScript from the webpage. Note that a Boolean
// will be parsed as a 0 for false and 1 for true in the message's body. See WebKit documentation:
// https://developer.apple.com/documentation/webkit/wkscriptmessage/1417901-body.
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if let messageBody = message.body as? String {
print(messageBody)
}
}
}
Another odd thing is that I do not seem to be able to create a simple WKWebView app that loads a page and displays it. These are all just simple tests and my main application is able to load/display webpages just fine using AlamoFire/loadHTMLString() to display pages, I just have not been able to inject the JS required.
Everything I've done in the conversion is quite straight forward and required little or no change with the exception of the assignment of the userContentController - so perhaps that's the problem? This example works just fine in iOS with his original sample as a prototype. https://github.com/rckim77/WKWebViewDemoApp/blob/master/WKWebViewDemoApp/ViewController.swift
I'm guessing there must be something very simple I'm missing here. Any help would be greatly appreciated!
Heres how I have set my WebView on Mac try something like this
import Cocoa
import WebKit
class ViewController: NSViewController {
#IBOutlet weak var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
let javascript = """
function printStatement() {
try {
window.webkit.messageHandlers
.callbackHandler.postMessage({'payload': 'Hello World!'})
} catch(err) {
console.log('The native context does yet exist')
}
}
"""
let script = WKUserScript(
source: javascript,
injectionTime: WKUserScriptInjectionTime.atDocumentEnd,
forMainFrameOnly: true
)
webView.configuration.userContentController.add(
name: "callbackHandler"
)
webView.configuration.userContentController
.addUserScript(script)
webView.navigationDelegate = self
let html = """
<div onClick='javascript:printStatement()'>Print Statement</div>
"""
webView.loadHTMLString(html, nil)
}
}
extension ViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if(message.name == "callbackHandler") {
guard let body = message.body as? [String: Any] else {
print("could not convert message body to dictionary: \(message.body)")
return
}
guard let payload = body["payload"] as? String else {
print("Could not locate payload param in callback request")
return
}
print(payload)
}
}
}
Hopefully this answered your question and works if not let me know and i'll try figure it out!
Well, as it turns out a major part of the issue was that I needed to set the entitlements for both "App Sandbox" and "com.apple.security.files.user-selected.read-only" both to "no" in the WebTest.entitlements file.
This was not the case in previous versions of XCode (I'm on V10.1) and the default values basically disabled the WKWebView for what I was trying to do with it (ie, load a simple page either via URL or String)
However, Alex's fix did help once I got that solved... with a couple small tweaks (had to add 'self' to the userContentController.add() function. Also, I added my JS for it's original purpose which was to "push" to Swift every time the user changed the selection on the page.
Here's my final code:
import Cocoa
import WebKit
class ViewController: NSViewController, WKNavigationDelegate {
#IBOutlet var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
let javascript = """
function printStatement() {
try {
var foo = window.getSelection().toString()
window.webkit.messageHandlers.callbackHandler.postMessage({'payload': foo})
} catch(err) {
console.log('The native context does yet exist')
}
}
function getSelectionAndSendMessage() {
try {
var currSelection = window.getSelection().toString()
window.webkit.messageHandlers.callbackHandler.postMessage({'payload': currSelection})
} catch(err) {
console.log('The native context does yet exist')
}
}
document.onmouseup = getSelectionAndSendMessage;
document.onkeyup = getSelectionAndSendMessage;
document.oncontextmenu = getSelectionAndSendMessage;
"""
let script = WKUserScript(
source: javascript,
injectionTime: WKUserScriptInjectionTime.atDocumentEnd,
forMainFrameOnly: true
)
webView.configuration.userContentController.add(self, name: "callbackHandler")
webView.configuration.userContentController.addUserScript(script)
webView.navigationDelegate = self
let html = """
<div onClick='javascript:printStatement()'>Print Statement</div>
This is some sample text to test select with
"""
webView.loadHTMLString(html, baseURL: nil)
}
}
extension ViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if(message.name == "callbackHandler") {
guard let body = message.body as? [String: Any] else {
print("could not convert message body to dictionary: \(message.body)")
return
}
guard let payload = body["payload"] as? String else {
print("Could not locate payload param in callback request")
return
}
print(payload)
}
}
}
Thanks Alex for all your fantastic support!

get the token from wkwebview after login in ios swift

I am loading a url in wkwebview for login. After successful login it will redirect it to inner page, from there when click a button it will redirect to another page where token is generated. After token is generated how to push that to swift application. I need to take that token for further use in my app. Please help with a complete solution
This is my code
override func loadView()
{
super.loadView()
let url = URL(string: "urlString")
let request = URLRequest(url:url!)
let config = WKWebViewConfiguration()
self.webView = WKWebView(frame: self.view.bounds, configuration: config)
webView?.load(request)
self.view.addSubview(webView!)
}
How to proceed further to get the token
add message name in your user content controller in your config
config.userContentController.add(self, name: "setToken")
add following extenstion in your view controller
extension ViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) {
if (message.name == "setToken"){
if let token = message.body as? String{
print(token)
}
}
print("Received message from native: \(message)")
}
}
pass your token from js
window.webkit.messageHandlers.setToken.postMessage(TOKEN_STRING);