Swift by Northwest Plans

July 30, 2017

I’m excited to be speaking at Swift by Northwest in October in sunny Seattle. I just put the finishing touches on the abstract for my session:

These are a Few of My Stateful Machines

As app developers we have to deal with the challenges of stateful code every day. State machines are a powerful technique for managing state — but they’re underused in Mac and iOS development, in part because they can be awkward to implement in Objective-C. Swift changes that. Swift’s smart enums make implementing state machines quick, easy, and fun. In this talk we’ll look at how you can make your apps easier to write, debug, and evolve using state machines in Swift. Along the way, we’ll explore the power of enums, including associated values, methods, and properties.

Swift’s destructuring switch statements over smart enums are one of my favorite parts of the language. This session should be great fun to lead. I’ll do my best to make it fun to participate in as well.

Swift by Northwest is shaping up to be a great conference. Some of my favorite people in the Cocoa community are presenting, and Seattle is my favorite city. Early bird registration is open now. I hope you’ll be there!


Version 1.2 of Export OmniFocus View to OmniOutliner

June 25, 2017

Now available, version 1.2 of my Export View to OmniOutliner script. The script makes a new OmniOutliner document from your current view in OmniFocus.

With Version 1.2 you can now export from the Forecast perspective in OmniFocus. Due to a limitation in OmniFocus, the export from Forecast will have empty group titles in OmniOutliner. The tasks are exported correctly, however. (I hope to fix this limitation in a future release of OmniFocus.)

This version of the export script also better handles notes with embedded links.

Installing the Script

To install the script, download the latest version here. Then in OmniFocus, choose Help → Open Scripts Folder. Drag the Export View to OmniOutliner file into the scripts folder. Finally, use Customize Toolbar to add the script to the toolbar in OmniFocus.

Running the Script

Once you’ve installed the script, navigate to whatever perspective you’d like to export. You can even Focus on a particular project, or use View Options (⇧⌘V) to fine tune the display. Once the view in OmniFocus is showing just what you want, run Export View to OmniOutliner from your OmniFocus toolbar. OmniOutliner will launch and the script will create a new document containing just the information in your current OmniFocus view.

Only the titles, notes, and structure are exported from OmniFocus. The OmniOutliner document won’t contain contexts or other information, like defer and due dates, from OmniFocus. I find this is a handy way to get a summary of a project or a task list into OmniOutliner. From there, I can print to PDF to share with others without exposing the details of my own OmniFocus database.

Share and enjoy!

Drop me a note @curtclifton on Twitter if there’s something else you’d like to see automated in OmniFocus.


Appearance Manager

May 6, 2017

In my previous posts on fonts we looked at:

Today I want to share a utility class, AppearanceManager, that works with my UIFont extension and iOS’ appearance proxies to manage all the fonts in an app.

After Font with Style we had a system where we could update the font of a UI element with a call like this:

titleLabel.font = titleLabel.font.appFontWithSameStyle()

Unfortunately, we had to manually call that code for every UI element in every view. This also meant that we needed outlets for every UI element, even if we didn’t do anything else with it. Nearly as bad, every view controller had to override traitCollectionDidChange to update the fonts again in response to the user changing their system preferred content size.

AppearanceManager and a few small extensions on UIKit classes solve all three of these problems. I’ll discuss them in-line in this post; the final code is available here.

UIFont Tweaks

First I extended UIFont with a few more convenience methods:

extension UIFont {
    

    var smallCapsVariant: UIFont {
        let smallCapsFontDescriptor = fontDescriptor.addingAttributes(
            [
                UIFontDescriptorFeatureSettingsAttribute: [
                    [UIFontFeatureTypeIdentifierKey: kLowerCaseType, UIFontFeatureSelectorIdentifierKey: kLowerCaseSmallCapsSelector],
                ]
            ]
        )

        let smallCapsFont = UIFont(descriptor: smallCapsFontDescriptor, size: 0)
        return smallCapsFont
    }

    class func appFont(ofSize size: CGFloat) -> UIFont {
        let font = UIFont(name: "MuseoSlab-500", size: size)
        return font ?? systemFont(ofSize: size)
    }

    class var navigationItemFont: UIFont {
        return appFont(ofSize: 17)
    }

    class var navigationBarTitleFont: UIFont {
        return UIFont.appFont(for: .headline)
    }

    class var buttonFont: UIFont {
        return appFont(ofSize: 17)
    }
}

