WKWebView Help Support - swift

I am able to implement the new WebKit in 7.1 Deployment. I can use it without error on the devices running in iOS8 up. However, when the device falls below iOS8, my WKWebView becomes nil even after the initialization, my suspect was even if you silence webkit and successfully add it on your project and the deployment was 7.1, if the OS actually fall below iOS8 this WebKit becomes unvalable.
I want to confirm this error so I can proceed. Since this webkit was introduced as of the release of swift and iOS8. Thanks

Here is a simple example, where I create a new protocol and extend both UIWebView and WKWebView from the same protocol. With this, it makes a easy to keep track of both these views inside my view controller and both of these use common method to load from url, it makes easy for abstraction.
protocol MyWebView{
func loadRequestFromUrl(url: NSURL!)
}
extension UIWebView:MyWebView{
func loadRequestFromUrl(url: NSURL!){
let urlRequest = NSURLRequest(URL: url)
loadRequest(urlRequest)
}
}
extension WKWebView:MyWebView{
func loadRequestFromUrl(url: NSURL!){
let urlRequest = NSURLRequest(URL: url)
loadRequest(urlRequest)
}
}
// This is a simple closure, which takes the compared system version, the comparison test success block and failure block
let SYSTEM_VERSION_GREATER_THAN_OR_EQUAL: (String, () -> (), () -> ()) -> Void = {
(var passedVersion: String, onTestPass: () -> (), onTestFail: () -> ()) in
let device = UIDevice.currentDevice()
let version = device.systemVersion
let comparisonOptions = version.compare(passedVersion, options: NSStringCompareOptions.NumericSearch, range: Range(start: version.startIndex, end: version.endIndex), locale: nil)
if comparisonOptions == NSComparisonResult.OrderedAscending || comparisonOptions == NSComparisonResult.OrderedSame{
onTestPass()
}else{
onTestFail()
}
}
class ViewController: UIViewController{
var webView: MyWebView!
override func viewDidLoad() {
super.viewDidLoad()
SYSTEM_VERSION_GREATER_THAN_OR_EQUAL("8.0",
{
let theWebView = WKWebView(frame: self.view.bounds)
self.view.addSubview(theWebView)
self.webView = theWebView
},
{
let theWebView = UIWebView(frame: self.view.bounds)
self.view.addSubview(theWebView)
self.webView = theWebView
})
webView.loadRequestFromUrl(NSURL(string: "http://google.com"))
}
}

Related

Upload file using input type=file with WKWebView does not open file dialog

I'm using Xcode 11.5 and loading a web page in a WKWebView. I have file access set to read only and this is my apps capability list.
This is how my loadView look like
override func loadView() {
//Inject JS string to read console.logs for debugging
let configuration = WKWebViewConfiguration()
let action = "var originalCL = console.log; console.log = function(msg){ originalCL(msg); window.webkit.messageHandlers.iosListener.postMessage(msg); }" //Run original console.log function + print it in Xcode console
let script = WKUserScript(source: action, injectionTime: .atDocumentStart, forMainFrameOnly: false) //Inject script at the start of the document
configuration.userContentController.addUserScript(script)
configuration.userContentController.add(self, name: "iosListener")
//Initialize WKWebView
webView = WebView(frame: (NSScreen.main?.frame)!, configuration: configuration)
//Set delegates and load view in the window
webView.navigationDelegate = self
webView.uiDelegate = self
view = webView
}
When I use Safari on iOS and I tab the input type=file the dialog opens up but using my macOS app using a webview it does not work? Do I need to set more capabilities on my app? I found some old issues with file inputs but they seem to be resolved?
Implemented the UIDelegate like this.
func webView(_ webView: WKWebView, runOpenPanelWith parameters: WKOpenPanelParameters, initiatedByFrame frame: WKFrameInfo, completionHandler: #escaping ([URL]?) -> Void) {
let openPanel = NSOpenPanel()
openPanel.canChooseFiles = true
openPanel.begin { (result) in
if result == NSApplication.ModalResponse.OK {
if let url = openPanel.url {
completionHandler([url])
}
} else if result == NSApplication.ModalResponse.cancel {
completionHandler(nil)
}
}
}

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!

Alamofire background working on simulator but not on device

