Rich Text Editor for iOS using WKWebView

If you are a iOS engineer working for a tech company like HubSpot, where the product main access point is a browser, the chances that you will have to deal with HTML/CSS are very high. In some sections of HubSpot there are text fields (see image below) where content is html and styling is handled via CSS.

While building the HubSpot iOS app we needed a text field able to display content created from the web version and vice versa, able to create content that web version can consume. Summarizing the text field has to satisfy the following requirements:

  • Multi-line support
  • Display HTML
  • Basic CSS support
  • Bold, italic and underline styles support (BIU)

The first thing that I tried was UITextView and NSAttributedString which can be used to display HTML passing NSHTMLTextDocumentType as NSDocumentTypeDocumentAttribute. The problem with UITextView is that it doesn’t support the CSS styles needed to create the annotation in the image below.

Also there’s no way to select BIU out of the box, and you will need to implement a custom UIToolBar and assign it to the inputAccessoryView property. We needed something else to solve this problem and so we started to look for rich text editors.

If you google for rich text editors you will find some implementations that are using a UIWebView. By using a webview you will get full HTML/CSS support so we gave it a shot. For apps running on iOS 8 and later Apple recommends to use WkWebView. WKWebView offers a richer api and a better Javascript engine so let’s implement a basic rich text editor with a WKWebView and Swift.

We need three things:

  • HTML page containing a div with the contenteditable attribute
  • Javascript file for setting/getting information to/from HTML
  • custom UIView that wraps the WKWebView

HTML

For the HTML page we can use a basic template like this:

When you tap on a webview showing a contenteditable element the system will automatically present the keyboard and you will also get BIU styles from the long press menu.

Javascript

One of the things that I really like about WKWebView is the way you can communicate from Javascript to native code. Using UIWebView the only way is by changing window.location passing the required parameters in the query string and implementing shouldStartLoadWith delegate function. In this delegate function you have to parse the request url to understand the message that has been sent from Javascript.
Using WKWebView we have a new set of tools for achieve this: WKUserContentController and WKScriptMessageHandler protocol.

WKUserContentController can be used to register message handlers and to inject Javascript. Using func add(_ scriptMessageHandler: WKScriptMessageHandler, name: String) we can add a message handler that will result in exposing a function window.webkit.messageHandlers.name.postMessage(messageBody) to the Javascript environment. Using func addUserScript(_ userScript: WKUserScript) we can inject arbitrary Javascript code.

WKScriptMessageHandler is a protocol that contains a single function and this function gets called, on the conforming object, each time you use window.webkit.messageHandlers.name.postMessage(messageBody) from javascript and the received WKScriptMessage object is an object that encapsulates the message body and the message name.

The Javascript file will expose an api to insert text and to get the height of the current displayed text. This is the script that I am using:

WKWebView wrapper

Putting it all together we can create a re-useable rich text component that subclasses UIView. The class exposes a text, height and placeholder properties and it also has a delegate property used to notify text and height change. You can find the implementation here.

We can use this view like any other UIView and set the delegate if you need to handle dynamic height and retrieve the content text. One important thing that you need to watch out for is the userContentController strong reference to the message handler. This is the reason behind the use of WeakScriptMessageHandler.

The gif below shows the final result using random HTML. As you can see it handles many HTML/CSS combinations very well.


Any of this sound interesting? HubSpot is hiring iOS developers! You can find more details here.