AppearanceManagerUpdatable Protocol

Then I wrote a bunch of little extensions on UIKit classes. The idea is to add an API to UIViewController and UIView to allow recursively walking the entire view hierarchy of an app, updating fonts as we go.

This API has two methods, collected in a protocol:

protocol AppearanceManagerUpdatable {
    /// Perform a local update of the fonts in this object.
    func updateFontsForContentSizeChange()

    /// Recursively update the fonts in the children of this object.
    func recursivelyUpdateFontsForContentSize()
}

For UIViews, we want to update actual fonts. For UIViewControllers, we want to bounce the messages to the view hierarchy.

Extending UIViews

Let’s look at the UIView extensions first, since they’re easier to understand. The default extension does nothing for fonts, since not every UIView has a font, but it handles the recursion:

extension UIView: AppearanceManagerUpdatable {
    /// Default implementation does nothing.
    ///
    /// Subclasses with font attributes should override to update those fonts.
    func updateFontsForContentSizeChange() {
        // no-op
    }

    /// Calls `recursivelyUpdateFontsForContentSize()` recursively on all subviews, then calls `updateFontsForContentSizeChange()` on self.
    func recursivelyUpdateFontsForContentSize() {
        for view in subviews {
            view.recursivelyUpdateFontsForContentSize()
        }
        updateFontsForContentSizeChange()
    }
}

Next we extend the basic font-bearing UI elements to update their fonts:

extension UILabel {
    override func updateFontsForContentSizeChange() {
        font = font.appFontWithSameStyle()
    }
}

extension UITextField {
    override func updateFontsForContentSizeChange() {
        guard let currentFont = font else {
            return
        }
        font = currentFont.appFontWithSameStyle()
    }
}

extension UITextView {
    override func updateFontsForContentSizeChange() {
        guard let currentFont = font else {
            return
        }
        font = currentFont.appFontWithSameStyle()
    }
}

This brute-force updating makes some UIKit views unhappy. For example, UITabBar embeds private UILabels. You end up with a mess if you change them. So, this extension on UITabBar keeps our recursion from mucking with it:

extension UITabBar {
    override func recursivelyUpdateFontsForContentSize() {
        // Leave tab bar untouched. It doesn't like having subviews twiddled.
    }
}

In my app, I wanted to update the navigation bar title and button fonts specially, so added extensions for them as well:

extension UINavigationBar {
    override func updateFontsForContentSizeChange() {
        titleTextAttributes = [NSFontAttributeName: UIFont.navigationBarTitleFont]
    }

    override func recursivelyUpdateFontsForContentSize() {
        // Leave navigation bar subviews untouched. It doesn't like having subviews twiddled. However, still update self.
        updateFontsForContentSizeChange()
    }
}

extension UIButton {
    override func updateFontsForContentSizeChange() {
        titleLabel?.font = titleLabel!.font.smallCapsVariant
    }

    override func recursivelyUpdateFontsForContentSize() {
        // Leave button subviews untouched. However, still update self.
        updateFontsForContentSizeChange()
    }
}

You might need to tweak these extensions, or add others, if your app uses different views. That’s the primary reason I haven’t tried to make a micro-framework from this technique. Extending it to cover every app would obscure the ideas.

Extending UIViewControllers

Now that we have our views ready to update, let’s extend our UIViewControllers to play along. First, UIViewController itself gets an extension to hand off updates to its view and child view controllers:

extension UIViewController: AppearanceManagerUpdatable {
    /// Default implementation calls `updateFontsForContentSizeChange()` on every view in the view hierarchy rooted at `self.viewIfLoaded`.
    func updateFontsForContentSizeChange() {
        viewIfLoaded?.recursivelyUpdateFontsForContentSize()
    }

    /// Calls `recursivelyUpdateFontsForContentSize()` recursively on all child view controllers, then calls `updateFontsForContentSizeChange()` on self.
    func recursivelyUpdateFontsForContentSize() {
        for viewController in childViewControllers {
            viewController.recursivelyUpdateFontsForContentSize()
        }
        updateFontsForContentSizeChange()
    }
}

Then collection views and table views swing a big hammer to update their views. We just have them do a full reloadData. This seems OK to me, since a user is unlikely to change their preferred content size frequently.

extension UITableViewController {
    override func updateFontsForContentSizeChange() {
        tableView.reloadData()
    }
}

extension UICollectionViewController {
    override func updateFontsForContentSizeChange() {
        collectionView?.reloadData()
    }
}

