TipKit — Apple’s new framework to help discover features with ease.

Rakshith N
Swiftable
Published in
8 min readAug 7, 2023

We have all at some point designed custom overlays to act as onboarding screens or provide tips to the user. After years, Apple has released a framework, that makes this task so simple. Introducing TipKit, a new framework to display contextual tips to showcase features which may be less used or not discovered by the user.

TipKit is available for both SwiftUI and UIKit and is easily testable, however, it requires iOS 17 or above. Given it is still in its early stages, it might undergo changes going ahead. Nonetheless, it’s highly useful and does eliminate the need for third-party SDKs. It can cleverly be combined with data analytics to nudge the user to explore or use certain features which they have not in the past.

The main components of TipKit

There are just 2 essential things which you need to understand to build a basic tip, first is the Tip protocol and second the TipView.

Let’s quickly understand these two, starting with the Tip protocol.

Tip Protocol

The Tip protocol defines the properties the tip will need to configure its content. You can conform to this protocol to create custom tips. The basic requirement is that every tip must have a title, the other properties are optional. The following properties help generate the content for your tip.

  1. title: of type Text, the title of the Tip.
  2. message: of type Text, a short description message to provide further information.
  3. asset: of type Image, displays the selected image to the left of the title and message.
  4. id: of type String, this is the unique identifier for your tip, if not set defaults to the name of the type that conforms to the Tip protocol.
  5. actions: an array of type TipKit.Action. Use these to provide primary and secondary buttons to help users learn more or provide custom action on interaction with the tip.

We can initialise an Action using the following two initialisers:

public init(id: String? = nil, 
disabled: Bool = false,
perform handler: (@Sendable () -> Void)? = nil,
_ label: @escaping @Sendable () -> Text)

public init(id: String? = nil,
title: some StringProtocol,
disabled: Bool = false,
perform handler: (@Sendable () -> Void)? = nil)

The id for the action defaults to the index of the action if not specified. We can pass in a function to be called when the user interacts with the action using the perform parameter. We have two options for displaying the purpose of the action using either the label parameter which expects a Text type to be returned, else use the title parameter of type String.

Further, we also have options to decide the conditions based on which the tip will be displayed.

  1. rules: an array of type Tip.Rule. This can be used to lay out any rules that determine when the tip needs to be displayed.
  2. options: an array of type Tip.Option. Let’s us provide options for defining the behaviour of the tip.

TipView

The UI element that represents the inline tip. The initialiser for TipView takes an instance of the Tip protocol we discussed earlier, an Edge parameter, which is optional, for deciding the edge of the tip view that displays the arrow.

This is a directional arrow that points away from the tip.

Lastly, an action closure to execute when the user triggers a tip’s button, this can be used to obtain either the id or the index of the action that was performed. This is an optional parameter as well.

Setting up your Tip

With the basics covered, it’s now time to combine all that we learnt above into creating our first tip, Yay! This is how you go about setting up your tip.

  1. Create a struct conforming to the Tip protocol and define how the content is configured. In the code below I have created a simple tip.
  2. Create an instance of your tip, in the View where you wish to display the tip. (Marker 1)
  3. Create an instance of a TipView, passing in the custom tip we created in step 1. Provide the arrow edge and action handler if required (Marker 2).
  4. Doing all the above would not lead to the tip being displayed, the last step and most important one is to configure and load the tip using the configure method as described in Marker 3. This gets called at the app startup and configures the persistent state of the tip.
// Code for creating a custom tip
struct HomeScreenTips: Tip {

var title: Text {
Text("This is a Tip for homescreen")
}

var message: Text? {
Text("Provide a description for the tip here.")
}

var asset: Image? {
Image(systemName: "lightbulb")
}

var actions: [Action] = [Action(id: "1", title: "Primary Action", perform: {
print("primary action selected")
}),
Action(id: "2", title: "Secondary Action", perform: {
print("secondary action selected")
})]
}
struct ContentView: View {

// MARKER 1: Create an instance of the Tip
var homeScreenTip = HomeScreenTips()

var body: some View {
VStack {

// MARKER 2: Create the TipView passing in the homescreenTip
TipView(homeScreenTip, arrowEdge: .bottom) { action in
print("action was performed \(action.id)")
}
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()
.task {

// MARKER 3: Configure the tip
try? await Tips.configure {
DatastoreLocation(.applicationDefault)
DisplayFrequency(.immediate)
}
}
}
}

Let’s have a closer look at the configure function.

static func configure(@Tips.ConfigurationBuilder options: @escaping () -> some TipsConfiguration = { defaultConfiguration }) async throws

Notice that the options parameters accept types confirming to TipsConfiguration. There are two options available for the TipsConfiguration namely DatastoreLocation and DisplayFrequency. We will discuss these in much more detail further in the article. For now, provide the values as shown in the code above to the options closure of the configure method and hit run.

Congratulations!! You just created your first-ever tip using TipKit.

A good thing to note here is that you can display the tip in two ways. The first is a pop overview. As the name suggests, it allows for the tip to appear over the app’s UI. Following is a snippet of how to use it.

SomeView {
}
.popoverTip(<instance of your tip>, arrowEdge: .trailing)

The second is the inline view, which we discussed above, in which the app’s UI is adjusted to fit the tip temporarily so that no UI is blocked.

credits: apple.developer.com

Now back to discussing the different configuration options we have.

