1 comment
There are many options available to display charts within your [native] iOS apps. You could write one yourself (why?) or use one of the many third party libraries that are available . Check out this GitHub repo for a list of charting libraries in swift/ ObjC.
However, if you are interested in using Highcharts within your app, then read on . Highcharts is a popular JS charting engine and you can learn more about it at http://www.highcharts.com.
This post will demonstrate how you can load a Highcharts based chart into a iOS native app. We will use the example charts available as part of the Highcharts library. A more interesting and complete example that demonstrates how you can dynamically load chart data is available at https://github.com/rajagp/iOS_Highcharts_Sample.
1) Download the sample xcode app project from http://www.priyaontech.com/wp-content/uploads/2017/01/HighchartDemo.zip. This is a simple single view application that will load a .html file into a WKWebView.
2) Download the Highcharts library from http://www.highcharts.com/download
3) Once downloaded , unzip its contents and examine it. You should see contents list similar to screenshot below
4) Navigate to the “examples” folder
5) Copy the “index.htm” file from any of the examples into your project. Make sure you copy it into your project
6) Your project should look something like this
7) In your xcode project, open and review the the index.htm file that you just copied over. It contains a JS function that creates a chart using the Highcharts chart API as defined in http://api.highcharts.com/highcharts/chart. The chart data and options are defined statically.
|
$(function() { // Create the chart Highcharts.chart('container', { chart: { type: 'column' }, title: { text: 'Browser market shares. January, 2015 to May, 2015' }, ……… |
8) Now, open the “ChartViewController.swift” file and review it. This is a simple view controller that manages a WKWebview and loads the index.html into the web view.
Look for the “loadContainerPage” function . This is where the index.htm is loaded.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
fileprivate func loadContainerPage() { print(#function) // Loads the index.html that is the base page for loading the chart guard let filePath = Bundle(for: ChartViewController.self). path(forResource: "index", ofType: "htm") else{ print("Could not locate index.htm") return } do{ let currFrame = self.view.frame; let content = tryString(contentsOfFile:filePath) let formattedContent = String.init(format: content,currFrame.size.width,currFrame.size.height) let_= self.webView?.loadHTMLString(formattedContent, baseURL: URL.init(fileURLWithPath: Bundle(for: ChartViewController.self).bundlePath)) } catch{ print("Failed to load contents of index.html") } } |
9) That’s it ! Run the app and you should see the sample chart load …
The chart data and chart options were statically predefined as part of the JS function. In a real world application, you would probably want to load the chart data and options dynamically.
A complete example that demonstrates how you can dynamically load chart data is available at https://github.com/rajagp/iOS_Highcharts_Sample.
5 comments
In iOS8 Apple introduced the Modern WebKit framework. The Modern WebKit supports a multi-process architecture wherein web content is loaded in a process separate from the app (upto a limit). The framework is unified across the iOS and OS X platforms and includes a lot of performance enhancements such as the optimized JS Nitro Engine , hardware accelerated 60fps scrolling etc. It also includes support for WebGL for 3D rendering.
The WKWebView For Displaying Web Content
The WKWebView is a replacement for the UIWebView on iOS for loading web content within a native app. It is essentially a wrapper around the WebKit.framework and exposes an interface that is a lot more powerful than what the UIWebViews allowed.
One of the significant improvements is the simplified communication model between Native and JS .
This article will discuss how Native-WebView bridging can be achieved.
WKWebViews are associated with a WKWebViewConfiguration object. Multiple webviews can share the configuration object.
The following code snippet creates an instance of WKWebView
Obj-C
|
self.webView= [[WKWebViewalloc]initWithFrame:self.view.frameconfiguration:self.webConfig]; |
Swift
|
webView = WKWebView(frame: self.view.frame, configuration: webConfig) |
Native to JS Bridging Using User Scripts.
User Scripts are JS that you inject into your web page at either the start of the document load or after the document is done loading. User scripts are extremely powerful because they allow client-side customization of web page, allow injection of event listeners and can even be used to inject scripts that can in turn call back into the Native app.
The following code snippet creates a user script that is injected at end of document load. The user script is added to the WKUserContentController instance that is a property on the WKWebViewConfiguration object.
ObjC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
// Create WKWebViewConfiguration instance _webConfig = [[WKWebViewConfigurationalloc]init]; // Setup WKUserContentController instance for injecting user script WKUserContentController* userController = [[WKUserContentControlleralloc]init]; // Get script that's to be injected into the document NSString* js = [self buttonClickEventTriggeredScriptToAddToDocument]; // Specify when and where and what user script needs to be injected into the web document WKUserScript* userScript = [[WKUserScriptalloc]initWithSource:js injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:NO]; // Add the user script to the WKUserContentController instance [userController addUserScript:userScript]; // Configure the WKWebViewConfiguration instance with th |
Swift:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
// Create WKWebViewConfiguration instance var webCfg:WKWebViewConfiguration= WKWebViewConfiguration() // Setup WKUserContentController instance for injecting user script var userController:WKUserContentController= WKUserContentController() // Get script that's to be injected into the document let js:String= buttonClickEventTriggeredScriptToAddToDocument() // Specify when and where and what user script needs to be injected into the web document var userScript:WKUserScript = WKUserScript(source: js, injectionTime: WKUserScriptInjectionTime.AtDocumentEnd forMainFrameOnly: false) // Add the user script to the WKUserContentController instance userController.addUserScript(userScript) // Configure the WKWebViewConfiguration instance with the WKUserContentController webCfg.userContentController= userController; |
JS to Native Bridging Using Script Messages
Your web page can post messages to your native app via the
window.webkit.messageHandlers.<name>.postMessage (<message body>) method.
Here, “name” is the name of the message being posted back. The JS can post back any JS object as message body and the JS object would be automatically mapped to corresponding ObjC / Swift native object.
The following JS code snippet posts back a message when a button click event occurs on a button with Id “ClickMeButton”
JS
|
varbutton = document.getElementById("clickMeButton"); button.addEventListener("click", function() { varmessageToPost = {'ButtonId':'clickMeButton'}; window.webkit.messageHandlers.buttonClicked.postMessage(messageToPost); },false); |
Handling Callbacks Using Script Message Handlers
In order to receive messages posted by your web page, your native app needs to implement the WKScriptMessageHandler protocol.
The protocol defines a single required method. The WKScriptMessage instance returned in the callback can be queried for details on the message being posted back.
ObjC :
|
- (void)userContentController:(WKUserContentController*)userContentController didReceiveScriptMessage:(WKScriptMessage*)message { if([message.name isEqualToString:@"buttonClicked"]) { // The message.body contains the object being posted back } } |
Swift:
|
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) { if let messageBody:NSDictionary= message.body as? NSDictionary{ // Do stuff with messageBody } } |
Finally, the native class that implements WKScriptMessageHandler protocol needs to register itself as a message handler with the WKWebView as follows.
ObjC:
|
// Add a script message handler for receiving "buttonClicked" event notifications posted from the JS document [userController addScriptMessageHandler:selfname:@"buttonClicked"]; |
Swift:
|
// Add a script message handler for receiving "buttonClicked" event notifications posted // from the JS document userController.addScriptMessageHandler(self, name: "buttonClicked") |
Sample Code:
A complete example app demonstrating the bridging concept can be downloaded from https://github.com/rajagp/iOS-WKWebViewBridgeExample-ObjC.git. The Swift Version can be downloaded from https://github.com/rajagp/iOS-WKWebViewBridgeExample-Swift.git.
In this sample app, we load a simple HTML page that has a button using a WKWebView. When the page loads, the native app injects a JS (“User Script”) into the loaded document that listens for button click event and calls back into the native app (“Script Message”) . The native app implements a listener to handle the callback message from the web page and updates the color of the button from within the callback handler
I did a short presentation on this topic at CocoaHeads. The presentation can be downloaded from here.