Extending Custom Classes

Having these hooks on views and view controllers is handy for custom layout. Most of my views just update automatically using this code and autolayout, but in one of my table view cells I want to update a constraint based on font size. I can do that by overriding updateFontsForContentSizeChange like so:

override func updateFontsForContentSizeChange() {
    super.updateFontsForContentSizeChange()

    guard let bodyFont = body.font else {
        assertionFailure("body.font not set")
        return
    }

    let position = bodyFont.capHeight / 2
    completionButtonCenteringConstraint.constant = round(position)
    containerView.setNeedsLayout()
}

Wiring It All Together

The AppearanceManager class triggers the automatic update process. It’s a concise 20 lines of code. The appearance manager holds a reference to the app’s root view controller, watches for content size notifications, and sends font update messages down the view controller hiearchy.

/// Controls app-wide appearance.
///
/// Responsible for updating appearance proxies and passing font update messages down the view controller and view hierarchies when the system font size changes.
class AppearanceManager: NSObject {
    private weak var rootViewController: UIViewController?

    init(rootViewController: UIViewController) {
        self.rootViewController = rootViewController

        super.init()
        UIBarButtonItem.appearance().setTitleTextAttributes([NSFontAttributeName: UIFont.navigationItemFont], for: [.normal])
        UINavigationBar.appearance().titleTextAttributes = [NSFontAttributeName: UIFont.navigationBarTitleFont]

        NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange(_:)), name: .UIContentSizeCategoryDidChange, object: nil)
    }

    dynamic private func contentSizeCategoryDidChange(_ notification: Notification) {
        rootViewController?.recursivelyUpdateFontsForContentSize()
    }
}

My app delegate’s application(_:, willFinishLaunchingWithOptions:) method loads the app’s custom fonts and instantiates an AppearanceManager instance:

UIFont.loadAppFonts()
appearanceManager = AppearanceManager(rootViewController: window.rootViewController!)

In each of my UIViewController subclasses, I add a single line to the viewDidLoad() method:

override func viewDidLoad() {
    
    recursivelyUpdateFontsForContentSize()
}

Finally, for any custom views, such as table view or collection view cells, I update fonts on load and reuse:

override func awakeFromNib() {
            
    recursivelyUpdateFontsForContentSize()
}

override func prepareForReuse() {
    
    recursivelyUpdateFontsForContentSize()
}

We could use swizzling to inject these last three calls, but that seems a step too clever to me.

Next Steps

With just under 300 lines of total code, we can add custom fonts to an app. Although we developers like to complain, the conciseness of this is really a testament to the power and elegance of UIKit.

I’ve collected all the posts in this series, along with the final code and steps for adding the code to your app.

Share and enjoy.


Integrating Micro.blog

April 30, 2017

I’m excited about the soft launch of micro.blog. Manton Reece’s creation lets users easily own their short-form content. Instead of posting short items and photos to Twitter or Facebook, you can post them to your own website, or to you.micro.blog. Automatic cross-posting means that your audience can still follow you on Twitter and, soon, Facebook, but you own all the posts so they can survive a Twitter-pocalypse or Facebook-mageddon.

I’m a strong believer in controlling my own content, even if I share it freely. Facebook is at the whims of its advertizers and Zuck’s political ambitions. Who knows what drives Twitter’s corporate decision making. Having the source material on my own domains means I can keep them available as long as I choose to.

If you didn’t have the good fortune to back micro.blog on Kickstarter, you’ll have to wait for the system to open to everybody. It will be worth your wait.

I spent part of the day today integrating my micro.blog site into my main site. It was a fun exercise, and I thought it might be helpful to share the steps.

Validating My Site

The first step was validating my site with micro.blog. This proves to Manton’s servers that I own my site. This was straightforward following the instructions here.

  1. First, I added a link tag to the header of my site’s source:

    <head>
      <link href="https://micro.blog/curt" rel="me" /></head>
    
  2. Then I edited my account profile on micro.blog to point at my website:

    http://curtclifton.net
    

Adding My RSS Feed

I’m using a paid micro.blog-hosted account. For $5 a month, this gives me an always-on server that I can post to plus automatic cross-posting. My own site is statically generated. I host the site on Digital Ocean, but don’t run anything server-side besides nginx and a locked down ssh daemon. It was a no-brainer to let Manton manage the micro.blog for me.

