Refactoring to TipKit from AppStorage and Custom Views in SwiftUI
This post is brought to you by Emerge Tools, the best way to build on mobile.
In my latest update to Elite Hoops, I replaced several homegrown solutions with Apple’s nascent iOS 17 equivalents. The most prominent example of this is TipKit.
To prompt coaches about a functional user experience feature in Elite Hoops, I had created my own view that worked like this:
It works, but I was not particularly fond of how it looked, or the code I had to maintain to make it work. Any interface code, relying on user path logic combined with caching flags, has a unique propensity to naturally become voluminous — making them less than fun to maintain years later. Let alone the first time — but adding more and more logic to it? Nah, I’d rather not mess with it.
All that to say, when I have that feeling about some code — I always replace it with a baked-in Apple API if one is available.
Thus, TipKit. Here is what it looked like after replacing my own stuff with it:
let concatenatedThoughts = """
Plus, plugging in `.blurReplace()` makes it so fun.
"""
In addition to being a familiar user experience to most people (Apple has been using TipKit-esque tips for years), it also makes the code easier to reason about.
Skip the Extra Caching Flag
When using my old method, I had to manually cache the flag to hide or show it:
final class AppSettings: ObservableObject {
@AppStorage("hasSeenPassingTipsChanges") var hasSeenPassingTipsChanges: Bool = false
}
And then later, in the view:
CourtView()
.onFirstAppear {
0.33.delayThen {
if appSettings.hasSeenWelcomeToTeamsExplainer && !appSettings.hasSeenPassingTipsChanges {
appSettings.hasSeenPassingTipsChanges = true
showTipsPopover.toggle()
}
}
}
With TipKit, it all becomes a little more intuitive. I can skip the manual caching layer, because TipKit inherently uses its own. That also helps me worry about one less thing and it keeps the actual tip logic housed within the tip code itself:
import TipKit
struct PassingTip: Tip {
@Parameter
static var showPassingTip: Bool = false
var rules: [Rule] {
[
#Rule(Self.$showPassingTip) {
$0 == true
}
]
}
var title: Text {
Text("Tip: Easily pass to players")
}
var message: Text? {
Text("· Tap twice on a player to pass.\n· Tap and hold on a player for more.")
}
var image: Image? {
Image(systemName: "lightbulb.circle.fill")
}
}
Which means the original calling site doesn’t even really have to change much:
CourtView()
.onFirstAppear {
0.33.delayThen {
if appSettings.hasSeenWelcomeToTeamsExplainer && !PassingTip.showPassingTip {
PassingTip.showPassingTip.toggle()
}
}
}
Further, they are easier to test without changing the call site (before, I needed to change the call site or nuke UserDefaults
, which had a trickle down effect on a bunch of other things I wasn’t testing):
// In EliteHoopsApp
init() {
try? Tips.resetDatastore()
try? Tips.configure()
}
Aggregation
While moving over a few other tips, I was pleasantly suprised to see how trivial it was to aggregate tip logic. That is, TipA
may have a flag that TipB
needs to know about in its conditions to present.
In the same view, I show another tip about an easier way to undo things. But, it shouldn’t present if the previous tip hasn’t shown yet. Sticking all of this into the rules
array of the other tip makes it all easy peezy:
struct UndoTip: Tip {
@Parameter
static var showUndoRedoTip: Bool = false
var rules: [Rule] {
[
#Rule(Self.$showUndoRedoTip) {
$0 == true
},
// References the previous tip above
#Rule(PassingTip.$showPassingTip) {
$0 == true
}
]
}
var title: Text {
Text("Tip: Easily undo")
}
var message: Text? {
Text("· Tap twice with two fingers to undo.")
}
var image: Image? {
Image(systemName: "arrow.uturn.backward.circle.fill")
}
}
In the end, I made things look a little nicer, removed code I didn’t need to maintain and rested easier knowing I can let Apple handle Tip-related UI code going forward. Typically, I find frameworks from an API design standpoint to either be easy to use but rigid, or they are hard to get started with but extremely flexible.
TipKit is easy to use and it’s flexible. That…that’s the good stuff.
Until next time ✌️.