Dynamic fonts in WKWebView

I like the fact that Apple takes accessibility quite seriously. We have talks during WWDC, and tools like the Accessibility Inspector to help us test and audit our apps.
In this article I’ll show you how to write your CSS in order to support dynamic fonts in iOS, so when your user changes the preferred font size in settings you can make sure your app, displaying web content that you control. As a bonus, I’ll even show you how to support dark mode.

Sample app

As usual, I have a sample app on GitHub so you can find all the code there. There is little to do in Swift actually, but you have to write some CSS.
I’m not familiar with Ionic or other similar frameworks, they (hopefully) have a solution to this problem. I guess at least the CSS implementation I’ll show you should work on with those frameworks, as it doesn’t require any native implementation, but if you have custom fonts I have a solution for them that requires some code to be written in a ViewController.
In my project the HTML is on the html directory. You can find a simple html, a CSS and a Javascript file.

Dynamic fonts in CSS

Let’s start with the simpler solution. Suppose you like Apple’s system font, the one you’d normally use on a UILabel unless you provide another font. If you want to use this font, good news: you can set it in CSS and the system will take care of “everything” for you.
In fact, you still have to do a little work if you want your app to respond to changes in settings while running. I’ll show you how to do it later on, for now, let’s focus on the CSS.


:root {
   font: -apple-system-body;
}

That’s it. You’re telling the web view to render the page with Apple’s system font of type body. Try to increase it in settings and you’ll see the correct font.
I chose body, but you can access all the variations like headline, subheadline, caption1 etc.
Try using the Accessibility Inspector (right click on the Xcode icon, Open Developer Tool) and change the font size. Nothing happens, as you need to refresh the page in order to see the font changing. This is not ideal, let’s fix it.

Get notified of font size change

In order to respond to font size changes while our app is running, we need to add an observer to the NotificationCenter. The implementation can be found here


NotificationCenter.default.addObserver(self, selector: #selector(preferredContentSizeChanged(_:)), name: UIContentSizeCategory.didChangeNotification, object: nil)

@objc private func preferredContentSizeChanged(_ notification: Notification) {
        if let userInfo = notification.userInfo,
            let contentSize = userInfo["UIContentSizeCategoryNewValueKey"] {
            print("new category \(contentSize)")
        }
        let font = UIFont.preferredFont(forTextStyle: .body)
        if useCustomFont {
            notifyFontSize(newSize: font.pointSize)
        }
        else {
            webView.reload()
        }
        
    }

Every time the user changes the Font size in settings (you can simulate it in Accessibility Inspector) this function gets called.
If you’re using the system font, you can just reload the web view and see the font updated to the correct size. That’s great, but I can use this example to show you how to use custom fonts, and still help your users if they need a larger font.

Custom font

Suppose you have a custom font, the web view won’t change its size automatically.
You have to do a little bit of work in your ViewController, and on the CSS.
Let’s start with the ViewController.
In the code pasted above you see I can read the userInfo of the notification, and I can get the new category value. Is a String, and you could implement an enum to decide what to do, but I find it too complex and I prefer to leave the decision to the web content.
In the example I’m still using the body variant of the font as a reference, but I don’t need it on the CSS. In the ViewController, after the notification is received, I can check for the current size of the body font via let font = UIFont.preferredFont(forTextStyle: .body) then I can pass it to the web content by calling a javascript function. That’s all I need to do in Swift, the rest is pure CSS and Javascript.


private func notifyFontSize(newSize:CGFloat) {
        let javascriptFunction = "fontSizeChanged(\(newSize))"
        webView.evaluateJavaScript(javascriptFunction) { (result, error) in
            print("font size changed in webview")
        }
    }

And now, some Javascript


function fontSizeChanged(size) {
    console.log("font changed size = " + size);
    var pixelSize = size + "px";
    document.documentElement.style.setProperty("font-size", pixelSize);
}

As you can see, I changed the font-size property of the root element. If we use rem instead of px for our fonts in CSS, changing only the font-size for the root element should be enough.


:root {
    font-size:16px;
}

h1 {
    font-size: 2rem;
    color: red;
}

p {
    font-size: 1.5rem;
}

H1 is 2rem, so 2 times the font-size defined in :root, p is 1.5. When we increment the font-size in the Javascript function h1 and p will be updated as well, and we don’t need to reload the page this time.
I chose 16px as an example, you may want to find the “magic” number that suits your need for your web content.

Support dark mode

All right, time for the bonus section. I have a good news: you can implement dark mode in CSS and you won’t need to mess with the NotificationCenter in order to have your web view update automatically.
I wrote an article about implementing dark mode so if you want to know how to get notified when the user changes aspect, you can find everything there.


:root {
    color-scheme: light dark; /* enable light and dark mode compatibility */
    supported-color-schemes: light dark; /* enable light and dark mode */
}

You can use color-schemi or supported-color-schemes, the latter is newer and should be supported by web browsers. If you’re using a WKWebView, supported-color-schemes works.
By adding this, you’re telling the web view you support dark mode. Try to switch it, you’ll see the background color change from white to black, and the font from black to white and viceversa.
That’s great, but you can have more control with a media query


/* media query for dark mode */
@media (prefers-color-scheme: dark) {
    h1 {
        color:pink;
    }
}

In this example, I want h1 to have a pink color only in dark mode. You can have different images for dark mode as well with this media query, or you can use the picture tag as follows:

<picture>
    <source srcset="night.jpg" media="(prefers-color-scheme: dark)">
    <img src="day.jpg">
</picture>

So a different image is displayed in dark mode.

That’s all for now, I encourage your to support dynamic font even for your web content, at least if you’re in control of it.
Happy coding 🙂