My site generator produces a full-content RSS feed. Micro.blog lets me pull content — titles and links — from that feed into my micro.blog timeline.

Setting up a Custom Microblog Domain

With a paid account, my micro blog is available at curt.micro.blog. I wanted to host the content at my own domain. This way, if Manton’s project ends someday, I can keep my posts available at microblog.curtclifton.net.

This was an easy process:

  1. On my account page on micro.blog, I added microblog.curtclifton.net as my custom domain name.

    custom domain mapping

  2. At my domain name provider, Hover, I clicked on curtclifton.net on my account page.

  3. Then I clicked DNS

  4. Finally I clicked Add New, where I mapped a CNAME entry for microblog to point at pages.micro.blog.

    DNS settings

  5. After saving this change and waiting a few minutes for the DNS record to propagate, my microblog was available at the new URL.

Embedding Recent Posts

The last thing I did to integrate micro.blog with my regular site was to embed recent micro posts on my site. Manton provides a javascript to do this.

  1. First I added a link to the script in the sidebar of my posts template and in-line in a new microblog page:

    <script type="text/javascript" src="http://micro.blog/sidebar.js?username=your_username"></script>
    
  2. Next I took advantage of the div classes returned by the script to style the posts to blend in with the rest of my site. That involved adding the following to my site CSS:

    div.microblog_post {
        padding-bottom: 3ex;
    }
    
    div.microblog_text p {
        margin-bottom: 0px;
    }
    
    div.microblog_text img {
        margin-top: 0.5ex;
    }
    
    div.microblog_time {
        font-family: "Museo Sans", "museo-sans", Helvetica, sans-serif;
        font-weight: 300;
        font-size: 14px;
        color: #585858;
    
        padding-bottom: 1ex;
        border-bottom: 1px #8C221B solid;
        box-shadow: 0px 1px #ededed;
    }
    

Your Turn

I feel like micro.blog could be a big step forward for the open web. I hope you’ll sign up. And if you’re already on board, I hope you’ll set up a custom domain name for your short-form writing.


Fonts with Style

April 11, 2017

I really appreciate the preferred content size feature on iOS. This is the system setting that let’s you choose to make text smaller or larger than the default size. Apps that use Apple’s named font styles get properly sized variants of the system fonts. The API for getting a named font style is:

let font = UIFont.preferredFont(forTextStyle: .body)

Apple offers ten named styles, shown here with the default preferred content size:

All ten named preferred font styles at the preferred content size

This composite shows the same named styles at the smallest and largest preferred content sizes.

Named preferred font styles at the smallest and largest sizes

In Interface Builder, you can set the font for an object to one of the named styles using the Attributes Inspector.

Choosing a named font style with the Attributes Inspector in Interface Builder

In previous posts I showed how to embed custom fonts in your app. Suppose we want to use custom fonts but want the convenience of named font styles. Being good developers, we also want to do the right thing for our users by letting them choose their preferred content size.

I’ve been using a little extension on UIFont to meet all three objectives.

The Usual Approach

The usual way to convert between fonts in Cocoa apps is to get the fontDescriptor from a font, make a modified version of the descriptor, then construct a new font using UIFont(descriptor:, size:).

Let’s look at an example. First we get an instance of a font and print some information about it:

let font = UIFont(name: "HoeflerText-Regular", size: 17)!
print(font.fontDescriptor)
print(font)

// UICTFontDescriptor <0x600000088e80> = {
//     NSFontNameAttribute = "HoeflerText-Regular";
//     NSFontSizeAttribute = 17;
// }
// <UICTFont: 0x7fe7f16014c0> font-family: "HoeflerText-Regular";
// font-weight: normal; font-style: normal; font-size: 17.00pt

We can then make a new descriptor by changing the font family. This replaces the "HoeflerText-Regular" font name attribute with the "Helvetica" font family attribute:

let descriptor = font.fontDescriptor.withFamily("Helvetica")
print(descriptor)

// UICTFontDescriptor <0x600000086310> = {
//     NSFontFamilyAttribute = Helvetica;
//     NSFontSizeAttribute = 17;
// }

Finally, we can make a UIFont from that descriptor:

let newFont = UIFont(descriptor: descriptor, size: 0)
print(newFont.fontDescriptor)
print(newFont)

// UICTFontDescriptor <0x600000084fb0> = {
//     NSFontNameAttribute = Helvetica;
//     NSFontSizeAttribute = 17;
// }
// <UICTFont: 0x7fe7f1606960> font-family: "Helvetica"; font-weight:
// normal; font-style: normal; font-size: 17.00pt

