Loading scripts from HTML string wkwebview - ios

Is it possible to load script files from Xcode project via html loaded with WKWebView loadHTMLString method?
For example if I have a project named DemoProject, with javascript files called script1.js and script2.js in the root directory. Then I'm loading an html string that tries to reference those files. Is that possible? If so how do I properly reference those files?

Try injecting it as user script.
If your folder structure is like this, use the below function to load it as user script
private func fetchScript() -> WKUserScript!{
var jsScript = ""
if let jsPath = Bundle.main.path(forResource: "hello", ofType: "js", inDirectory: "scripts"){
do
{
jsScript = try String(contentsOfFile: jsPath, encoding: String.Encoding.utf8)
}catch{
print("Error")
}
}
let wkAlertScript = WKUserScript(source: jsScript, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
return wkAlertScript
}
Add it to controller
func registerScriptsAndEvents() {
let controller = self.wkWebView.configuration.userContentController
// Load the entire script to the document
controller.addUserScript(fetchScript())
}

Related

Text file to array in swift

I'm trying to create a dictionary app on IOS. I have a text file (words_alpha.txt) in my Bundle, and I want to read all the words/lines, and place them into an arrray. String = ["word1", "word2", "word3"]. Here is my current code I got from bit.ly/39IC642
let path = Bundle.main.path(forResource: "words_alpha", ofType: "txt") // file path for file "words_alpha.txt"
let string = try String(contentsOfFile: path!, encoding: String.Encoding.utf8)
I am getting this error: Cannot use instance member 'path' within property initializer; property initializers run before 'self' is available
I am fairly new to using Swift and coding in general, please be detailed with your answers. Thank you!
If you are writing an iOS app, you can move such initialization into viewDidLoad():
var wordArray: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
//...
let url = Bundle.main.url(forResource: "words_alpha", withExtension: "txt")! // file URL for file "words_alpha.txt"
do {
let string = try String(contentsOf: url, encoding: .utf8)
wordArray = string.components(separatedBy: CharacterSet.newlines)
} catch {
print(error)
}
}
If your words_alpha.txt does contain multiple words per line, you may need some other way.

File Manager sometimes doesn't find subfolder in documents directory

