Build a macOS UI with JS and WebKit

Creating simple macOS applications using Swift and Xcode is easy, but it's complicated for complex ones. To produce a modern looking UI, you have to put in a lot of effort to get a good looking result.

If you are an experienced web developer it can be a lot easier to get the UI you want by using web technologies. That's why many developers today are using Electron to develop their desktop applications.

Electron apps are getting a lot of critic because of the file size and memory usage. But they also do have a big advantage, they are cross-platform out of the box.

If you are developing a macOS only app, then there is an alternative for you. You can build it using Swift for the backend, and a WKWebView for the user interface.

The App

As a proof of concept, I developed a simple system monitor application. The source code is available on GitHub.

The application consists of two separate layers. The first one is a standard Swift Cocoa application providing the WKWebView for the UI. It is also responsible for collection the system information using the SystemKit library.

Second, a web application, build using Create React App. This application layer is providing the user interface using JavaScript, HTML, and CSS.

And that's how it looks:

WebkitSysmon Screenshot

JavaScript ↔︎ Swift Communication

The communication between Swift and JavaScript is always asynchronous. To get new data from the system, the JavaScript layer has to periodically poll for it.

To be able to receive such a polling request, the Swift backend registers a script message handler named refresh:

userContentController.add(self, name: "refresh")

After getting the new system data, it will get passed to the global JavaScript function window.pushData as a JSON object:

webView.evaluateJavaScript("window.pushData(\(data));", completionHandler: nil)

This callback is registered in the main React component of the application:

window.pushData = this.dataHandler;

The callback itself is pretty simple:

dataHandler = data => {
    this.setState({
        data: data,
        lastUpdated: new Date()
    });
    this.timer = setTimeout(this.refresh, 1000 * 10);
};

And finally here is the function which triggers this whole process:

refresh() {
    window.webkit.messageHandlers.refresh.postMessage({});
}

Development

Developing the Swift part of the application is done with Xcode in the same way as every other macOS app. For the JavaScript part, on the other hand, you will need to use different tools.

Xcode provides basic support for web development, but I chose to use VSCode for developing the JavaScript and CSS styles of the project.

It's also necessary to start the development server in a terminal:

cd webkit-sysmon-ui
yarn start

After that, you can start WebkitSysmon from Xcode and the content of the WKWebView will reload automatically on every change without restarting the application.

Debugging the web application

It is possible to use Safari to debug the running application. You can connect to the running instance of your WKWebView through the "Develop" menu of Safari. You will find your running instance under your hostname.

After the connection, Safari will open a web inspector window where you can:

  • Inspect and modify the HTML and CSS
  • View the web console
  • Set breakpoints and debug your application

Should you do it?

Well, it depends!

If your application doesn't have to be cross-platform, it can be a good solution. Especially if the UI is not very macOS like or you are converting an already existing web application.

Compared to an Electron app, this solution is a lot better regarding startup time, memory and disk usage. Of course, it's not a cross-platform application, but it should be easy to port if needed.

If you need deep macOS system integration, or you are not very experienced in web development, please think twice using it.

You can always use it just for prototyping the UI. The direct feedback of your changes will help you a lot.