Suggesting features to users with TipKit
WWDC 2023 brought many surprises, one of them being the introduction of TipKit, a framework that allows displaying suggestions or tips to users of an app. Some use cases may include making existing but lesser-known features more visible or showcasing new features introduced in an app update.
TipKit is available starting from devices with iOS 17, iPadOS 17, macOS 14, watchOS 10, and tvOS 17.
Example app in SwiftUI
As a demonstration, create an iOS project with SwiftUI in Xcode 15 and replace the content of the ContentView.swift file with the following code:
import SwiftUI
struct ContentView: View {
@State var number = 0
var body: some View {
VStack {
Text("\(number)")
.onTapGesture { number += 1 }
.font(.title)
}
.padding()
}
}
#Preview {
ContentView()
}
This code defines a SwiftUI view that counts the number of times the text is pressed. It uses a @State
variable number
to store the count of times the text is pressed and displays the number in a title format.
When running the project, the example app should look as shown below:
As you can see, this view doesn’t clearly communicate the functionality of tapping the text to the user. In this article, you’ll learn how to effectively use TipKit to suggest this functionality.
Enabling TipKit
Before creating your first tip, you need to enable TipKit at the launch of your app. To do this, open the structure that conforms to the App
protocol, add import TipKit
, and the following code after the definition of the main view within the WindowGroup
:
.task {
try? Tips.configure()
}
Tips.configure(_:)
loads and configures the states of all the tips in the app.
Another way to enable TipKit is by creating an initializer in the structure that conforms to the App
protocol:
init() {
try? Tips.configure()
}
For the purpose of this article, add try? Tips.resetDatastore()
before try? Tips.configure(_:)
to reset all tips to their initial state and allow showing the tips every time you run the app on the simulator or device. Your code should look like this:
.task {
try? Tips.resetDatastore()
try? Tips.configure()
}
If you want to test the tips from the Xcode Canvas, add the above code inside the #Preview
of the ContentView
, for example:
#Preview {
ContentView()
.task {
try? Tips.resetDatastore()
try? Tips.configure()
}
}
Remember to remove
try? Tips.resetDatastore()
before publishing your app in production.
Creating a Tip
To create your first tip, you need to create a structure that implements the Tip
protocol. This protocol defines the content of your tip. Next, create a structure named CountTip
and add the following code:
import TipKit
struct CountTip: Tip {
// 1
var title: Text {
Text("Press the text to count")
}
// 2
var message: Text? {
Text("The text will change when you tap it.")
}
// 3
var image: Image? {
Image(systemName: "hand.tap.fill")
}
}
title
: The title that will be displayed in the tip.message
: The message that will be displayed in the tip.image
: The image that will be displayed in the tip.
These properties take SwiftUI views, so you can use modifiers to customize them to your preferences, such as the
.foregroundStyle
modifier to change the color.
Adding a Tip to a View
The first thing you should do is import TipKit into ContentView
with import TipKit
, and then create an instance of the CountTip
tip:
var countTip = CountTip()
With the instance created, there are two ways to display a tip in a SwiftUI view: Inline and Popover.
Inline Tip
It is displayed within the current view with a card or bubble-like design, respecting the elements of the user interface already added. To add an Inline Tip above the Text
, use:
TipView(countTip)
When running the app, you will see the Inline Tip with the title, message, and image you defined in CountTip
, along with a close button with an X.
TipView
has an arrowEdge
parameter that allows you to display an arrow on the side of your choice. For example, if you want an arrow pointing downward, use:
TipView(countTip, arrowEdge: .bottom)
To modify the corner radius of the TipView
, you can use the .tipCornerRadius(_:antialiased:)
modifier. For example:
TipView(countTip)
.tipCornerRadius(100)
Popover Tip
Use the .popoverTip(_:arrowEdge:action:)
modifier to display the tip above the main view and anchored to the element that has this modifier.
To avoid showing the same tip twice, remove the inline tip TipView
and replace the Text
with the following code:
Text("\(number)")
.onTapGesture { number += 1 }
.font(.title)
.popoverTip(countTip)
When running the app, you will see the tip anchored to the text and a slight shadow to indicate to the user that the tip is on top of the main view.
By default, an arrow pointing to the anchored view is included. If you want to modify it, as with TipView
, use the arrowEdge
parameter. This will also change the position of the tip. For example, if you want the arrow to point downward:
.popoverTip(countTip, arrowEdge: .bottom)
From this section onward, what you learn applies to both ways of displaying a tip, so use the one you prefer, whether
TipView
or.popoverTip(_:arrowEdge:action:)
.
Manually Closing a Tip
In addition to closing the tip by pressing the X button, you can do so manually using the invalidate(reason:)
function, providing a reason to describe why the tip was invalidated. Reasons can be:
actionPerformed
: Indicates that the action described in the tip has been performed.displayCountExceeded
: Indicates that the tip has been displayed more times than allowed.tipClosed
: Indicates that the user closed the tip while it was displayed.
For example, if you want to close the tip when the user taps the Text
, add the following inside its .onTapGesture
modifier:
countTip.invalidate(reason: .actionPerformed)
Adding Buttons to the Tip
It’s straightforward to add buttons to your tips, as the Tip
protocol includes the var actions: [Action]
variable, which requires a list of Tips.Actions
.
Tips.Actions
has two initializers to create a button: init(id:perform:_:)
and init(id:title:perform:)
. These allow you to:
- Add an optional identifier in the
id
parameter. - Use a
String
orText
as the button's title. - Provide a closure in the
perform
parameter to specify the action to be taken when the user presses the button.
Open the CountTip.swift structure and insert the following code after the image
variable:
var actions: [Action] {
// 1
Action(id: "action-ok", title: "Ok")
// 2
Action {
invalidate(reason: .tipClosed)
} _: {
Text("Close")
.foregroundStyle(.red)
}
}
- An
Action
with the identifier "action-ok" and the text "Ok." - An
Action
that, when pressed, will close the tip and display the "Close" text in red usingText
and theforegroundStyle
modifier.
Another way to handle the button’s action from the SwiftUI view is by using the action
parameter in TipView
or the .popoverTip
modifier. This is simply a closure that contains the selected Action
.
If you are using TipView
, replace the view with the following code:
TipView(countTip) { action in
if action.id == "action-ok" {
countTip.invalidate(reason: .tipClosed)
}
}
If you are using .popoverTip
, replace the modifier with the following code:
.popoverTip(countTip) { action in
if action.id == "action-ok" {
countTip.invalidate(reason: .tipClosed)
}
}
In both cases, the id
of the action that the user pressed is checked, and the tip is closed accordingly.
Other Configurations
With Tips.configure(_:)
, you can add global configurations to all the tips in your app. These configurations include .displayFrequency(_:)
and .datastoreLocation(_:)
.
.displayFrequency(_:)
defines how often the tips will be shown. They can be shown all at once or one at a time every hour, day, week, or month..datastoreLocation(_:)
defines where the tip data will be stored. It can be stored in the app's default directory, a group container, or a specificURL
.
try? Tips.configure([
.displayFrequency(.immediate),
.datastoreLocation(.applicationDefault)
])
In this example, the tips are configured to be shown immediately and stored in the app’s default directory.
Other configurations that you can apply to a specific tip include setting the maximum number of times it will be shown before it is automatically invalidated with MaxDisplayCount
or ignoring the display frequency with IgnoresDisplayFrequency
. These two options are used within the options: [TipOption]
property of the Tip
protocol. For example:
To show the tip only 2 times:
var options: [TipOption] {
MaxDisplayCount(2)
}
To ignore the display frequency of the tip:
var options: [TipOption] {
IgnoresDisplayFrequency(true)
}
These two options override the global configuration set with .displayFrequency(_:)
for the tip in which they are specified.
Finally, in addition to Tips.resetDatastore()
, for testing purposes, you can use:
try? Tips.showAllTipsForTesting(_:)
: to show all or some tips.try? Tips.hideTipsForTesting(_:)
: to hide all or some tips.
Remember that these last two should be placed before Tips.configure(_:)
.
Conclusions
Apple recommends that all tips be actionable, instructive, and easy to remember. Additionally, TipKit allows you to synchronize the state of a tip through iCloud to ensure that a tip seen on one device is not displayed on others.
Don’t forget to share your tips with us on our social media! 😉
If you want to read the Spanish version of this article, you can find it here: https://asynclearn.com/blog/sugerir-funcionalidades-tipkit/