Save a PDF from a WKWebView - swift

This questions pertains to macOS, not iOS.
Note that this question is being reported as a duplicate of this question. The answers on that page pertain either to iOS (irrelevant) or uses the deprecated WebView as a solution, which is exactly what my question is about in the first place.
So Apple has deprecated WebView in favor of WKWebView, but I'm not seeing a working solution for being able to export (or print) a PDF from the new view type. I've tried the following (and more) from within the delegate method webView(_ webView: WKWebView, didFinish navigation: WKNavigation!)
1.
let pdfData = webView.dataWithPDF(inside: webView.frame)
try? pdfData.write(to: URL(fileURLWithPath: "/filepath/to/test.pdf"))
This resulted in a literal blank pdf file.
2.
webView.takeSnapshot(with: nil) { (image, error) in
if let error = error {
print("Error: \(error)")
return
}
guard let image = image else {
print("No image")
return
}
try? image.tiffRepresentation?.write(to: URL(fileURLWithPath: "/path/to/test.tif"))
}
And while this got actual content in the image, it was a (giant) rendered bitmap image having lost all of its textual/string data, which also only showed what was visible on screen, nothing beyond the edges of the window.
Meanwhile, following the numerous examples available on the web, using WebView works well and as expected. Am I to believe that Apple released a half baked replacement for a deprecated framework, or am I doing something wrong?

This is now possible with WKWebView's createPDF method: https://developer.apple.com/documentation/webkit/wkwebview/3650490-createpdf
Here's an example:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
let config = WKPDFConfiguration()
//Set the page size (this can match the html{} size in your CSS
config.rect = CGRect(x: 0, y: 0, width: 792, height: 612)
//Render the PDF
webView.createPDF(configuration: config){ result in
switch result{
case .success(let data):
try! data.write(to: URL(fileURLWithPath: "/path/file.pdf"))
case .failure(let error):
print(error)
}
}
}
I have it working on both macOS and iOS. Hopefully this helps.

Related

How to Load Several Swift WKWebviews and Know When They Are All Done

I am using WKWebView to render several (around 100) web pages that I then need to render to PDF. I am using the createPDF method of WKWebView to accomplish this. The reason I'm doing each individual page in its own web view is because createPDF doesn't respect page breaks in the HTML (as far as I know).
So I have a class where I start the loop to render each page:
class PrintVC: ViewController, WKNavigationDelegate {
var pages = [Page]()
func start(){
//A "page" is a struct that has the string content to load each web view
for page in pages{
let webView = WKWebView()
webView.navigationDelegate = self
webView.loadHTMLString(page.content, baseURL: Bundle.main.bundleURL)
}
}
}
I know the page is ready to be saved to PDF in the didFinish navigation delegate method:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
let config = WKPDFConfiguration()
config.rect = CGRect(x: 0, y: 0, width: 792, height: 612)
//Create the PDF
webView.createPDF(configuration: config){ result in
switch result{
case .success(let data):
do{
try data.write(to: URL(fileURLWithPath: "file-???.pdf"))
}catch let error{
print(error)
}
case .failure(let error):
print(error)
}
}
}
}
The trouble I'm having is I don't know when each individual page is done rendering. I also don't know how to pass each page's name to be used in the file path to save it.
How can I start a bunch of WKWebView loads and know when they are all done? Or better still, how can I reuse the same WKWebView and load each individual page in the same way? I assume using the same web view would be a better use of memory.
How can I start a bunch of WKWebView loads and know when they are all done?
Well, you'd need to identify which web view caused the delegate method to be called. It is for this reason that the first parameter - webView: WKWebView - exists.
One way is to put each (web view, pair) into a dictionary ([WKWebView: Page]). Then start the loading:
// assume you have declared a property "self.webViewDict"
for page in pages{
let webView = WKWebView()
webView.navigationDelegate = self
self.webViewDict[webView] = page
webView.loadHTMLString(page.content, baseURL: Bundle.main.bundleURL)
}
When one finishes loading, you can identify the page by doing webViewDict[webView]. You should then remove the web view from the dictionary:
webViewDict[webView] = nil
if webViewDict.isEmpty {
// everything is loaded!
}
how can I reuse the same WKWebView and load each individual page in the same way?
Note that if you use the same WKWebView, you'll have to load the pages sequentially. The same web view can't load multiple things at the same time.
You can just removed the loaded pages from pages. If you don't want to do that, you can copy pages to another var first.
In start, load the first page:
if let firstPage = pages.first {
webView.loadHTMLString(firstPage.content, baseURL: Bundle.main.bundleURL)
}
When you successfully load a page, do the same thing again:
case .success(let data):
pages.removeFirst()
if let firstPage = pages.first {
webView.loadHTMLString(firstPage.content, baseURL: Bundle.main.bundleURL)
} else {
// we are done!
}