I have asked this before, but had to reformulate everything so it becomes more understandable.
In my project, I have created a subfolder inside the documents directory called HTML with the following code:
fileprivate func createFolderOnDocumentsDirectoryIfNotExists() {
let folderName = "HTML"
let fileManager = FileManager.default
if let tDocumentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first {
let filePath = tDocumentDirectory.appendingPathComponent("\(folderName)")
if !fileManager.fileExists(atPath: filePath.path) {
do {
try fileManager.createDirectory(atPath: filePath.path, withIntermediateDirectories: true, attributes: nil)
} catch {
print("Couldn't create document directory")
}
}
print("Document directory is \(filePath)")
}
}
The print statement prints the following:
Document directory is file:///var/mobile/Containers/Data/Application/103869C9-9D46-4595-A370-A93BCD75D495/Documents/HTML
Inside my app's Bundle I have a .css file for me to use in a HTML string to be presented in a WKWebView.
As the .css file needs to be in the same folder as the HTML baseURL , I copy that .css file from the Bundle to the directory created with the above function, with the following code:
fileprivate func copyCSSFileToHTMLFolder() {
guard let cssURL = Bundle.main.url(forResource: "swiss", withExtension: "css") else { return }
let fileManager = FileManager.default
if let tDocumentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first {
let fp = tDocumentDirectory.appendingPathComponent("HTML")
if fileManager.fileExists(atPath: fp.path) {
let fp2 = fp.appendingPathComponent(cssURL.lastPathComponent)
do {
try fileManager.copyItem(atPath: cssURL.path, toPath: fp2.path)
} catch {
print("\nFailed to copy css to HTML folder with error:", error)
}
}
}
}
I know I could use both in the app's Bundle by setting the HTML baseURL to the main.Bundle, but since I need to present local images inside the HTML and not web-linked images, I had to move those into a subfolder in order for me to later copy the desired images into that subfolder, since the main.Bundle is read-only and not writable (We cannot save there images).
Now I have a ViewController that gets pushed through a UINavigationController; inside that pushed ViewController I have a WKWebView that will present the HTML.
I also have a var markdownString: String? {} property that once instantiated I convert it to a HTML string using the following function:
func getHTML(str: String) -> String {
/*
the cssImport here points to the previously copied css file as they are on the same folder
*/
let cssImport = "<head><link rel=\"stylesheet\" type=\"text/css\" href=\"swiss.css\"></head>"
let htmlString = try? Down(markdownString: str).toHTML()
return cssImport + htmlString!
}
My WKWebView is declared with the following code:
private lazy var webView: WKWebView = {
// script to fit the content with the screen
var scriptContent = "var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta);"
let wkuscript = WKUserScript(source: scriptContent, injectionTime: WKUserScriptInjectionTime.atDocumentEnd, forMainFrameOnly: true)
let wkucontroller = WKUserContentController()
wkucontroller.addUserScript(wkuscript)
let wkwebconfig = WKWebViewConfiguration()
wkwebconfig.userContentController = wkucontroller
let wv = WKWebView(frame: .zero, configuration: wkwebconfig)
wv.frame.size.height = 1
return wv
}()
Whenever my markdown String is set, I convert it to an HTML string, then I instantiate the HTML baseURL and the I load it into my webView with the following code:
public var markdownString: String? {
didSet {
guard let kPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("HTML", isDirectory: true) else { return }
guard let str = markdownString else { return }
let html = getHTML(str: str)
print("base url:", kPath)
let fm = FileManager.default
do {
let items = try fm.contentsOfDirectory(atPath: kPath.path)
print("items:", items)
} catch {
print("Error:", error)
}
webView.loadHTMLString(html, baseURL: kPath)
}
}
Here's where my problem starts.
The swiss.css file is in the following directory:
file:///var/mobile/Containers/Data/Application/103869C9-9D46-4595-A370-A93BCD75D495/Documents/HTML/
The HTML baseURL points to the following path:
file:///var/mobile/Containers/Data/Application/103869C9-9D46-4595-A370-A93BCD75D495/Documents/HTML/
As you can see, they're both pointing to the same path.
Sometimes my HTML finds the swiss.css file, as it prints the items in the do{}catch{} block, but other times it doesn't find the folder, the try fm.contentsOfDirectory(atPath: kPath.path) fires an error catched in the catch{} block printing the following:
Error: Error Domain=NSCocoaErrorDomain Code=260 "The folder “HTML” doesn’t exist." UserInfo={NSFilePath=/var/mobile/Containers/Data/Application/103869C9-9D46-4595-A370-A93BCD75D495/Documents/HTML, NSUserStringVariant=(
Folder
), NSUnderlyingError=0x1c044e5e0 {Error Domain=NSPOSIXErrorDomain Code=2 "No such file or directory"}}
This both happens in the simulator and on the real device.
What's causing this issue? Why does it finds the folder sometimes and other times it doesn't, even without rebooting the app, which may reset the documents directory path, even though I'm not forcing the path, but instead I'm fetching it as you can see above.
I don't understand this behavior.
UPDATE
So I was thinking that reading the html string directly might be causing some problems so I changed my markdown: String? declaration.
In it, I write the converted markdown string (not an html string) into a file in the same path, called index.html.
Now one think different happened -- when I push this view in the first time, it now finds my swiss.css file and the html file detects it and uses it as expected; so far so good.
but when I dismiss this view controller, poping to the parent view controller (pressing the back button) and then try to push it again, the same error (described previously) prompts:
Failed to write html to file with error: Error Domain=NSCocoaErrorDomain Code=4 "The folder “index.html” doesn’t exist." UserInfo={NSURL=file:///var/mobile/Containers/Data/Application/C0639153-6BA7-40E7-82CF-25BA4CB4A943/Documents/HTML/index.html, NSUserStringVariant=Folder, NSUnderlyingError=0x1c045d850 {Error Domain=NSPOSIXErrorDomain Code=2 "No such file or directory"}}
Note that some of the paths here are different than the previous, because it is a different launch and it changes, but that's not the problem here because I never store the path and force write it, I always fetch the URLs with the default File Manager fetch,
The updated markdown declaration goes by the following:
public var markdownString: String? {
didSet {
guard let kPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("HTML", isDirectory: true) else { return }
guard let str = markdownString else { return }
let html = getHTML(str: str)
do {
let writePath = kPath.appendingPathComponent("index.html")
try html.write(to: writePath, atomically: true, encoding: .utf8)
webView.loadFileURL(writePath, allowingReadAccessTo: kPath)
} catch {
print("Failed to write html to file with error:", error)
return
}
let fm = FileManager.default
do {
let items = try fm.contentsOfDirectory(atPath: kPath.path)
print("items:", items)
} catch {
print("Error:", error)
}
}
}
Somehow this confused me even more.
Why does it works on the first presentation and then it doesn't?
The error is telling you that it can't write to that file path, which means it doesn't exist. You should confirm this in your markdownString method by adding a check for FileManager.default.fileExists(atPath: kPath.path) before you try to write index.html to the writePath.
Once you confirm that, you need to track the life-cycle of the HTML folder's creation (just keep checking FileManager.default.fileExists(atPath: thePath) until you track down when it's disappearing/not getting created.
From the code you posted, there's no obvious indication why this is happening. It has to be somewhere else in your app.
In your UPDATE section you mention the following error:
Failed to write html to file with error: Error Domain=NSCocoaErrorDomain Code=4 "The folder “index.html” doesn’t exist."
As far as I understood your issue, index.html should not be a folder but a file.
I have no idea why it treats index.html as folder, but maybe replacing
let writePath = kPath.appendingPathComponent("index.html")
with
let writePath = kPath.appendingPathComponent("index.html", isDirectory: false)
helps or at least leads to a more specific error message.

WKWebView does load resources from local document folder

In my Swift iOS app, I want to download some dynamic HTML pages from a remote server, save them in the document directory, and display those pages from document directory.
I was using this to load the page:
var appWebView:WKWebView?
...
appWebView!.loadRequest(NSURLRequest(URL: NSURL(fileURLWithPath: htmlPath)))
Everything works on the simulator, but when I moved to real phones, it just showed a blank page. I then connected to the app using Safari, and found it complained with "Failed to load resource".
I then tried to first read the content of the page at htmlPath, then use
appWebView!.loadHTMLString()
to load the page. It works when the HTML page is simple. But if the HTML references something else, i.e. a JavaScript file also in the document directory (with an absolute path like <script src="file:////var/mobile/Containers/Data/Application/762035C9-2BF2-4CDD-B5B1-574A0E2B0728/Documents/xxxxx.js">), it will fail to load.
Does anyone know why this happens, and how to resolve the issue?
More info:
XCode version: 7.3.1
Deployment Target: 8.1 (I tried to use 9.3 too, but that didn't help.)
This is a simplified version of what I have used to load local files in a project of mine (iOS 10, Swift 3). I have just updated my code (7.5.2017) after testing it out again on iOS 10.3.1 and iPhone 7+ as requested by Raghuram and Fox5150 in the comments.
I just created a completely new project and this is the folder structure:
Update 19.04.2018: Added a new feature to download a .zip with HTML, CSS, JS files, unzip it in /Documents/ (Alamofire + Zip) and then load those files into the webView. You can find it in the GitHub sample project as well. Again, feel free to fork & star! :)
Update 08.02.2018: finally added a GitHub sample project, which also includes a local JavaScript file. Feel free to fork & star! :)
Version 1 with webView.loadFileURL()
ViewController.swift
import UIKit
import WebKit
class ViewController: UIViewController, WKNavigationDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let webView = WKWebView()
let htmlPath = Bundle.main.path(forResource: "index", ofType: "html")
let htmlUrl = URL(fileURLWithPath: htmlPath!, isDirectory: false)
webView.loadFileURL(htmlUrl, allowingReadAccessTo: htmlUrl)
webView.navigationDelegate = self
view = webView
}
}
Version 2 with webView.loadHTMLString()
ViewController.swift
import UIKit
import WebKit
class ViewController: UIViewController, WKNavigationDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let webView = WKWebView()
let htmlPath = Bundle.main.path(forResource: "index", ofType: "html")
let folderPath = Bundle.main.bundlePath
let baseUrl = URL(fileURLWithPath: folderPath, isDirectory: true)
do {
let htmlString = try NSString(contentsOfFile: htmlPath!, encoding: String.Encoding.utf8.rawValue)
webView.loadHTMLString(htmlString as String, baseURL: baseUrl)
} catch {
// catch error
}
webView.navigationDelegate = self
view = webView
}
}
Gotchas to look out for:
Make sure that your local html/js/css files are in Project -> Target -> Build Phases -> Copy Bundle Resources
Make sure that your html files don't reference relative paths e.g. css/styles.css because iOS will flatten your file structure and styles.css will be on the same level as index.html so write <link rel="stylesheet" type="text/css" href="styles.css"> instead
Given the 2 versions and the gotchas here are my html/css files from the project:
web/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Offline WebKit</title>
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<h1 id="webkit-h1">Offline WebKit!</h1>
</body>
</html>
web/css/styles.css
#webkit-h1 {
font-size: 80px;
color: lightblue;
}
If somebody wants a GitHub sample project, tell me in the comments section and I'll upload it.
Swift 4 Method
This method allows WKWebView to properly read your hierarchy of directories and sub-directories for linked CSS/JS files. You do NOT need to change your HTML, CSS or JS code.
Updated for Xcode 9.3
Step 1
Import the folder of local web files anywhere into your project. Make sure that you:
☑️ Copy items if needed
☑️ Create folder references (not "Create groups")
☑️ Add to targets
Step 2
Go to the View Controller with the WKWebView and add the following code to the viewDidLoad method:
let url = Bundle.main.url(forResource: "index", withExtension: "html", subdirectory: "website")!
webView.loadFileURL(url, allowingReadAccessTo: url)
let request = URLRequest(url: url)
webView.load(request)
index – the name of the file to load (without the .html extension)
website – the name of your web folder (index.html should be at the root of this directory)
Conclusion
The overall code should look something like this:
import UIKit
import WebKit
class ViewController: UIViewController, WKUIDelegate, WKNavigationDelegate {
#IBOutlet weak var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
webView.uiDelegate = self
webView.navigationDelegate = self
let url = Bundle.main.url(forResource: "index", withExtension: "html", subdirectory: "Website")!
webView.loadFileURL(url, allowingReadAccessTo: url)
let request = URLRequest(url: url)
webView.load(request)
}
}
If any of you have further questions about this method or the code, I'll do my best to answer. :)
This solution helped me:
[configuration.preferences setValue:#YES forKey:#"allowFileAccessFromFileURLs"];
This works well (Swift 3, Xcode 8):
import UIKit
import WebKit
class ViewController: UIViewController, WKNavigationDelegate {
var webView: WKWebView!
override func loadView() {
webView = WKWebView()
webView.navigationDelegate = self
view = webView
}
override func viewDidLoad() {
super.viewDidLoad()
if let url = Bundle.main.url(forResource: "file", withExtension: "txt")
{
do
{
let contents = try String(contentsOfFile: url.path)
webView.loadHTMLString(contents, baseURL: url.deletingLastPathComponent())
}
catch
{
print("Could not load the HTML string.")
}
}
}
}
This works nicely with file URL or remote URL, and whether file is in the bundle or in documents:
if url.isFileURL {
webView.loadFileURL(url, allowingReadAccessTo: url)
} else {
let request = URLRequest(url: url)
webView.load(request)
}
Constructing the URLs this way allowed me to load resources from the document directory with WKWebView:
guard let docDir = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) else {
return
}
let resourceURL = docDir.appendingPathComponent("/Path/To/Your/Resource")
self.wkWebView.loadFileURL(resourceURL, allowingReadAccessTo: docDir)
The files must be in the document directory.
I implemented the following to retrieve a document:
let documentDirUrl = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let fileNameWithExtension = "IMG_0002.PNG"
let indexFileUrl = documentDirUrl.appendingPathComponent(fileNameWithExtension)
if FileManager.default.fileExists(atPath: indexFileUrl.path) {
webView.loadFileURL(indexFileUrl, allowingReadAccessTo: documentDirUrl)
}
Check that ticket: iOS: How to load local files (not in the bundle) in a WKWebView?
var nsurl = URL(fileURLWithPath: URL(fileURLWithPath: URL(fileURLWithPath: documentsDirectory()).appendingPathComponent(user_appli).absoluteString).appendingPathComponent("index.html").absoluteString) //locally
var readAccessToURL: URL? = nsurl.deletingLastPathComponent?.deletingLastPathComponent
if let anURL = readAccessToURL {
webView?.loadFileURL(nsurl, allowingReadAccessTo: anURL)
}

