Home / Blog / TipKit in the wild: A/B results from two feature-discovery scenarios

TipKit in the wild: A/B results from two feature-discovery scenarios

I skipped TipKit in the iOS 17 beta and picked it up seriously with 17.1, rolling it out in two projects. It’s a good tool for feature discovery, but not a “drop it in and forget” solution. Here’s what I learned from the A/B tests. Why TipKit I used to write my own tooltip system: […]

I skipped TipKit in the iOS 17 beta and picked it up seriously with 17.1, rolling it out in two projects. It’s a good tool for feature discovery, but not a “drop it in and forget” solution. Here’s what I learned from the A/B tests.

Why TipKit

I used to write my own tooltip system: onboarding UserDefaults flags, “show once” logic, display frequency, recording tutorial dismissals. 500 lines of code doing what TipKit now does. TipKit collapses that down to 30 lines. How it manages the underlying state is completely opaque.

Scenario one: pull-to-refresh discovery

A news app. Analytics showed 62% of users never used pull-to-refresh, they restarted the app manually to pick up new stories. A small TipKit hint:

struct PullToRefreshTip: Tip {
    var title: Text { Text("Pull down to refresh") }
    var message: Text? { Text("Drag the top of the list down to update your news.") }
    var image: Image? { Image(systemName: "arrow.down.circle") }
}

Configure TipKit on app launch:

try Tips.configure([
    .displayFrequency(.immediate),
    .datastoreLocation(.applicationDefault)
])

On the view:

struct NewsListView: View {
    private let refreshTip = PullToRefreshTip()
    var body: some View {
        List {
            TipView(refreshTip)
            ForEach(news) { item in ... }
        }
        .refreshable { await viewModel.reload() }
    }
}

Control: when does it show?

Use Rule. For example, show it to users who have opened the app at least three times:

struct PullToRefreshTip: Tip {
    @Parameter static var appOpenCount: Int = 0
    var rules: [Rule] {
        [
            #Rule(Self.$appOpenCount) { $0 >= 3 }
        ]
    }
}

// On app launch:
PullToRefreshTip.appOpenCount += 1

Without this rule the tip shows on the very first launch, before the user has even had a chance to explore the feature. That confuses new users. A 3-opens threshold works well as a default.

A/B results, scenario one

Six weeks of A/B testing. Control group: no TipKit. Test group: the pull-to-refresh tip on the third launch.

  • Pull-to-refresh usage went from 38% to 71% (+33 points absolute)
  • Feed refreshes per session went from 0.8 to 1.4
  • Tip dismiss rate: 24% (low)

Scenario two: tab discovery (the one that failed)

Same app, the fourth tab in the bottom bar was “Favorites”. Analytics: 78% of users never opened it. I added a popover tip on the tab:

struct FavoritesTabTip: Tip {
    var title: Text { Text("Your Favorites live here") }
}

TabView { ... }
    .popoverTip(favoritesTip, arrowEdge: .bottom)

A/B results, scenario two

  • Favorites tab usage went from 22% to 28% (+6 points)
  • Session length dropped 4%
  • Tip dismiss rate: 53% (very high)

Takeaway: users could already see the tab. If they hadn’t opened it, the reason wasn’t “they didn’t notice”, it was “they weren’t interested”. A tip does not fix that. I killed the A/B test after two weeks.

Event-based tips

A much stronger trigger: show the tip after the user performs a specific action. Example: after reading an article for 10+ seconds, show the hint about adding to favorites.

struct AddToFavoritesTip: Tip {
    @Parameter static var readDuration: Double = 0
    var rules: [Rule] {
        [#Rule(Self.$readDuration) { $0 >= 10.0 }]
    }
}

// In the article view:
.onAppear { startTimer() }
.onDisappear {
    AddToFavoritesTip.readDuration = totalSeconds
}

A tip shown in the right context performs much better. Favorite additions went from 8% to 19%.

Invalidation: don’t show it again

tip.invalidate(reason: .actionPerformed)

Once a user performs the action, invalidate the tip. If they’ve pulled to refresh, stop showing “pull down”. I wire an action onto the tip object and invalidate it on the action state.

Display frequency

Default .immediate shows immediately. .daily prevents another tip from appearing the same day. Serious warning: never let more than one tip be visible at once. Tip spam scatters the user fast.

Closing advice

TipKit is useful but resist the urge to attach a tip to every feature. A good tip shows when the user actually needs it, appears once, and dismisses quickly. A bad tip sits on screen permanently telling the user something they already know. Don’t ship tips to production without A/B testing them. Only the data tells you which ones actually helped.

Have a project on this topic?

Leave a brief summary — I’ll get back to you within 24 hours.

Get in touch