Tools Blog Learn Quizzes Smile API Log In / Sign Up
Tools Blog Learn Quizzes Smile API Log In / Sign Up
« Return to the tutorials list
We have updated our privacy policy to let you know that we use cookies to personalise content and ads. We also use cookies to analyse our traffic and we share information about your use of our site and application with our advertising and analytics partners. By using this website or our application you agree to our use of cookies. Learn more about the way this website uses cookies or remove this message.

You can read this article in: English :: Español :: русский

Proper communication between JavaScript in WKWebView and SWift 3.0

October 14, 2016 Difficulty: 20 / 50 Tweet
swift-js-wkwebview-poker-game

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

Step 1: Let's start with the AppDelegate.swift file

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);
            }
        }
    

Step 2: Let's play the introductory video file now

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)
            }
        }
	

Step 3: Now let's show the GameViewController which in turn loads the html page with our game

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:

  1. create the WKUserContentController
  2. create the WKUserScript
  3. put the WKUserScript into the WKUserContentController
  4. create native callback handler JS object
  5. put all of the above into a WKWebViewConfiguration object
  6. load the WKWebView with the above created configuration
  7. load the file URL allowing access to the ".documentDirectory" (otherwise features like HTML 5 video tags won't work)
  8. present the WKWebView

	
        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);
            }
        }
	

Step 4: Handle the "post" coming from JS

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")
            }
        }
	

Step 5: Don't show the video if already shown once

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.

comments powered by Disqus

Better Docs For A Better Web - Mozilla Developer Network

Alerts

2017-05-23 - The zend_string_extend function in Zend/zend_string.h in PHP through 7.1.5 does not prevent changes to string objects that result in a negative length, which allows remote attackers to cause a denial of service (application crash) or possibly have unspecified other impact by leveraging a script's use of .= with a long string. Read more ...
2017-02-16 - Format string vulnerability in the php_snmp_error function in ext/snmp/snmp.c in PHP before 5.5.34, 5.6.x before 5.6.20, and 7.x before 7.0.5 allows remote attackers to execute arbitrary code via format string specifiers in an SNMP::get call. Read more ...

See All Entries...