Using Javascript with WKWebView

Harnessing The Power of Both Languages in Your Apps

WebKit allows us to use javascript along side with the native swift code. On one hand, we could call the javascript statements in swift. On the other hand, javascript from web view could be able to trigger a delegate method defined in swift code. This gives us a two way communication between the native swift code and javascript used by a web view.

Basic Setup

Here we have a window with a WKWebView showing some text. It has 2 buttons inside. One for showing the text and triggering a swift handler, the other for hiding the text. Also, we have a native button that triggers a javascript function to hide the text.

The Demo App

Triggering Javascript Functions from Swift

The IB outlet and action of the app as follow. Note that we simply use evaluateJavaScript(_:completionHandler:) to trigger some javascript in the web view, hideText() is a custom method we defined inside javascript.

Receiving Javascript Messages

To receive message from the javascript, we need to provide a message name for javascript to call upon. We just name it “jsHandler”. After that we load the html and javascript into the web view.

We also need to adopt the WKScriptMessageHandler protocol in our view controller, and implement the userContentController(_:didReceive:) method. For simplicity, we just print out the message received from the javascript.

The HTML and javascript we loaded as follow. Note that in the showText() method, the window.webkit.messageHandlers.jsHandler.postMessage() is the method we use to trigger the delegate method implemented in our view controller.

Download Sample Project

Adding Login Items for macOS

Automatically launch your app after login

To create an app that can be auto launched after login (i.e. adding login item) is far more complicated then expected…

For sandboxed app, the recommended approach is using the Service Management Framework. (Adding login items using a Shared File List is another approach for non-sandboxed app, which will not be discussed here.)

The basic concept is to create an “Helper Application” that registered to the system, which responsible for launching your main app while user login.

The Helper App

In your project, create a new Cocoa Application target.

Project with 2 targets

As the helper app should have no visual element, remove the view controller and window scene from the storyboard, as well as the related swift files.

  • Remove the ViewController.swift
  • Remove the View Controller Scene in the storyboard
  • Remove the Window Controller Scene (if you want visual on the helper app to see if its successfully launched by the main app or not, as well as launched by the system service or not after logging in, you may keep it for a while and delete it afterwards)

As the helper app will have the same default AppDelegate.swift which conflict with the main app. You need to rename it and adjust the class name of App Delegate in the Application Scene accordingly.

Renaming Conflicted Files

Then make the helper app as a background service. In Info.plist, set Application is background only to Yes.

Info Plist Setting of the Helper App

In the app delegate, check if the main app is already running or not. If not, launch it.

The tricky part is to delete the last four path components of the helper app bundle path. It is because the helper app is actually embedded inside the main app bundle, under the subdirectory Contents/Library/LoginItems. So including the helper app name there will be a total of 4 path components to be deleted.

Another tricky part is to set skip install = YES in the build setting.

Build Setting of the Helper App

You then turn on App Sandbox in Capabilities of the helper target, and the helper app part is done.

Main App

You first import ServiceManagement, then check if the helper app is already running or not, launch and register it with SMLoginItemSetEnabled(_:_:) if necessary.

In my sample app I created an checkbox to toggle the helper app, as well as showing if its already launched or not.

We then copy the helper app into the main app bundle. In the Build Phases of the main app, create a new Copy Files phase.

New Copy Files Phase

with the following settings:

  • Destination: Wrapper
  • Subpath: Contents/Library/LoginItems
  • Add the helper app file
Embed the helper app

The last step is to enable App Sandbox and Development Signing of both main and helper app, then your are good to go.

Testing

You can test the auto launch feature as follow:

  1. Build the main app — NOT RUN (if you build and run the main app in Xcode, the helper app may not function after quitting Xcode)
  2. Right click the product of your main app → Show in Finder
  3. Double click the main app in Finder to launch it, and check the auto launch button
  4. Quit everything and log out
  5. Log in again

The main app should be auto launched by the helper, with the auto launch button checked.

Main app

Download Sample Project

Create Menu Bar Apps On macOS

Screen Shot 2017-06-12 at 6.51.22 P

Menu bar apps refer to apps that sit on the menu bar (a.k.a. status bar) of macOS. It provide instant access of the key functionalities, as well as being accessible all time during a login session.

Creating the Status Bar Item

We use NSStatusBar.system().statusItem(withLength: -1) to request a status bar item from the system. Note that it may be failed (return nil) if the status bar is already full packed with many other status bar items.

Upon successfully got an status bar item, you got a NSButton as the UI element to put on the menu bar. You need to configure it by providing an image, as well as setting the target and action of the button.