Why does swift not recognise my resource?

Swift is telling me my file does not exist and unfortunately I cannot find the directory myself. Am I using the URL for resource method incorrectly or do I just need to find a way of removing the optional brackets? When I refer to this object directly it works but I need to find a way of creating a path my team mates can use as well.
override func setUp()
{
super.setUp()
let bundleURL = NSBundle.mainBundle().URLForResource("meetingexample", withExtension: "ics")
let eventsFile = NSURL(fileURLWithPath: ("\(bundleURL)"))
//let eventsFile = NSURL(fileURLWithPath: docFolderURL.URLByAppendingPathComponent("/meetingexample.ics"))
content = try! String(contentsOfURL: eventsFile, encoding: NSUTF8StringEncoding)
}
Print of bundleURL:
Optional(file:/Users/GB112922/Library/Developer/CoreSimulator/Devices/EF6A2594-6B31-4E38-B46D-2F3AAAF25210/data/Containers/Bundle/Application/35E04003-072F-476E-957E-98C70B792539/CallIn.app/meetingexample.ics)`
You have two issues:
"\(bundleURL)" is wrapping the value of the NSURL in the Optional(...) extra text.
URLForResource is already giving you an NSURL. There's no need to create another NSURL from it.
Just use bundleURL. There is no need for the eventsFile variable.
override func setUp()
{
super.setUp()
let bundleURL = NSBundle.mainBundle().URLForResource("meetingexample", withExtension: "ics")
content = try! String(contentsOfURL: bundleURL, encoding: NSUTF8StringEncoding)
}

How do I retrieve cached files in iOS swift?

Thanks for the help in my last question. This time I would like to ask for help again for an application whose contents need to be downloaded and cached when it's opened for the first time.
Indeed it's a web app where the view controller consists of a WebView. In order to cache the whole website (which consists of "index.htm", "first.htm, "second.htm" and etc), I have scraped the whole site using the Kanna library and hence generated numerous links (generatedURL). Then I write the HTML of each link into a single file using the approach answered here. Read and write data from text file
Here is my code in the didFinishLaunchingWithOptions of AppDelegate.swift.
// get the documents folder url
let documentDirectoryURL = try! NSFileManager.defaultManager().URLForDirectory(.DocumentDirectory, inDomain: .UserDomainMask, appropriateForURL: nil, create: true)
for index in 0..<generatedURL.count {
let fileDestinationUrl = documentDirectoryURL.URLByAppendingPathComponent(String(index)+".htm")
cachedURL[index] = fileDestinationUrl //store the cached urls
let fileURL = NSURL(string: generatedURL[index])
//if (NSFileManager.defaultManager().fileExistsAtPath(fileDestinationUrl)) {
let data = NSData(contentsOfURL: fileURL!)
if (data != nil) {
//writing to disk
data?.writeToURL(fileDestinationUrl, atomically: true)
// saving was successful. any code posterior code goes here
//reading from disk
do {
let mytext = try String(contentsOfURL: fileDestinationUrl, encoding: NSUTF8StringEncoding)
print(fileDestinationUrl)
print(mytext) // "some text\n"
} catch let error as NSError {
print("error loading from url \(fileDestinationUrl)")
print(error.localizedDescription)
}
}
// } else {
// print("The files already exist")
// //reading from disk
// do {
// let mytext = try String(contentsOfURL: fileDestinationUrl, encoding: NSUTF8StringEncoding)
// //print(fileDestinationUrl)
// //print(mytext) // "some text\n"
// } catch let error as NSError {
// print("error loading from url \(fileDestinationUrl)")
// print(error.localizedDescription)
// }
//
// }
}
When running the program, the HTMLs of all the links are stored locally in those files. There's no problems in loading the HTML and thereby displaying the cached page in the WebView.
file:///var/mobile/Containers/Data/Application/.../Documents/0.htm
file:///var/mobile/Containers/Data/Application/.../Documents/1.htm
file:///var/mobile/Containers/Data/Application/.../Documents/2.htm
.
.
.
However, the current problem is that I lost the linkage between the cached pages. For example, in the website, there is a button on "index.htm" that links to "first.htm".
Now after loading the cached "index.htm" which is now "file:///var/....../0.htm", I won't be able to go to the cached "first.htm" because "file:///var/....../1.htm" is not in the HTML of the button.
So how do I retrieve the cached files in their original urls? Should I change the approach of generating the file or just create a new version of the website with all the cached file paths?
Thanks for reading my question.
OK, i think I can answer my own question now. Using the following function in the ViewController.swift containing the webView object, I can prompt the webView to load the cached url if the original url is clicked.
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
if navigationType == UIWebViewNavigationType.LinkClicked {
if (request.URL!.absoluteString == generatedURL[index] {
let requestObj = NSMutableURLRequest(URL: appDelegate.cachedURL[index]!);
webView.loadRequest(requestObj)
//return false
}
}
return true
}

Resources