Styles Collide

But look what happens with the same code but starting with a named font style. Instead of a font name or family, the font descriptor has a key "NSCTFontUIUsageAttribute" and value UICTFontTextStyleBody representing the named font style:

let font = UIFont.preferredFont(forTextStyle: .body)
print(font.fontDescriptor)

// UICTFontDescriptor <0x60800009c890> = {
//     NSCTFontUIUsageAttribute = UICTFontTextStyleBody;
//     NSFontSizeAttribute = 17;
// }

The font itself is from the private font family .SFUIText:

print(font)

// <UICTFont: 0x7f86f6c03be0> font-family: ".SFUIText"; font-weight:
// normal; font-style: normal; font-size: 17.00pt

Trying to change the family on the font descriptor gets us a descriptor that includes both the new family and the old named font style:

let descriptor = font.fontDescriptor.withFamily("Helvetica")
print(descriptor)

// UICTFontDescriptor <0x60800009ea00> = {
//     NSCTFontUIUsageAttribute = UICTFontTextStyleBody;
//     NSFontFamilyAttribute = Helvetica;
//     NSFontSizeAttribute = 17;
// }

Creating a font from the confused descriptor takes us right back where we started:

let newFont = UIFont(descriptor: descriptor, size: 0)
print(newFont.fontDescriptor)
print(newFont)

// UICTFontDescriptor <0x60800009c890> = {
//     NSCTFontUIUsageAttribute = UICTFontTextStyleBody;
//     NSFontSizeAttribute = 17;
// }
// <UICTFont: 0x7f86f6d04fc0> font-family: ".SFUIText"; font-weight:
// normal; font-style: normal; font-size: 17.00pt

Our Helvetica is nowhere to be found.

One Approach

The first approach that I attempted was to munge the font descriptor, like so:

  1. Take the descriptor that we retrieved from the original font,
  2. grab its fontAttributes dictionary,
  3. strip the "NSCTFontUIUsageAttribute" key, and
  4. make a new font descriptor from the results.

This would work once, but discards the named style information. Without that info, we lose the ability to update the font when the user changes their preferred content size.

A Better Way

If we’re using custom fonts, the key feature of named styles that we need to maintain is the mapping from named style to font size. As we saw above, the font descriptor for a named style only includes the style and font size. Features like weight, kerning, leading, and tracking are embedded in the private fonts.

What if we had a way to maintain the style information even when switching to a custom font? With that available, we could look up the correct font size even when the user changes their preferred content size.

Here’s an extension on UIFont that does just that. The key trick is using associated objects to piggyback style information on the font. Associated objects let us attach arbitrary data to an existing object. They can be expensive if attached to many objects and accessed frequently, but they’re a great tool when used carefully.

First we need an UnsafeRawPointer to use with the C-legacy associated object API.

private var baseStyleAssociatedObjectKey = UnsafeRawPointer(UnsafeMutableRawPointer.allocate(bytes: 1, alignedTo: 1))

Then we add a computed property to UIFont for getting and setting the font style.

extension UIFont {
    private var baseStyle: UIFontTextStyle? {

Our baseStyle property is really just a wormhole to carry the style data from one point in time to another in the lifetime of the app. Setting the property doesn’t change the font’s appearance. Our setter just needs to store the information for later retrieval.

First we check whether the font’s descriptor already includes the "NSCTFontUIUsageAttribute". This key string is bound to the global variable UIFontDescriptorTextStyleAttribute by UIKit. If the key is there, then we can use its value at retrieval time and avoid the expense of adding an associated object. If the key is not there, then we use the associate object API to associate the newValue passed to the setter with the font.

        set {
            if fontDescriptor.object(forKey: UIFontDescriptorTextStyleAttribute) is UIFontTextStyle {
                // ignore for system fonts
                return
            }

            objc_setAssociatedObject(self, baseStyleAssociatedObjectKey, newValue, .OBJC_ASSOCIATION_ASSIGN)
        }

The getter first checks to see if the font’s descriptor includes the "NSCTFontUIUsageAttribute" key. If the key isn’t there, then we check for an associated object on the font. If neither is there, we return nil.

        get {
            if let systemStyle = fontDescriptor.object(forKey: UIFontDescriptorTextStyleAttribute) as? UIFontTextStyle {
                return systemStyle
            }

            if let associatedStyle = objc_getAssociatedObject(self, baseStyleAssociatedObjectKey) as? UIFontTextStyle {
                return associatedStyle
            }

            return nil
        }
    }    

With that property in place, we can implement a pair of functions on UIFont to convert from system named font styles into our custom ones. The first function let’s us go from a named font set on an object in a nib or storyboard to a custom font. It looks up the current base style, asserts if it’s missing, then calls a new class function on UIFont passing the baseStyle, or .body as a fallback.

