Hi there,
In this tutorial I want to share my experience related to WebViews in Swift 3.0 and explain how I managed to communicate back and forth between the JavaScript and native code.
In my project I needed to be able to store content in the UserDefaults
persistent storage and later make that available to the JavaScript code for off-line usage.
Specifically I needed to present the user with an introductory video the first time the app was loaded, then store some sort of persistent variable that would switch to the main content ViewController when the app was opened the second time.
Later on, the requirement was to display user related data in the webView
First action for me is to copy the contents of the whole WebView folder in the .documentDirectory
of my app so that I can have access to the files. In the application
function in AppDelegate
I check to see if my folder already exists in the .documentDirectory
and if it doesn't I copy it there.
My app is a Video Poker game, so you will see a lot of references to that in the source code.
let sharedpath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let gamefolder = sharedpath.path + "/videopoker";
if( FileManager.default.fileExists( atPath: gamefolder ) == false ){
do {
try FileManager.default.copyItem(
atPath: Bundle.main.url(forResource: "videopoker", withExtension:"")!.path,
toPath: gamefolder
)
} catch {
print("Couldn't copy videopoker folder");
print(error);
}
}
Switching gears to the ViewController file, import the necessary libraries to be able to play the video in the viewDidAppear
method.
Also, we will make use of NotificationCenter
to add an observer function which will be called when the video has finished playing.
Inside the observer, we will store a persistent value - "appInitializedOnce" and later on use that to figure out that the video has played once, or even better that the app has been loaded for the first time.
import AVKit;
import AVFoundation;
// ...
self.moviePlayerViewController = AVPlayerViewController()
let path = Bundle.main.path(forResource: "1", ofType: "mp4"); // the video file (don't forget to import it in xcode)
let url = NSURL.fileURL(withPath: path!)
self.moviePlayer = AVPlayer(url: url);
self.moviePlayerViewController!.showsPlaybackControls = false;
self.moviePlayerViewController!.player = moviePlayer;
self.present(self.moviePlayerViewController!, animated: true) {
self.moviePlayerViewController!.player!.play()
}
// add the observer which will call the "playerDidFinishPlaying" method
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.playerDidFinishPlaying), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: self.moviePlayer?.currentItem)
// ...
func playerDidFinishPlaying() {
NotificationCenter.default.removeObserver(self); //don't forget to remove the observer now
let userDefaults = UserDefaults.standard
userDefaults.set(true, forKey: "appInitializedOnce"); //store the value for "appInitializedOnce"
//switch back to the main thread
OperationQueue.main.addOperation {
let gameViewController = self.storyboard?.instantiateViewController(withIdentifier: "Game") as! GameViewController
// tricky --- don't use self here because the "visible controller" is now the "moviePlayerViewController"
self.moviePlayerViewController?.present(gameViewController, animated: true, completion: nil)
}
}
The small game I've written is written in JavaScript so I need a WebView to load the html in the folder I've copied at Step 1
Because the contents of the WebView is not relevant to our discussion, we will focus more on the back and forth communication between native and JavaScript.
The GameViewController
needs several configuration options to be active in order to allow for communication between swift code and JavaScript.
The WKUserContentController
and WKScriptMessageHandler
will allow to inject JavaScript code into the DOM and listen to "posted" messages from the WKWebView.
In a few easy steps, here's what happens in the code below:
WKUserContentController
WKUserScript
WKUserScript
into the WKUserContentController
WKWebViewConfiguration
object
import UIKit
import WebKit;
import JavaScriptCore;
class GameViewController: UIViewController, WKScriptMessageHandler {
var webview: WKWebView?;
// ...
override func viewDidAppear(_ animated: Bool) {
let contentController = WKUserContentController();
let userScript: WKUserScript;
// the injected javascript
userScript = WKUserScript(
source: "console.log('test')",
injectionTime: WKUserScriptInjectionTime.atDocumentEnd,
forMainFrameOnly: true
)
contentController.addUserScript(userScript);
//set the native handlers for the post back of the JS methods
contentController.add(
self,
name: "nativeCallbackHandler"
);
//put the above config into a brand new wkwebview
let config = WKWebViewConfiguration()
config.userContentController = contentController
self.webview = WKWebView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: self.view.frame.height), configuration: config);
//put the wkwebview into the view and load the html url but first close any previously opened webviews
for subview in self.view.subviews {
if(subview is WKWebView) {
print("Found the previous wkwebview");
subview.removeFromSuperview();
}
}
self.view.addSubview(self.webview!)
//load the html from the folder we've copied it in at step 1
// allowing access to the root folder of the ".documentDirectory"
let folderPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!;
let url = URL(fileURLWithPath: folderPath.path + "/videopoker/index.html");
self.webview!.loadFileURL(url, allowingReadAccessTo: folderPath);
}
}
In the same GameViewController
create a userContentController
function to handle any data coming back from the WKWebView
From JavaScript, you post like this: window.webkit.messageHandlers.nativeCallbackHandler.postMessage(JSON.stringify({"token": token, "user_id": "" + user_id}));
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if (message.name == "nativeCallbackHandler") {
//whatever you wanna do here
//here's an example which related to the above mentioned JavaScript call
let userDefaults = UserDefaults.standard
userDefaults.set(message.body, forKey: "userData")
}
}
As seen on step 2, we have stored the key called "appInitializedOnce" as true when the video finished playing. Thus, what we need to do now is to take that into account when loading the app, so we return to AppDelegate.swift
and add the code below in the application method - Show the GameViewController
directly if the app has been opened at least once in the past.
//...
//If the userData is already set (user has already seen the video) don't show the welcome video anymore
let userDefaults = UserDefaults.standard
let appInitializedOnce = userDefaults.bool(forKey: "appInitializedOnce");
if ( appInitializedOnce == true ) {
self.window = UIWindow(frame: UIScreen.main.bounds)
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let initialViewController = storyboard.instantiateViewController(withIdentifier: "Game")
self.window?.rootViewController = initialViewController
self.window?.makeKeyAndVisible()
}
//...
This small example is meant to get the coder used to how WebViews work in Swift 3.0 and to acquaint the beginner Swift developer with concepts such as ViewControllers, Video Players and hybrid applications development for iOS.
You can view, clone the whole source code of the application on GitHub. Thanks for reading, hope this proves useful.