  1. DatastoreLocation — This describes the location used for persisting any tips and associated data. You don’t need to instantiate the store location, rather use the initialisation methods to control how the tips are persisted and loaded.

The following are the initialisers available.

public init(url: URL, shouldReset: Bool = false)

public init(_ location: DatastoreLocation, shouldReset: Bool = false)

public init(groupIdentifier: String,
directoryName: String? = nil,
shouldReset: Bool = false) throws

The shouldReset, if set to true will erase all data from the datastore. Resetting all tips present in the application.
Make use of the url init to provide the specific URL where you wish to persist the data store.
location, which is a predefined datastore location, the default value for which is ‘applicationDefault’ which would persist the datastore in the app’s support directory.
groupIdentifier, the name of the group whose shared directory we wish to obtain, use the optional directoryName to specify a directory within this group.

2. DisplayFrequency — Lets you control the frequency of your tips, with options such as immediate, which eliminates any restriction on the frequency. You could also use the hourly, daily, weekly and monthly values to display no more than one tip hourly, weekly and so on respectively.

In this section, we dig a bit deeper to gain better control of when our tip is displayed. The framework offers support to handle such cases rather than having separate logic to decide the conditions under which the tip needs to be displayed. Let us first understand the Rule in more detail.

Rule types

Broadly there are two types of rules, parameter-based rules. These are persistent and best suited for State and Boolean comparisons. The second is event-based rules which define an action that needs to be performed by the user after which the tip is made available to the user.

In the code below, I demonstrate the example provided by Apple and extending it to the custom tip we created before. We use Macros to define a rule to check if the user is logged in. If you wish to experiment with this on the beta version make sure to add the following script under the Other Flags section of the Build settings for your project.

-external-plugin-path
$(SYSTEM_DEVELOPER_DIR)/Platforms/iPhoneOS.platform/Developer/usr/lib/swift/host/plugins#$(SYSTEM_DEVELOPER_DIR)/Platforms/iPhoneOS.platform/Developer/usr/bin/swift-plugin-server

By adding the ‘isLoggedIn’ rule, we ensure that it gets displayed only after the isLoggedIn flag is set to true, this can be done based on any precondition that you define in your code.

struct HomeScreenTips1: Tip {

@Parameter
static var isLoggedIn: Bool = false

var title: Text {
Text("This is a Tip for homescreen")
}

var message: Text? {
Text("Provide a description for the tip here.")
}

var asset: Image? {
Image(systemName: "lightbulb")
}

var actions: [Action] = [Action(id: "1", title: "Primary Action", perform: {
print("primary action selected")
}),
Action(id: "2", title: "Secondary Action", perform: {
print("primary action selected")
})]

//Rules for the tip
var rules: [Rule] {
#Rule(Self.$isLoggedIn) { $0 == true }
}
}

Event-based rule
Use this rule when you want to track an action that occurs one or more times in the app. Each event will have an associated id of type String, make sure these are unique to differentiate between various events. Use the donate() method of the event to increment the counter when the desired action takes place. As these values are persisted, it saves you the hassle of saving and updating these flags.

struct HomeScreenTips: Tip {

static let eventClickedOnHelpIcon = Event(id: "event_clicked_help_icon")

var title: Text {
Text("This is a Tip for homescreen")
}

var message: Text? {
Text("Provide a description for the tip here.")
}

var asset: Image? {
Image(systemName: "lightbulb")
}

var actions: [Action] = [Action(id: "1", title: "Primary Action", perform: {
print("primary action selected")
}),
Action(id: "2", title: "Secondary Action", perform: {
print("primary action selected")
})]

var rules: [Rule] {
//The tip becomes eligible after this event has occured twice.
#Rule(Self.eventClickedOnHelpIcon) { $0.donations.count > 2 }
}
}

The Tip protocol lets us further refine when the tip will be displayed by using options. In the example below we set the MaxDisplayCount for the tip as 2, thus avoiding displaying the tip to the user constantly. We also have IgnoresDisplayFrequency, which if set to true will override any previous setup performed for displaying tips during the configuration phase.

struct HomeScreenTips: Tips {
var title: Text {
Text("This is a Tip for homescreen")
}
...

var options: [TipOption] = [MaxDisplayCount(2)]
}

You can use the invalidate method on a tip to discontinue displaying the tip and pass the reason for invalidation, as show below.

<your_custom_tip>.invalidate(reason: .userPerformedAction)
//Other possible values for InvalidationReason: userClosedTip, maxDisplayCountExceeded

TipKit offers a status for each tip, which can be used to check if the tip is invalidated or active. You can also asynchronously monitor the status change of a tip using ‘statusUpdates’

Customising the look and feel of the tip

TipKit provides the following methods to further customise the look and feel of your tip. These are pretty much self explanatory so I will leave it here for this part.

public func tipAssetSize(_ size: CGSize) -> some View

public func tipCornerRadius(_ cornerRadius: Double,
antialiased: Bool = true) -> some View

public func tipBackground(_ style: some ShapeStyle) -> some View

Before you go…

A few last pointers to be aware of while creating your tip.

Apple prescribes that tips be actionable, instructional and easy to remember. Avoid using tips for promotional or error messages. Abstain from creating tips which are loaded with information or are lengthy, rather provide a high-level message and to explain things in much detail, navigate the user to a separate screen with all the required information.

That’s all for now with TipKit, thank you for reading and I hope the article provided a good understanding of TipKit. Please do hit the clap button and share the article if you felt it added some value to you, will do wonders to my confidence :)

--

--