Article summary
I recently helped develop a native iOS app for a client that sells software to many different educational organizations. We wrote the app in Swift, and it interacts with our client’s pre-existing web API.
One challenge we faced was that many of our client’s customers require single-account, multiple-login (SAML) support through their own web portals. To support SAML, we needed an easy way to pass a user’s API credentials from a web page to our iOS application. In this post, I’ll show how this can be accomplished using WKWebView
.
The Problem
The high-level overview of the issue we faced:
- Our client provides software to many organizations, and each has users that must log in.
- To log in, a user must identify his or her organization.
- After identifying the organization, the user can log in using our client’s API or, in some cases, an external web site hosted by the organization.
With SAML, users are sent to the organization’s web page to log in, then redirected back to a landing page that our client controlls. Once that redirect occurs, we have the login credentials (for instance, a user token required by our client’s API). The question is: How do we pass that token to the native iOS app?
WKWebView
In iOS 8, Apple provided WKWebView as part of WebKit, a newer way of embedding web views in a native app. (Among other features, WKWebView boasts better performance than the older UIWebView.) In the following code samples, I will show how a developer can pass data in the form of a JavaScript object from a web page to the native iOS app.
First, create a simple UIViewController in Interface Builder and give it a custom class. I chose to name mine SamlLoginViewController
. In the view controller’s loadView
method, we create the WKWebView
and set up the communication channel between the web and our app as follows:
import UIKit
import WebKit
class SamlLoginViewController: WKScriptMessageHandler {
var webView: WKWebView?
let userContentController = WKUserContentController()
override func loadView() {
super.loadView()
let config = WKWebViewConfiguration()
config.userContentController = userContentController
self.webView = WKWebView(frame: self.view.bounds, configuration: config)
userContentController.addScriptMessageHandler(self, name: "userLogin")
self.view = self.webView
}
Before diving into loadView
, note that there are two instance variables declared at the top: webView
and userContentController
. Further, we are going to add the WKScriptMessageHandler
protocol to our class. Our class will handle messages passed from JavaScript on the web page.
To create a WKWebView
, we first construct a WKWebViewConfiguration
object and set its userContentController
value to the WKUserContentController
on our class. The WKWebView
can then be constructed like this:
let config = WKWebViewConfiguration()
config.userContentController = userContentController
self.webView = WKWebView(frame: self.view.bounds, configuration: config)
After creating the WKWebView
, we set up the message handler:
userContentController.addScriptMessageHandler(self, name: "userLogin")
The side-effect of addScriptMessageHandler
is that it will create an object that will be added to window.webkit.messageHandlers
for any webpage loaded in the WKWebView
. In this case, we name the handler “userLogin” so our JavaScript code will be able to access window.webkit.messageHandlers.userLogin
.
Next, we need to create the delegate method to receive messages in our view controller:
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
let dict = message.body as! [String:AnyObject]
let username = dict["username"] as! String
let secretToken = dict["secretToken" as! String
// now use the name and token as you see fit!
}
The JavaScript Object
The last piece of the puzzle is the JavaScript needed to send the message. The following code must be run on the page loaded into the WKWebView
:
This code creates a simple HTML button and adds an even listener to call the messageHandler that is automatically set up by WKWebView
. You can verify that window.webkit.messageHandlers.userLogin
only exists in the context of your iOS app’s view. Just visit the URL in a browser and it will be undefined. When postMessage
is called, the iOS app receives the sent JavaScript object. It’s that easy!
Funny, I’ve never seen that acronym for SAML before. I thought it was Security Assertion Markup Language.
But thanks for tutorial. This is just the info I needed.
Had you a requirement in your project to return a value from native function back to JS calling function? Or maybe you know, if it’s possible to return a value or at least give js callback function as a parameter to the native function?
I have a question regarding the “sent javascript object” that we receive in the native code.
What kind of object is it then? Javascript?
How to handle it?
Regards
Alexandre,
It comes back as a dictionary:
let dict = message.body as! [String:AnyObject]
And you can use it as follows:
let username = dict[“username”] as! String
let secretToken = dict[“secretToken” as! String
See the code snippet right above “The JavaScript Object”
Oh sorry I missed that… Thanks a lot John. Could have tested it but I didn’t had access to xCode at the time.
Cheers
Hi I was wondering if there is a way to save the username and password that the user enters when the wkwebview is loaded. In my app, we don’t have a separate webview for login, it is just the same url that connects to the website. I need to access the username and password that the user enters in order to send it to the api and receive the user token. Any ideas?
You can do that using the same approach I outlined in this blog post. It’s probably not advisable to store the password– you may instead want to store the API token. Be sure to store it in a secure location. For iOS I recommend you use the Keychain.
This cocoapod looks like a good bet:
https://cocoapods.org/pods/SwiftKeychainWrapper
Is there any way to interpret wbkit.postmessagehandler in UIWebView
I’m trying to do more or less the same thing but I cannot get the events back to ios.
I have google this around but don’t seem to get my head around this issue.