On a Monday morning, I thought I’d take a crack at implementing tips in one of my apps. There were some gestures and other features that customers weren’t finding and explaining those things with tips would be helpful. I expected the task to take about a day.
It ended up taking a week.
Why so long?
Well, the first problem is when you watch the introduction video from WWDC ’23 you’ll see outdated code throughout the presentation. TipsCenter doesn’t exist. Configuration is completely different. And there is no mention of UIKit.
Then, when you look at the sample code, it’s all SwiftUI. At least the sample compiles and runs because it uses a completely different syntax than what was shown at WWDC.
But the fact remained: I had UIKit views where I wanted to display tips. And there wasn’t any information on how to accomplish that. Did Apple really release a framework that didn’t work on the code we’ve been crafting for the past 20 years?
Tips are something you want to add to existing code. And UIKit is the most likely case there: you’re not going to rewrite your views in SwiftUI just to explain some features. The biggest failure with the TipKit introduction is that it ignored the past and how apps have historically been built.
Eventually I stumbled upon TipUIPopoverViewController which inherits from UIViewController. That should work!
And it did.
But it didn’t: the close button on the popover didn’t work. And my first day was over.
This is the point where you start to learn that a Tip is a dynamic state machine that’s mostly out of your control. My problem was understanding how that state machine interacted with my own code (and its state).
Eventually, you also learn that the Tip’s state is persisted. Until you realize that any changes you make are stored in a SQLite database, debugging is very confusing. It’s also easy for your own state to get out-of-sync with the Tip state: there is not a single source of truth.
Your first task will be to figure out how to get the Tip close buttons to work. The sample code for TipUIPopoverViewController hints at what you need to do: implement an asynchronous task that monitors the state of the tip. When that tip gets into the right state, it’s your job to both present and dismiss the popover view controller.
Unfortunately, that sample code doesn’t scale well. If you have a view with multiple tips, you’re going to be littering your code with tip instances, observation tasks, and popover controllers. It’s a mess and a clear sign that the TipKit developers didn’t think much about UIKit.
My solution is a TipPresenter class. It’s instantiated by your view controller, starts an observation task, and then presents or dismisses a popover as the Tip’s state changes. It significantly reduces the clutter, makes refactoring views/tips easier, and it even works from Objective-C code (yes, some of us still have that).
One thing to keep in mind when you’re using TipPresenter: the observation task is a strong reference with weak references to a view and controller. Make sure you call stop() as you clean up your view: if you’ve ever used a notification observer, you’ll know the pattern and why you need it :-)
Another pattern emerged once I had a convenient way to present tips: view updates and tip updates go hand in hand.
Most of our UIKit apps rely on a Model-View-Controller architecture. The model gets updated, changes propagate to the controller, which uses a view to display the new information. Every UIViewController has something like an updateView().
You’ll quickly find that your model changes will need something that moves state to the tip presenter. Whenever updateView() gets called, you’ll also call updateTips().
I’ve been referencing a Tipster project repository throughout this post. Feel free to download and experiment. If your UIViewControllers are written in Swift, check out PresentedViewController. If you’re working with Objective-C, take a look at LegacyViewController. The ToggleTipPresenter is used in both view controllers.
The TipKitHelper file contains the Tip definitions, the TipPresenters, and the configuration helper class (which also has Objective-C members). Also of note: this code is compatible with iOS 17 and later.
I’m not thrilled with the need to have separate implementations of the TipPresenter classes, but since Tip is a struct that can’t be bridged to Objective-C, it has to be that way.
To quickly find important stuff in the project, search for NOTE:. There is also a lot of debugLog() in the code to help you see what’s going on from the Xcode console.
As you get more familiar with TipKit, I highly recommend this article on Fatbobman’s Blog – it digs into the more advanced aspects of TipKit and includes an example of how to do an inline tip with UIView or NSView.
Armed with this code and information, adding TipKit to your UIKit app should take less than a week. Maybe even just a single day like I initially thought!