Reputation: 984
I have a HTML string like this:
<html>
<head>
<script>
window.user = {"username":"admin", "meta":{"city": "Los Angeles", "country": "US"}}
window.flash = {"success": "", "warning": "you cannot do this"}
</script>
</head>
</body>
</html>
I am implementing a function
func dictionaryWithHtml(html: String, variable: String) -> [String: AnyObject]? {
}
usage:
calling dictionaryWithHtml(html, variable: "window.user")
should return the dictionary
{"username":"admin", "meta":{"city": "Los Angeles", "country": "US"}}
I am trying to search window.user =
substring as index1
, then find window.flash =
substring as index2
, then I can easily find the range of JSON for user dictionary.
For window.flash
, I could find </script>
substring, and get the range similarly, but what if the server updates and add more variables in the tag? or if server changes the order of assignment.
So is there any better and more robust approach?
Upvotes: 1
Views: 4489
Reputation: 32861
You could use an html parser to retrieve the values of the script
tag, or you could load the html string into a WKIWebView
, and ask it to execute the javascript code that would return to you the value of the variables you search within the window
object. This has the advantage of being more reliable as it will not break if the script structure changes, and also works for dynamically javascript computed values.
let webView = WKWebView(frame: CGRectZero)
self.view.addSubview(webView)
webView.loadHTMLString(htmlString, baseURL: nil)
// wait until the page has loaded
// either this, or execute the javascript code in
// the didFinishLoading delegate method
while (webView.loading) {
CFRunLoopRunInMode(NSDefaultRunLoopMode, 0.1, false)
}
// get the two objects in one round-trip to the javascript engine
webView.evaluateJavaScript("[window.user, window.flash]") { (result, error) -> Void in
print(result, error)
}
In both cases you need to add the web view to another view, so this code likely will need to be run into a view controller. Another possible disadvantage is the fact that the code will need to run on the main thread.
Note 1. WKWebView
was added in iOS 8, for iOS 7 and earlier you can use an UIWebView
instead:
let webView = UIWebView(frame: CGRectZero)
self.view.addSubview(webView)
webView.loadHTMLString(htmlString, baseURL: nil)
// wait until the page has loaded
// either this, or execute the javascript code in
// the didFinishLoading delegate method
while (webView.loading) {
CFRunLoopRunInMode(NSDefaultRunLoopMode, 0.1, false)
}
let userJSON = webView.stringByEvaluatingJavaScriptFromString("window.user")
let flashJSON = webView.stringByEvaluatingJavaScriptFromString("window.flash")
Note 2. The javascript engine will return the parsed json, if you need to obtain the values as strings you can query after JSON.stringify(window.user)
and JSON.stringify(window.flash)
.
If you want to run this code in a playground, then you need to configure the playground to not terminate when the last line of code executes, and we need to wait until the javascript execution callback is called. Thanks to this SO question I found out how it can be done.
This code works in a playground:
import WebKit
import XCPlayground
// configuring the playground to not stop after evaluating the last line of code
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
let htmlString = "<html>\n" +
"<head>\n" +
"<title>aaa</title>\n" +
"<script>\n" +
"window.user = {\"username\":\"admin\", \"meta\":{\"city\": \"Los Angeles\", \"country\": \"US\"}};\n" +
"window.flash = {\"success\": \"\", \"warning\": \"you cannot do this\"};\n" +
"</script>\n" +
"</head>\n" +
"</body>\n" +
"</html>"
let webView = WKWebView(frame: CGRectZero)
// commented out the addSubview call as we don't have a view in a playground
//self.view.addSubview(webView)
webView.loadHTMLString(htmlString, baseURL: nil)
// wait until the page has loaded
// either this, or execute the javascript code in
// the didFinishLoading delegate method
while (webView.loading) {
CFRunLoopRunInMode(NSDefaultRunLoopMode, 0.1, false)
}
// get the two objects in one round-trip to the javascript engine
webView.evaluateJavaScript("[window.user, window.flash]") { (result, error) -> Void in
print(result, error)
}
and prints the following to the console:
Optional((
{
meta = {
city = "Los Angeles";
country = US;
};
username = admin;
},
{
success = "";
warning = "you cannot do this";
}
)) nil
Upvotes: 3