As the title mentions I've set up a backgroundURL with Alamofire. It works like a charm in simulator but on my device doesn't. I'm sure I'm missing something here since I'm not that experienced with URL.
Here's the code I have so far:
class NetworkManager {
static let shared = NetworkManager()
private lazy var backgroundManager: Alamofire.SessionManager = {
let bundleIdentifier = MyStruct.identifier
return Alamofire.SessionManager(configuration: URLSessionConfiguration.background(withIdentifier: bundleIdentifier))
}()
var backgroundCompletionHandler: (() -> Void)? {
get{
return backgroundManager.backgroundCompletionHandler
}
set{
backgroundManager.backgroundCompletionHandler = newValue
}
}
}
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void) {
NetworkManager.shared.backgroundCompletionHandler = completionHandler
}
In my ViewController:
func populateArrays(){
Alamofire.request("http://www.aps.anl.gov/Accelerator_Systems_Division/Accelerator_Operations_Physics/sddsStatus/mainStatus.sdds.gz").responseData { response in
switch response.result{
case .success:
print("Validation Successful")
case .failure(let error):
print(error.localizedDescription)
}
if let data = response.result.value{
Solved it. For anyone else that has this problem you need to add the following code to your appDelegate.
func applicationDidEnterBackground(_ application: UIApplication) {
var bgTask = 0
var app = UIApplication.shared
bgTask = app.beginBackgroundTask(expirationHandler: {() -> Void in
app.endBackgroundTask(bgTask)
})
It seems to me that you are not using the background manager you've created. Instead of
Alamofire.request("http://www.aps.anl.gov...")
which calls the default (not background) session manager, you should use:
backgroundManager.request("http://www.aps.anl.gov...")
Which Jon Shier mentioned in the comments by the way.

Saving WebView to PDF returns blank image?

I'm trying to figure out how to save a WebView to a PDF and totally stuck, would really appreciate some help?
I'm doing this in Cocoa & Swift on OSX, here's my code so far:
import Cocoa
import WebKit
class ViewController: NSViewController {
override func loadView() {
super.loadView()
}
override func viewDidLoad() {
super.viewDidLoad()
loadHTMLString()
}
func loadHTMLString() {
let webView = WKWebView(frame: self.view.frame)
webView.loadHTMLString("<html><body><p>Hello, World!</p></body></html>", baseURL: nil)
self.view.addSubview(webView)
createPDFFromView(webView, saveToDocumentWithFileName: "test.pdf")
}
func createPDFFromView(view: NSView, saveToDocumentWithFileName fileName: String) {
let pdfData = view.dataWithPDFInsideRect(view.bounds)
if let documentDirectories = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).first {
let documentsFileName = documentDirectories + "/" + fileName
debugPrint(documentsFileName)
pdfData.writeToFile(documentsFileName, atomically: false)
}
}
}
It's pretty simple, what I'm doing is creating a WebView and writing some basic html content to it which renders this:
And then takes the view and saves it to a PDF file but that comes out blank:
I've tried grabbing the contents from the webView and View but no joy.
I've found a similar problem here How to take a screenshot when a webview finished rending regarding saving the webview to an image, but so far no luck with an OSX Solution.
Could it be something to do with the document dimensions?
or that the contents is in a subview?
maybe if you capture the View you can't capture the SubView?
Any ideas?
iOS 11.0 and above, Apple has provided following API to capture snapshot of WKWebView.
#available(iOS 11.0, *)
open func takeSnapshot(with snapshotConfiguration: WKSnapshotConfiguration?, completionHandler: #escaping (UIImage?, Error?) -> Swift.Void)
Sample usage:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
if #available(iOS 11.0, *) {
webView.takeSnapshot(with: nil) { (image, error) in
//Do your stuff with image
}
}
}
iOS 10 and below, UIWebView has to be used to capture snapshot. Following method can be used to achieve that.
func webViewDidFinishLoad(_ webView: UIWebView) {
let image = captureScreen(webView: webView)
//Do your stuff with image
}
func captureScreen(webView: UIWebView) -> UIImage {
UIGraphicsBeginImageContext(webView.bounds.size)
webView.layer.render(in: UIGraphicsGetCurrentContext()!)
let image: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
Here's another relevant answer
So I kind of figured out how to solve it, it turns out you can't (especially on OSX) access and print a webview from a WKWebView.
You have to use a WebView and NOT a WKWebView (I originally started with WKWebView because a few of the articles I read said to use that).
A WebView object is pretty much similar to a WKWebView object, which is fun as hell :-)
But it gives you access to .mainFrame & .frameView which you'll need to print it's content.
Here's my code:
let webView = WebView(frame: self.view.frame)
let localfilePath = NSBundle.mainBundle().URLForResource(fileName, withExtension: "html");
let req = NSURLRequest(URL: localfilePath!);
webView.mainFrame.loadRequest(req)
self.view.addSubview(webView)
Once it's rendered I then added a 1 second delay just to make sure the content has rendered before I print it,
// needs 1 second delay
let delay = 1 * Double(NSEC_PER_SEC)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(time, dispatch_get_main_queue()) {
// works!
let data = webView.dataWithPDFInsideRect(webView.frame)
let doc = PDFDocument.init(data: data)
doc.writeToFile("/Users/john/Desktop/test.pdf")
// works!
let printInfo = NSPrintInfo.sharedPrintInfo()
let printOperation = NSPrintOperation(view: webView.mainFrame.frameView, printInfo: printInfo)
printOperation.runOperation()
}
Here I'm printing it and saving it as a PDF, just so I'm doubly sure it works in all circumstances.
I'm sure it can be improved, I hate the delay hack, should replace that with some kind of callback or delegate to run when the content has fully loaded.