WKNavigation delegate functions calling too late

I am loading two websites in two different web views and after loading website i am hiding some content of it which is working perfect using WKNavigationDelegate did finish method.
but issue is did finish function is calling about 90 sec after loading website in web view.
Function is working fine but i just want to know why this function is loading too late it should execute function right after loading website.
my code is
web_view.navigationDelegate = self as? WKNavigationDelegate
web_view.isUserInteractionEnabled = true
let request = URLRequest(url: url!)
self.web_view.load(request)
self.view.addSubview(self.web_view)
delegate method
extension urdu_HomeViewController : WKNavigationDelegate{
//enable javascript to remove vavigation from website
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
let removeelementid = "javascript:(function() { " + "document.getElementsByClassName('td-header-menu-wrap-full td-container-wrap')[0].style.display=\"none\"; " + "})()"
webView.evaluateJavaScript(removeelementid) { (res, error) in
if error != nil
{
print("Error")
}
else
{
//print(res!)
}
}
}
WKNavigationDelegate didFinish method is called only after all subresources are loaded. So in your case it might be that some resource takes too much time to load and that delays calling didFinish.
You should rather use WKUserScript to execute your JS right after DOM is ready.

Swift wkwebview get title returns nothing if title contains 2 or more arabic words

I have a weird problem when I call get title from wkwebview. If web title in English it works perfectly but in the title in Arabic and contains 2 or more words it returns nothing. The problem disappears when title contains one Arabic word. any help?
the code I use:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
let webTitle:String = webView.title!
print("Title: \(webTitle)")
self.title = webTitle
}
here are photos that explain the problem
English Title
One Arabic word
Two Arabic words
TEMPORARY FIX
As #EmilioPelaez said that it seems to be a bug. I fixed this using evaluateJavaScript (Thanks to #the4kman).
The code I used:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
let javascript = "document.title\n"
webView.evaluateJavaScript(javascript) { (result, error) -> Void in
if error == nil {
self.title = String(describing: result!)
}
}
}
I hope this bug will be fixed soon :)

Swift 3 - Check if WKWebView has loaded page

My question:
How do I check if a page in WKWebView has fully loaded in Xcode using Swift 3?
This is my problem:
Webpage 1:
From this page I load Webpage 2
Webpage 2:
I need to get html data from Webpage 2, but when I print the HTML data I get HTML data of webpage 1, which I don't want. But when I print HTML data 2 seconds later it gives me the right HTML data.
I need to know whether or not a page in WKWebView did finish loading. I can see in the WebView it is loaded and also the progressbar is fully loaded, but when I print html data of the page I get html data of previous page, which is not what I want. Only if I wait a second it gives the right data, probably cause Webpage 2 is loaded.
So how do I let Xcode to print html when the next page is totally loaded?
I have tried several methods:
detect WKWebView finish loading
Call JavaScript function from native code in WKWebView
Maybe I can use:
if webView.isloading { get }
but I don't know how to implement this method and if it should work.
I have tried several methods from Stack but these are not working for me or outdated.
Do you guys know a solution for this problem in Swift 3?
Thanks!
Answer (Big thanks to #paulvs )
To check if your WKWebView has loaded easily implement the following method:
import WebKit
import UIKit
class ViewController: UIViewController, WKNavigationDelegate {
let webView = WKWebView()
func webView(_ webView: WKWebView,
didFinish navigation: WKNavigation!) {
print("loaded")
}
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "https://www.yourwebsite.com/") !
let request = URLRequest(url: url)
webView.navigationDelegate = self
webView.load(request)
// Do any additional setup after loading the view, typically from a nib.
}
}
Add WKNavigationDelegate to class
Add:
func webView(_ webView: WKWebView,didFinish navigation: WKNavigation!) { print("loaded") }
Result: It will print "loaded" in the console everytime the WKWebView has finished loading the page. This was excactly what I was looking for, so again a big thanks to Paulvs!
Set delegate > WKNavigationDelegate
Objective-C
// Start loading WKWebView
-(void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {
NSLog(#"Start loading");
}
//Finished loading WKWebView
-(void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
NSLog(#"End loading");
}
Swift 4.2
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
print("Start loading")
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print("End loading")
}

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.