    /// Returns the app-specific font with the same UIFontTextStyle as this, but sized to the current content size category.
    func appFontWithSameStyle() -> UIFont {
        assert(baseStyle != nil)
        return UIFont.appFont(for: baseStyle ?? .body)
    }

The new class function maps from named font styles to custom fonts, but uses the named style to look up the correct font size. The size will vary with named style and the user’s preferred content size.

    class func appFont(for style: UIFontTextStyle) -> UIFont {
        let preferredFont = UIFont.preferredFont(forTextStyle: style)
        let pointSize = preferredFont.pointSize        

Once we have the font size, we can switch on the named style to choose the font we want to use in our app. We fallback on the system preferred font if the font instantiation fails.

        let font: UIFont?

        switch style {
        case UIFontTextStyle.title1:
            font = UIFont(name: "MuseoSans-700", size: pointSize)
        case UIFontTextStyle.title2:
            font = UIFont(name: "MuseoSans-500", size: pointSize)
        case UIFontTextStyle.title3:
            font = UIFont(name: "MuseoSans-500", size: pointSize)
        case UIFontTextStyle.headline:
            font = UIFont(name: "MuseoSans-700", size: pointSize)
        case UIFontTextStyle.subheadline:
            font = UIFont(name: "MuseoSans-300", size: pointSize)
        case UIFontTextStyle.body:
            font = UIFont(name: "MuseoSlab-500", size: pointSize)
        case UIFontTextStyle.callout:
            font = UIFont(name: "MuseoSlab-500", size: pointSize)
        case UIFontTextStyle.caption1:
            font = UIFont(name: "MuseoSans-300", size: pointSize)
        case UIFontTextStyle.caption2:
            font = UIFont(name: "MuseoSans-300", size: pointSize)
        case UIFontTextStyle.footnote:
            font = UIFont(name: "MuseoSlab-300", size: pointSize)
        default:
            // Other font styles are undocumented. We could use the system preferred font for the style or force our body font. The latter precludes us from using the System Font choice in Interface Builder, so let's go with the former.
            font = preferredFont
        }

        assert(font != nil, "Failed to load app font for style “\(style)”")

        let actualFont = font ?? preferredFont // fall-back to preferred font if necessary

Once we have the new font, we use our baseStyle property to pass the style information along.

        actualFont.baseStyle = style // carry the base style forward
        return actualFont
    }    
}

With this in place, we can implement viewDidLoad() in our view controllers to replace system named fonts with our own:

override func viewDidLoad() {
    super.viewDidLoad()

    titleLabel.font = titleLabel.font.appFontWithSameStyle()
    // … repeat with all UI elements
}

We can update when the user changes their preferred content size by overriding traitCollectionDidChange(_:).

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection) {
    super.traitCollectionDidChange(previousTraitCollection)

    titleLabel.font = titleLabel.font.appFontWithSameStyle()
    // … repeat with all UI element
}

In viewDidLoad the fonts will start as those set in the nib or storyboard but will end as our custom fonts. In traitCollectionDidChange the fonts will start as our custom ones and will end as our custom ones at the new preferred content size.

The font update code is exactly the same in both cases, so a simple refactoring gives us this result:

override func viewDidLoad() {
    super.viewDidLoad()
    
    updateFonts()
}

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection) {
    super.traitCollectionDidChange(previousTraitCollection)
    updateFonts()
}

private func updateFonts() {
    titleLabel.font = titleLabel.font.appFontWithSameStyle()
    // … repeat with all UI elements
}

In a subsequent post I’ll show how we can generalize updateFonts() so we don’t have to repeat that code in our view controllers or even enumerate all our UI elements.

Until then, I hope this is helpful and encourages you to add support for preferred content sizes in your apps. My old eyes will thank you, and someday soon yours will too.

(Thanks to Paul Goracke for inspiring me to dig into this with his Xcoders talk on dynamic type.)