{"id":1199,"date":"2026-09-10T00:46:43","date_gmt":"2026-09-10T00:46:43","guid":{"rendered":"https:\/\/x.sheep-mine.ts.net\/index.php\/tipkit-for-the-real-world\/"},"modified":"2026-09-10T00:46:43","modified_gmt":"2026-09-10T00:46:43","slug":"tipkit-for-the-real-world","status":"publish","type":"post","link":"https:\/\/x.sheep-mine.ts.net\/index.php\/tipkit-for-the-real-world\/","title":{"rendered":"TipKit for the Real World \u2022 furbo.org"},"content":{"rendered":"<p><br \/>\n<\/p>\n<div>\n<p>On a Monday morning, I thought I\u2019d take a crack at implementing tips in one of my apps. There were some gestures and other features that customers weren\u2019t finding and explaining those things with tips would be helpful. I expected the task to take about a day.<\/p>\n<p>It ended up taking a week.<\/p>\n<p>Why so long?<\/p>\n<p>Well, the first problem is when you watch the <a rel=\"nofollow\" target=\"_blank\" href=\"https:\/\/developer.apple.com\/videos\/play\/wwdc2023\/10229\/\">introduction video from WWDC \u201923<\/a> you\u2019ll see outdated code throughout the presentation. TipsCenter doesn\u2019t exist. Configuration is completely different. And there is no mention of UIKit.<\/p>\n<p>Then, when you look at <a rel=\"nofollow\" target=\"_blank\" href=\"https:\/\/developer.apple.com\/documentation\/tipkit\/highlightingappfeatureswithtipkit\">the sample code<\/a>, it\u2019s all SwiftUI. At least the sample compiles and runs because it uses a completely different syntax than what was shown at WWDC.<\/p>\n<p>But the fact remained: I had UIKit views where I wanted to display tips. And there wasn\u2019t any information on how to accomplish that. Did Apple really release a framework that didn\u2019t work on the code we\u2019ve been crafting for the past 20 years?<\/p>\n<p>Tips are something you want to add to existing code. And UIKit is the most likely case there: you\u2019re 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.<\/p>\n<p>Eventually I stumbled upon <a rel=\"nofollow\" target=\"_blank\" href=\"https:\/\/developer.apple.com\/documentation\/tipkit\/tipuipopoverviewcontroller\">TipUIPopoverViewController <\/a>which inherits from UIViewController. That should work!<\/p>\n<p>And it did.<\/p>\n<p>But it didn\u2019t: <a rel=\"nofollow\" target=\"_blank\" href=\"https:\/\/github.com\/chockenberry\/Tipster\/blob\/748b149db9823720be270bd0f4b3ebfd0b9d1b42\/Tipster\/PresentedViewController.swift#L100\">the close button on the popover didn\u2019t work<\/a>. And my first day was over.<\/p>\n<p>This is the point where you start to learn that a Tip is a dynamic state machine that\u2019s mostly out of your control. My problem was understanding how that state machine interacted with my own code (and its state).<\/p>\n<p>Eventually, you also learn that the Tip\u2019s state is persisted. Until you realize that any changes you make are <a rel=\"nofollow\" target=\"_blank\" href=\"https:\/\/fatbobman.com\/en\/posts\/mastering-tipkit-advance\/#finding-answers-from-tipkits-persistent-data\">stored in a SQLite database<\/a>, debugging is very confusing. It\u2019s also easy for your own state to get out-of-sync with the Tip state: there is not a single source of truth.<\/p>\n<p>Your first task will be to figure out how to get the Tip close buttons to work. The sample code for <a rel=\"nofollow\" target=\"_blank\" href=\"https:\/\/developer.apple.com\/documentation\/tipkit\/tipuipopoverviewcontroller\">TipUIPopoverViewController<\/a> 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\u2019s your job to both present and dismiss the popover view controller.<\/p>\n<p>Unfortunately, that sample code doesn\u2019t scale well. If you have a view with multiple tips, you\u2019re going to be littering your code with tip instances, observation tasks, and popover controllers. It\u2019s a mess and a clear sign that the TipKit developers didn\u2019t think much about UIKit.<\/p>\n<p>My solution is a <a rel=\"nofollow\" target=\"_blank\" href=\"https:\/\/github.com\/chockenberry\/Tipster\/blob\/748b149db9823720be270bd0f4b3ebfd0b9d1b42\/Tipster\/TipKitHelper.swift#L98\">TipPresenter<\/a> class. It\u2019s instantiated by your view controller, starts an observation task, and then presents or dismisses a popover as the Tip\u2019s state changes. It significantly reduces the clutter, makes <a rel=\"nofollow\" target=\"_blank\" href=\"https:\/\/github.com\/chockenberry\/Tipster\/blob\/639fd1a967767ec00617adeb677f335903089a96\/Tipster\/PresentedViewController.swift#L10\">refactoring views\/tips easier<\/a>, and it even works from Objective-C code (yes, some of us still have that).<\/p>\n<p>One thing to keep in mind when you\u2019re using TipPresenter: the observation task is a strong reference with weak references to a view and controller. Make sure you <a rel=\"nofollow\" target=\"_blank\" href=\"https:\/\/github.com\/chockenberry\/Tipster\/blob\/748b149db9823720be270bd0f4b3ebfd0b9d1b42\/Tipster\/PresentedViewController.swift#L48\">call stop() as you clean up your view<\/a>: if you\u2019ve ever used a notification observer, you\u2019ll know the pattern and why you need it \ud83d\ude42<\/p>\n<p>Another pattern emerged once I had a convenient way to present tips: view updates and tip updates go hand in hand.<\/p>\n<p>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 <a rel=\"nofollow\" target=\"_blank\" href=\"https:\/\/github.com\/chockenberry\/Tipster\/blob\/748b149db9823720be270bd0f4b3ebfd0b9d1b42\/Tipster\/PresentedViewController.swift#L80\">updateView()<\/a>.<\/p>\n<p>You\u2019ll quickly find that your model changes will need something that moves state to the tip presenter. Whenever updateView() gets called, you\u2019ll also call <a rel=\"nofollow\" target=\"_blank\" href=\"https:\/\/github.com\/chockenberry\/Tipster\/blob\/748b149db9823720be270bd0f4b3ebfd0b9d1b42\/Tipster\/PresentedViewController.swift#L87\">updateTips()<\/a>.<\/p>\n<p>I\u2019ve been referencing a <a rel=\"nofollow\" target=\"_blank\" href=\"https:\/\/github.com\/chockenberry\/Tipster\/tree\/main\">Tipster project repository<\/a> throughout this post. Feel free to download and experiment. If your UIViewControllers are written in Swift, check out <a rel=\"nofollow\" target=\"_blank\" href=\"https:\/\/github.com\/chockenberry\/Tipster\/blob\/main\/Tipster\/PresentedViewController.swift\">PresentedViewController<\/a>. If you\u2019re working with Objective-C, take a look at <a rel=\"nofollow\" target=\"_blank\" href=\"https:\/\/github.com\/chockenberry\/Tipster\/blob\/main\/Tipster\/LegacyViewController.m\">LegacyViewController<\/a>. The <a rel=\"nofollow\" target=\"_blank\" href=\"https:\/\/github.com\/chockenberry\/Tipster\/blob\/748b149db9823720be270bd0f4b3ebfd0b9d1b42\/Tipster\/TipKitHelper.swift#L213\">ToggleTipPresenter<\/a> is used in both view controllers.<\/p>\n<p>The <a rel=\"nofollow\" target=\"_blank\" href=\"https:\/\/github.com\/chockenberry\/Tipster\/blob\/main\/Tipster\/TipKitHelper.swift\">TipKitHelper<\/a> 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.<\/p>\n<p>I\u2019m not thrilled with the need to have separate implementations of the TipPresenter classes, but since Tip is a struct that can\u2019t be bridged to Objective-C, it has to be that way. <\/p>\n<p>To quickly find important stuff in the project, search for <code>NOTE:<\/code>. There is also a lot of <a rel=\"nofollow\" target=\"_blank\" href=\"https:\/\/github.com\/chockenberry\/Tipster\/blob\/639fd1a967767ec00617adeb677f335903089a96\/Tipster\/Debug.swift#L28\">debugLog()<\/a> in the code to help you see what\u2019s going on from the Xcode console.<\/p>\n<p>As you get more familiar with TipKit, I highly recommend <a rel=\"nofollow\" target=\"_blank\" href=\"https:\/\/fatbobman.com\/en\/posts\/mastering-tipkit-advance\/\">this article<\/a> on Fatbobman\u2019s Blog \u2013 it digs into the more advanced aspects of TipKit and includes an example of how to do an inline tip with UIView or NSView.<\/p>\n<p>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!<\/p>\n<\/p><\/div>\n<p><br \/>\n<br \/><a href=\"https:\/\/furbo.org\/2026\/08\/31\/tipkit-for-the-real-world\/\">Source link <\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>On a Monday morning, I thought I\u2019d take a crack at implementing tips in one&#8230;<\/p>\n","protected":false},"author":1,"featured_media":1200,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[997],"tags":[2351,2349,2350,836,2354,2357,2353,2360,2359,2358,2362,2355,2352,2356,2361],"class_list":["post-1199","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-development","tag-app","tag-chock","tag-design","tag-development","tag-experience","tag-iconfactory","tag-interface","tag-ios","tag-mac","tag-mobile","tag-software","tag-ui","tag-user","tag-ux","tag-web"],"_links":{"self":[{"href":"https:\/\/x.sheep-mine.ts.net\/index.php\/wp-json\/wp\/v2\/posts\/1199","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/x.sheep-mine.ts.net\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/x.sheep-mine.ts.net\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/x.sheep-mine.ts.net\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/x.sheep-mine.ts.net\/index.php\/wp-json\/wp\/v2\/comments?post=1199"}],"version-history":[{"count":0,"href":"https:\/\/x.sheep-mine.ts.net\/index.php\/wp-json\/wp\/v2\/posts\/1199\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/x.sheep-mine.ts.net\/index.php\/wp-json\/wp\/v2\/media\/1200"}],"wp:attachment":[{"href":"https:\/\/x.sheep-mine.ts.net\/index.php\/wp-json\/wp\/v2\/media?parent=1199"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/x.sheep-mine.ts.net\/index.php\/wp-json\/wp\/v2\/categories?post=1199"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/x.sheep-mine.ts.net\/index.php\/wp-json\/wp\/v2\/tags?post=1199"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}