Native Smartface & WebView Interaction
Using a Hybrid Approach in a Fully Native Environment
Smartface is a fully native development environment with 100% native API access, however, it has a JavaScript based architecture and Smartface applications developed with JavaScript exhibit similarities with Web apps developed via JavaScript. The main difference is that Smartface itself has its own implementation of the UI objects (sf-core) without any use of the DOM which is the basis of the visual portions of the web pages.
Without the DOM support in Smartface due to its native architecture, you cannot use other Web frameworks and libraries such as jQuery or Angular or reactJS. In those cases, Smartface WebView comes in handy, enabling the display of the web content (like a browser) within the App as a component.
Smartface JavaScript engine has its own scope and each WebView has it own scope. They are not interacting to each other directly but there are ways to establish a connection between each other.
In the rest of this guide, WebView component will be used intensively; assuming the reader is knowledgeable about WebView Guide and WebView API Reference.
WebView Code Injection
Smartface WebView instance has a evaluateJS method to run a code block within the displayed Web Page content.
This method takes the script as a string. You cannot pass objects between native app and the WebView directly, as the Smartface app and the Web Page have their own scopes. If you want to pass objects, you can serialize them as JSON and deserialize them on the other side.
const img = document.createElement("img");
img.src = "http://account.smartface.io/Images/logo.png";
document.body.insertBefore(img, document.body.firstChild);
Given code above adds the Smartface logo to the web page, which should be running inside the web page scope.
To run the code in the web page contained in the WebView, following things should be done in order:
- Convert your script-to-run to string
a. The injected code needs to run on document load! - Have your WebView instance set up
- After page is loaded, evaluate the script string
const script = `
function insertLogo() {
const img = document.createElement("img");
img.src = "http://account.smartface.io/Images/logo.png";
document.body.insertBefore(img, document.body.firstChild);
}
if (document.readyState === "complete")
insertLogo();
else
document.onload = insertLogo();
`;
The sample code given above is for modifying the document.onLoad event. If you are using jQuery you can use document.ready().
import WebView from "@smartface/native/ui/webview";
import FlexLayout from "@smartface/native/ui/flexlayout";
const url = "http://example.com/";
const myWebView = new WebView({
left: 0,
top: 0,
right: 0,
bottom: 0,
positionType: FlexLayout.PositionType.ABSOLUTE,
onShow: (event) => {
console.log(event.url);
if (url === event.url) {
//make sure script is running only on specific url(s)
console.log("about to eval");
myWebView.evaluateJS(script);
}
},
});
this.addChild(myWebView);
myWebView.loadURL(url);
import Page1Design from "generated/pages/page1";
import WebView from "@smartface/native/ui/webview";
import FlexLayout from "@smartface/native/ui/flexlayout";
import { Route, Router } from "@smartface/router";
const script = `
function insertLogo() {
const img = document.createElement("img");
img.src = "http://account.smartface.io/Images/logo.png";
document.body.insertBefore(img, document.body.firstChild);
}
if (document.readyState === "complete")
insertLogo();
else
document.onload = insertLogo();
`;
export default class Page1 extends Page1Design {
myWebView: WebView;
constructor(private router?: Router, private route?: Route) {
super({});
}
initWebView() {
const myWebView = new WebView({
left: 0,
top: 0,
right: 0,
bottom: 0,
positionType: FlexLayout.PositionType.ABSOLUTE,
onShow: (event) => {
console.log(event.url);
if (url === event.url) {
//make sure script is running only on specific url(s)
console.log("about to eval");
myWebView.evaluateJS(script);
}
},
});
this.myWebView = myWebView;
this.addChild(myWebView);
}
onShow() {
super.onShow();
const url = "http://example.com/";
this.myWebView.loadURL(url);
}
onLoad() {
super.onLoad();
this.initWebView();
}
}
Using WebView Bridge
WebView Bridge allows bi-directional communication with Web Page. The usages mentioned above, they all are invoked from Native side to Web Page: A JS code is executed and the synchronous response is parsed on the app.
For a press of a button, a timer event, a web socket event on the Web Page can cause an action on the native side. This util library allows event and event data flow to the native side.
In order to trigger an action on the native side, using this WebView Bridge, following code should be executed:
//using window variable is recommended
window.boubleEvent("eventName", eventDataObject);
It is possible to add this call to the Web Page from the source of the Web Page (if the developer can access its source code & change it), or it is possible to inject that code from Native using evaluateJS
- Web Page HTML
- Modified Web Page HTML
<html>
<head>
<title>Sample Page</title>
</head>
<body>
<button id="btn">Press me</button>
</body>
</html>
<html>
<head>
<title>Sample Page</title>
</head>
<body>
<button id="btn">Press me</button>
<script type="text/javascript">
const counter = 0;
const btn = document.getElementById("btn");
btn.addEventListener("click", () => {
window.boubleEvent("eventName", {
pressCount: counter
});
counter++;
});
</script>
</body>
</html>
Another approach is to inject that code from native side. There is no need to use them both; use only one of them.
const wvb = (page.wvb = new WebViewBridge({
webView: page.webView,
source: "http://www.google.com",
}));
page.wvb = wvb;
wvb.on("myEvent", () => {
alert("myEvent", "Event fired");
});
wvb.ready().then(() => {
wvb.evaluateJS(script);
});
const script = `
setTimeout(()=> {
window.boubleEvent("myEvent");
}, 1000);
`;
Example Use Cases
- Display charts or graphics (one way interactions work best for using WebViews in a native app)
- Logins provided via web (e.g. Oauth Implicit flow)
- Remove or hide unwanted elements, such as logos from the web page
- Share authentication
- Display other components that are on the web that require some data from the app
Hide Elements in a Web Page
In this example, a web page inside WebView is shown and 2 native buttons are added to bottom of the native Smartface page. Those buttons are performing show/hide operations within the web page.
<!DOCTYPE html>
<html>
<head>
<title>Smartface - WebView Hide & Show Example </title>
<style type="text/css">
#item {
background-color: RoyalBlue;
color: white;
width: calc(100% - 40px);
height: calc(100% - 40px);
margin: 20px;
display: flex;
align-items: center;
font-size: 72pt;
}
#item div {
width: 100%;
text-align: center;
display: block;
}
html,
body {
height: 100%;
margin: 0;
background-color: DarkSeaGreen;
}
</style>
</head>
<body>
<div id="item">
<div>Item</div>
</div>
</body>
</html>
Link to the web page is also available: https://az793023.vo.msecnd.net/examples/sf-core/webview/hide-show.html
Abstract Smartface code is given below:
const btnHide = new Button({
onPress: () => {
this.webview1.evaluateJS(
'document.getElementById("item").style.display="none";'
);
},
});
const btnShow = new Button({
onPress: function () {
page.webview1.evaluateJS(
'document.getElementById("item").style.display="flex";'
);
},
});
onShow() {
super.onShow();
this.webview1.loadURL(
"https://az793023.vo.msecnd.net/examples/sf-core/webview/hide-show.html"
);
}
- Page Hide Show Code
- Page Hide Show UI
import PageHideShowDesign from "generated/pages/pageHideShow";
import { Route, Router } from "@smartface/router";
export default class PageHideShow extends PageHideShowDesign {
constructor(private router?: Router, private route?: Route) {
super({});
}
initComponents() {
this.btnHide.onPress = () => {
this.webView1.evaluateJS(
'document.getElementById("item").style.display="none";',
() => {}
);
};
this.btnShow.onPress = () => {
this.webView1.evaluateJS(
'document.getElementById("item").style.display="flex";',
() => {}
);
};
}
onShow() {
super.onShow();
this.webView1.loadURL(
"https://az793023.vo.msecnd.net/examples/sf-core/webview/hide-show.html"
);
}
onLoad() {
super.onLoad();
this.headerBar.title = "Hide Show";
this.initComponents();
}
}
{
"components": [
{
"className": ".sf-page",
"id": "73dd-6eef-0570-de44",
"initialized": true,
"props": {
"children": [
"7d83-93d7-e744-5512",
"1587-0041-10da-aeb1",
"2ce7-f3d6-d755-c118",
"e8d7-ed5b-1706-d591"
],
"name": "pageHideShow",
"orientation": "PORTRAIT",
"parent": null
},
"type": "Page",
"userProps": {}
},
{
"className": ".sf-statusBar",
"id": "7d83-93d7-e744-5512",
"props": {
"children": [],
"isRemovable": false,
"name": "statusBar",
"parent": "73dd-6eef-0570-de44"
},
"type": "StatusBar",
"userProps": {}
},
{
"className": ".sf-headerBar",
"id": "1587-0041-10da-aeb1",
"props": {
"children": [],
"isRemovable": false,
"name": "headerBar",
"parent": "73dd-6eef-0570-de44",
"title": "pageHideShow"
},
"type": "HeaderBar",
"userProps": {
"title": "pageHideShow"
}
},
{
"className": ".sf-webView",
"id": "2ce7-f3d6-d755-c118",
"props": {
"children": [],
"name": "webView1",
"parent": "73dd-6eef-0570-de44",
"usePageVariable": true
},
"type": "WebView",
"userProps": {
"backgroundColor": "rgba( 255, 255, 255, 1 )",
"bottom": 0,
"flexProps": {
"flexGrow": 1
},
"testId": "seCIhrddt",
"usePageVariable": true
}
},
{
"className": ".sf-flexLayout",
"id": "e8d7-ed5b-1706-d591",
"props": {
"children": [
"32a0-2435-e9f4-9f5d",
"1e5b-0adf-7131-ac69"
],
"name": "flWrapper",
"parent": "73dd-6eef-0570-de44"
},
"type": "FlexLayout",
"userProps": {
"flexProps": {
"flexDirection": "ROW"
},
"height": 70,
"testId": "ntg288NQf"
}
},
{
"className": ".sf-button",
"id": "32a0-2435-e9f4-9f5d",
"props": {
"children": [],
"name": "btnHide",
"parent": "e8d7-ed5b-1706-d591",
"text": "Hide",
"usePageVariable": true
},
"type": "Button",
"userProps": {
"flexProps": {
"flexGrow": 1
},
"height": 70,
"testId": "nOZpeoW_O",
"text": "Hide",
"usePageVariable": true
}
},
{
"className": ".sf-button",
"id": "1e5b-0adf-7131-ac69",
"props": {
"children": [],
"name": "btnShow",
"parent": "e8d7-ed5b-1706-d591",
"text": "Show",
"usePageVariable": true
},
"type": "Button",
"userProps": {
"flexProps": {
"flexGrow": 1
},
"height": 70,
"testId": "Da1wDDoPp",
"text": "Show",
"usePageVariable": true
}
}
]
}
Display a chart
In this example, an HTML Chart is shown within the WebView and its data is being modified (set) by the native Smartface code.
Chart example has been taken from Chart.js and slightly modified for demo purposes.
Used sample page is on https://az793023.vo.msecnd.net/examples/sf-core/webview/chart.html
- Chart Page Code
- Chart Page UI
import PgChartDesign from "generated/pages/pgChart";
export default class PgChart extends PgChartDesign {
constructor(private router?: Router, private route?: Route) {
super({});
}
initComponents() {
this.btnRandomizeData.onPress = () => {
const script = `
config.data.datasets.forEach(function(dataset) {
dataset.data = dataset.data.map(function() {
return randomScalingFactor();
});
});
window.myLine.update();`;
this.webView1.evaluateJS(script, () => {});
};
this.btnAddDataSet.onPress = () => {
const script = `
const colorName = colorNames[config.data.datasets.length % colorNames.length];
const newColor = window.chartColors[colorName];
const newDataset = {
label: 'Dataset ' + config.data.datasets.length,
backgroundColor: newColor,
borderColor: newColor,
data: [],
fill: false
};
for (let index = 0; index < config.data.labels.length; ++index) {
newDataset.data.push(randomScalingFactor());
}
config.data.datasets.push(newDataset);
window.myLine.update();`;
this.webView1.evaluateJS(script, () => {});
};
this.btnRemoveDataset.onPress = () => {
const script = `
config.data.datasets.splice(0, 1);
window.myLine.update();`;
this.webView1.evaluateJS(script, () => {});
};
this.btnAddData.onPress = () => {
const script = `
if (config.data.datasets.length > 0) {
const month = MONTHS[config.data.labels.length % MONTHS.length];
config.data.labels.push(month);
config.data.datasets.forEach(function(dataset) {
dataset.data.push(randomScalingFactor());
});
window.myLine.update();
}`;
this.webView1.evaluateJS(script, () => {});
};
this.btnRemoveData.onPress = () => {
const script = `
config.data.labels.splice(-1, 1); // remove the label first
config.data.datasets.forEach(function(dataset, datasetIndex) {
dataset.data.pop();
});
window.myLine.update();`;
this.webView1.evaluateJS(script, () => {});
};
}
onShow() {
super.onShow();
this.webView1.loadURL(
"https://az793023.vo.msecnd.net/examples/sf-core/webview/chart.html"
);
}
onLoad() {
super.onLoad();
this.initComponents();
}
}
{
"components": [
{
"className": ".sf-page",
"id": "dd76-5bb1-4adc-c2f0",
"initialized": true,
"props": {
"children": [
"7411-c478-ff2f-1930",
"03a3-01bf-8f91-421b",
"1555-2314-51c9-9161",
"b200-db5a-6e9e-d332"
],
"name": "pgChart",
"orientation": "PORTRAIT",
"parent": null
},
"type": "Page",
"userProps": {}
},
{
"className": ".sf-statusBar",
"id": "7411-c478-ff2f-1930",
"props": {
"children": [],
"isRemovable": false,
"name": "statusBar",
"parent": "dd76-5bb1-4adc-c2f0"
},
"type": "StatusBar",
"userProps": {}
},
{
"className": ".sf-headerBar",
"id": "03a3-01bf-8f91-421b",
"props": {
"children": [],
"isRemovable": false,
"name": "headerBar",
"parent": "dd76-5bb1-4adc-c2f0",
"title": "Chart"
},
"type": "HeaderBar",
"userProps": {
"title": "Chart"
}
},
{
"className": ".sf-webView",
"id": "1555-2314-51c9-9161",
"props": {
"children": [],
"name": "webView1",
"parent": "dd76-5bb1-4adc-c2f0",
"usePageVariable": true
},
"type": "WebView",
"userProps": {
"flexProps": {
"flexGrow": 1
},
"testId": "_QyBNF7be",
"usePageVariable": true
}
},
{
"className": ".sf-flexLayout",
"id": "b200-db5a-6e9e-d332",
"props": {
"children": [
"2808-3b6f-1c76-bd54",
"abe0-a084-69b9-1686",
"9556-e787-9a36-eaba",
"082d-6c0f-166a-2b40",
"d328-f939-9e82-568d"
],
"name": "flWrapper",
"parent": "dd76-5bb1-4adc-c2f0"
},
"type": "FlexLayout",
"userProps": {
"flexProps": {
"flexDirection": "ROW",
"flexWrap": "WRAP"
},
"height": 210,
"testId": "x6w7n_iZz"
}
},
{
"className": ".sf-button",
"id": "2808-3b6f-1c76-bd54",
"props": {
"children": [],
"name": "btnRandomizeData",
"parent": "b200-db5a-6e9e-d332",
"text": "Randomize Data",
"usePageVariable": true
},
"type": "Button",
"userProps": {
"height": 70,
"testId": "UNDG84t8h",
"text": "Randomize Data",
"usePageVariable": true,
"width": 180
}
},
{
"className": ".sf-button",
"id": "abe0-a084-69b9-1686",
"props": {
"children": [],
"name": "btnAddDataSet",
"parent": "b200-db5a-6e9e-d332",
"text": "Add Dataset",
"usePageVariable": true
},
"type": "Button",
"userProps": {
"height": 70,
"testId": "yJ0BYv-VL",
"text": "Add Dataset",
"usePageVariable": true,
"width": 120
}
},
{
"className": ".sf-button",
"id": "9556-e787-9a36-eaba",
"props": {
"children": [],
"name": "btnRemoveDataset",
"parent": "b200-db5a-6e9e-d332",
"text": "Remove Dataset",
"usePageVariable": true
},
"type": "Button",
"userProps": {
"height": 70,
"testId": "aDSMf1DHG",
"text": "Remove Dataset",
"usePageVariable": true,
"width": 150
}
},
{
"className": ".sf-button",
"id": "082d-6c0f-166a-2b40",
"props": {
"children": [],
"name": "btnAddData",
"parent": "b200-db5a-6e9e-d332",
"text": "Add Data",
"usePageVariable": true
},
"type": "Button",
"userProps": {
"height": 70,
"testId": "owsuQpaMm",
"text": "Add Data",
"usePageVariable": true,
"width": 120
}
},
{
"className": ".sf-button",
"id": "d328-f939-9e82-568d",
"props": {
"children": [],
"name": "btnRemoveData",
"parent": "b200-db5a-6e9e-d332",
"text": "Remove Data",
"usePageVariable": true
},
"type": "Button",
"userProps": {
"height": 70,
"testId": "e6j6Wfems",
"text": "Remove Data",
"usePageVariable": true,
"width": 120
}
}
]
}
For more information on the Charts, see the following documentation:
Data ChartsGetting button press from WebView
In this example, let's assume the app is displaying some web content, such as an advertisement campaign. The content of the page is displayed, at the end of the document there is a button, such as "I am interested". If user press to the button, the app should capture the press and do something on the native side.
- Button Press Code
- Button Press UI
import WebVievBridgeTestDesign from 'generated/pages/webVievBridgeTest';
import { withDismissAndBackButton } from '@smartface/mixins';
import { Route, Router } from '@smartface/router';
import WebViewBridge from '@smartface/webviewbridge';
export default class WebVievBridgeTest extends withDismissAndBackButton(WebVievBridgeTestDesign) {
constructor(private router?: Router, private route?: Route) {
super({});
}
initWebViewBridge() {
try {
const url = 'https://az793023.vo.msecnd.net/examples/webviewbridge/buttonevent.html';
const script = `
const btn = document.getElementById("btn");
btn.onclick = function() {
window.boubleEvent("buttonPress");
};
`;
//Within constructor
const wvb = new WebViewBridge({
webView: this.webView1, //WebView. Should be assigned from UI editor.
source: url
});
wvb.on('buttonPress', (data) => {
const text = 'Button pressed';
console.info(text);
alert(text);
//Do your own logic
});
wvb.ready().then(() => {
wvb.evaluateJS(script);
});
} catch (error) {
console.log(error);
}
}
/**
* @event onShow
* This event is called when the page appears on the screen (everytime).
*/
onShow() {
super.onShow();
this.initBackButton(this.router); //Addes a back button to the page headerbar.
this.webView1.loadURL('https://az793023.vo.msecnd.net/examples/webviewbridge/buttonevent.html');
}
/**
* @event onLoad
* This event is called once when the page is created.
*/
onLoad() {
super.onLoad();
this.initWebViewBridge();
}
}
{
"components": [
{
"className": ".sf-page",
"id": "fb98-d27f-d144-77f6",
"initialized": true,
"props": {
"children": [
"c75a-bf12-3dd1-8036",
"29b7-dcda-bc59-8b09",
"6b25-f8da-fcf9-6af0"
],
"name": "pgButtonPress",
"orientation": "PORTRAIT",
"parent": null
},
"type": "Page",
"userProps": {}
},
{
"className": ".sf-statusBar",
"id": "c75a-bf12-3dd1-8036",
"props": {
"children": [],
"isRemovable": false,
"name": "statusBar",
"parent": "fb98-d27f-d144-77f6"
},
"type": "StatusBar",
"userProps": {}
},
{
"className": ".sf-headerBar",
"id": "29b7-dcda-bc59-8b09",
"props": {
"children": [],
"isRemovable": false,
"name": "headerBar",
"parent": "fb98-d27f-d144-77f6",
"title": "pgButtonPress"
},
"type": "HeaderBar",
"userProps": {
"title": "pgButtonPress"
}
},
{
"className": ".sf-webView",
"id": "6b25-f8da-fcf9-6af0",
"props": {
"children": [],
"name": "webView1",
"parent": "fb98-d27f-d144-77f6",
"usePageVariable": true
},
"type": "WebView",
"userProps": {
"flexProps": {
"flexGrow": 1
},
"testId": "UgtVwmh27",
"usePageVariable": true
}
}
]
}
WebView Bridge - Loading Dynamic Libraries
WebView bridge also allows loading other libraries into the WebView. In the Webviewbridge loadScripts method will be used to perform this action.
This method is adding script tags with src value set to the given URL. This URL should be relative to the page. If it is not relative, absolute URL should be used.
Loading of the scripts are performed after WebView Bridge is ready.
This method also returns a Promise
object. This Promise
should be used to evaluate scripts which are dependent on the libraries which are loaded with the loadScripts method.
Loading Cookie into WebView
In this example, a cookie string will be injected into the WebView. This cannot be done with JavaScript injection, because the very first initial request should be already authenticated.
import System from "@smartface/native/device/system";
const cookie = "Your full cookie";
const targetUrl = "http://example.com";
if (System.OS === System.OSType.IOS) {
//@ts-ignore
const MutableRequest = SF.requireClass("NSMutableURLRequest");
//@ts-ignore
const NSURL = SF.requireClass("NSURL");
const request = MutableRequest.requestWithURL(NSURL.URLWithString(targetUrl));
request.addValueForHTTPHeaderField(`${cookie}`, "Cookie");
webview.nativeObject.load(request); //this is equvalent of webview.loadURL
} else {
//@ts-ignore
const CookieManager = requireClass("android.webkit.CookieManager");
const cookieManager = CookieManager.getInstance();
cookieManager.setAcceptCookie(true);
cookieManager.setCookie(targetUrl, `${cookie}`);
webview.loadURL(targetUrl);
}
evaluateJS Performance
WebView.evaluateJS is used for two things:
- To execute a JavaScript on the WebView
- Parsing and getting the result of the executed JavaScript statement
Regardless of what both of the steps will be executed. In some cases, the result of the executes JavaScript statement might be too long to parse. This will greatly reduce the performance of the evaluateJS execution.
Simply add a single null
statement to the end of the JavaScript statement, this will reduce the parsing of the result operation.
WebViewBridge is designed with that performance trick. Here is the code block from WebView Bridge:
function evaluateJS(javascript, onReceive) {
if (!this.parseResponses)
//a flag used for wrapping the code or not
javascript = `(function(){\n${javascript}\n})();null;`; //Wraps the Code in an immedaite function
return this.webView.evaluateJS(javascript, onReceive);
}