All these should be done during the app launching period, so in the app delegate, we have:

Note that a strong reference must be used to retain the status bar item object returned by the system. Otherwise it will be released afterwards, and nothing will be shown on the menu bar.

Displaying the Menu

To display the menu, you need to provide a reference point for placing the menu object, in this case, the status bar button. We then create a mouse event, and use popUpContextMenu(_:with:for:) to bring up the menu.

Info.plist Setting

If your app is menu bar ONLY, similar to the Dropbox macOS client, you need to set Application is agent = YES in your Info.plist in order to omit the Dock’s app icon and the app menu on the upper left corner.

Download Sample Project

Providing Services On macOS

Make your app run as a background service

Apps providing services allows their users to use some of the features while using other apps. For example, you can let the users to launch your app by pressing a short cut key (say ⇧⌘E) while highlighting some text in Safari.

The first thing you need to do is to tell the system the details of your service. Say your app name, data type that you are going to handle, how to activate it… etc. You do so by defining a NSServices key in the Info.plist. Here is an example:

<key>NSServices</key>
<array>
<dict>
<key>NSKeyEquivalent</key>
<dict>
<key>default</key>
<string>E</string>
</dict>
<key>NSMenuItem</key>
<dict>
<key>default</key>
<string>My App Name</string>
</dict>
<key>NSMessage</key>
<string>myServiceHandler</string>
<key>NSPortName</key>
<string>Easy Dictionary</string>
<key>NSRequiredContext</key>
<dict/>
<key>NSSendTypes</key>
<array>
<string>NSStringPboardType</string>
</array>
</dict>
</array>

The meaning of the above key — value as follow:

  • NSKeyEquivalent: the short cut key of your service, preceding with shift-command
  • NSMenuItem: the title of your service inside the “Service” menu
  • NSMessage: the method in your app to handle the service request
  • NSPortName: the name of app. i.e. the product name of your build target
  • NSRequiredContext: the context of your service, i.e. in what situation your service name will be shown on the Services menu. Note even you provide an empty dict (means you want to show your service in all possible context), you still need to include this property otherwise your service will not be shown.
  • NSSendTypes: the data type that your service will be handled. It is also the data type that will be send to your handling method defined in NSMessage.

The details of these properties could be found in Apple documentation.

Screen Shot 2017-06-11 at 9.30.03 P

Service Handling Method

The handling method takes 3 arguments, in the form of functionName(_ pboard: NSPasteboard, userData: String, error: NSErrorPointer) Note that the argument label for the first parameters must be omitted (underscore) starting from Swift 3.

func myServiceHandler(_ pboard: NSPasteboard, userData: String, error: NSErrorPointer) {
if let str = pboard.string(forType: NSPasteboardTypeString) {

You then tell the system where you put your service handling method, you can do so while your app is launched. In the example below, self means the service handler is defined in the app delegate.

func applicationDidFinishLaunching(_ aNotification: Notification) {
NSApp.servicesProvider = self
}

Debugging

You register your app to the system by putting it into the Application folder. After re-logging in, your service should be shown in the Services menu. However, if you can’t find your service on the menu, you may want to know if it is successfully registered or not. All registered services could be found by typing the following command in terminal.

/System/Library/CoreServices/pbs -dump_pboard

More details could be found in the Services Implementation Guide.

UI Preparation of Universal Apps

To me, one of the most un-willing task for making universal iOS apps is to prepare the UI of iPad. Because I have to re-do everything that I’ve done for the iPhone UI, such as putting UI elements, setting UI constrains, connecting IBOutlets, setting IBActions…etc. I just boringly repeating my self…

Xcode didn’t provide any function to “migrate” an iPhone storyboard to iPad. However I saw a thread in Stack Overflow discussing how to do it manually. After experimenting myself, here summarised what I’ve done:

  1. Duplicate the iPhone storyboard and rename it Main_iPad.storyboard

  2. Right click and choose “Open As” -> “Source Code”

  3. Search for targetRuntime=”iOS.CocoaTouch”and change it to targetRuntime=”iOS.CocoaTouch.iPad”

  4. Replace <simulatedScreenMetrics key=”destination” type=”retina4″/> to <simulatedScreenMetrics key=”destination”/>

  5. Save everything and restart Xcode.

  6. In your target’s General tab, choose your newly edited storyboard in the iPad’s “Main Interface” setting.

Though we still need to adjust the size of the UI elements to fit into the iPad screen size, it saves us tremendous amount of time as all the outlets / connections / UI elements / constrains are there already!!