šŸš€ Discover Appleā€™s New Framework Called ā€œTipKitā€

Alkin Cakiralar
7 min readJul 28, 2023

--

tipkit, apple, swift, Xcode, iOS, macOS, iPadOS, visionOS

Introduction

In WWDC23, Apple introduced lots of new things to help us to make more efficient, maintainable and be able to avoid third-party libraries frameworks. ā€œTipKitā€ is the one of the framework that allows us to provide highlight the new features of our app among these.

Pros šŸ„³

  • Both SwiftUI and UIKit implementation available
  • Compatible with whole Apple platforms including iOS, macOS, iPadOS, visionOS
  • Can be easily integrate, testable
  • Chance to avoid third-party libraries such as ephread/Instructions

Cons šŸ¤”

  • Only compatible with iOS17 and newer versions as always ( Apple did this again without thinking the developers šŸ˜Ŗ )
  • Could irritate the users whose using your app if you wouldnā€™t act correctly in the app while highlighting your new features

šŸšØ Before we jumped out, please note that this feature only works with Xcode Beta 15ā€“5.0 and newer versions. Here is the link that you can download the latest Xcode version below. ā¬‡ļø

https://xcodereleases.com/

Letā€™s dive in to explore more details about ā€œTipKitā€

Letā€™s start with developing the screen that has an image to be to work on

struct ContentView: View {
var body: some View {
VStack {
Button {
//
} label: {
Image(.profile)
.resizable()
.frame(width: 200, height: 200)
.clipShape(.circle)
.padding(.leading, 22)
}
}
.padding()
.preferredColorScheme(.dark)
}
}

The first scenario is when we launched the app we expect that tooltip appear with title, message, icon and button actions on the bottom of the image. To be able to do this we have to create a struct class that conformed by Tip protocol. This protocol consist of ā¬‡ļø

id: String // Unique Identifier that auto assigned
title: Text
// The text that shows on the top of TipView as Title
message: Text?
// The text that shows on the middle of TipView as Message
asset: Image?
// The image that shows on the left side of TipView as icon
actions: [TipKit.Action]
// The action buttons that you can trigger user in the TipView
rules: [TipKit.Rule]
// Rules for adjusting to appearing of TipView
options: [TipOption]
// Customization options for style of TipView

Here is the UserWelcomeTip struct

struct UserWelcomeTip: Tip {
var title: Text {
Text("Welcome to The App")
}

var message: Text? {
Text("Enjoy it, discover and fun !")
}

var asset: Image? {
Image(systemName: "info.circle.fill")
}

var actions: [Action] {
[
Tip.Action(
id: "learn-about-me",
title: "Learn About Me"
),
]
}
}

Create an instance of UserWelcomeTip struct in the ContentView, then we will add popoverTip to the related button and set the property which name is userWelcomeTip. In addition to present the tip we have to adjust some settings with TipsConfiguration when view has appeared.

private let userWelcomeTip = UserWelcomeTip()
Button {
// action
} label: {
Image(.profile)
.resizable()
.frame(width: 100, height: 100)
.clipShape(.circle)
.padding(.leading, 22)
}
.popoverTip(userWelcomeTip, arrowEdge: .bottom)
.task {
try? await Tips.configure() {
DisplayFrequency(.immediate)
DatastoreLocation(.applicationDefault)
}
}

You can add custom configurations into to the configure() closure that conforms by TipsConfiguration which is currently we got two options including DisplayFrequency and DatastoreLocation.

DisplayFrequency helps you to adjust your tips frequency which consist of immediate, hourly, daily, weekly, monthly. We established that frequency using UserDefaults before. How cool and very simple it is, right?

DatastoreLocation allows you to configure the location that you want to store the tips preferences whatever path url inside the phone you want. In addition to this it has shouldReset parameter to reset the preferences as beginning. Itā€™s default value is false, you can use this parameter depends on your needs.

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

Run the application and you will be able to show your first tip that you just created on the bottom of the profile image as below šŸ„³

tipkit, apple, swift, Xcode, iOS, macOS, iPadOS, visionOStipkit, apple, swift, Xcode, iOS, macOS, iPadOS, visionOS

What if we want to present tips depends on some condition ?

Some scenarios, you may want to present your tips by conditionally. In this particular case Apple brings up with @Parameter and #Rule macros to allows us to manage conditions by each tip.

šŸšØ Before we use these macros you have to implement this into the BuildSettings -> Other Swift Flags section otherwise Xcode will not recognize these macros because itā€™s still experimental

-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

Letā€™s add the rule condition the our existing tip called UserWelcomeTip

Imagine you want to present the tip when user click the profile image, to be able to do that we need a parameter that we used in the rules of tip structs and we need to define our rule inside of the struct.

Here is the modified version of UserWelcomeTip Struct file below

struct UserWelcomeTip: Tip {
@Parameter
static var isActive: Bool = false

var title: Text {
Text("Welcome to The App")
}

var message: Text? {
Text("Enjoy it, discover and fun !")
}

var asset: Image? {
Image(systemName: "info.circle.fill")
}

var actions: [Action] {
[
Tip.Action(
id: "learn-about-me",
title: "Learn About Me"
),
]
}

var rules: [Rule] = [
#Rule(Self.$isActive) { $0 == true }
]
}

So, this tip only trigger when the static isActive property turns into the true. Until then, we will not encountered this tip in the application. Just set true to the isActive property when the button that has profile Image clicked to present the tip.

Button {
UserWelcomeTip.isActive = true
} label: {
Image(.profile)
.resizable()
.frame(width: 100, height: 100)
.clipShape(.circle)
.padding(.leading, 22)
}
.popoverTip(userWelcomeTip)
.task {
try? await Tips.configure() {
DisplayFrequency(.immediate)
DatastoreLocation(.applicationDefault, shouldReset: true)
}
}

šŸ‘€ Letā€™s dive into what happens in the background of these macros

Apple introduced SwiftMacros in WWDC23, basically it allows us to generate codes instead of us and help us to reduce our codebase more effective. Here is the link that you can get more knowledge about SwiftMacros from Apple Documentation below ā¬‡ļø

https://developer.apple.com/documentation/Swift/applying-macros#

When you select @Parameter or #Rule macros and right click you will see that there is an ExpandMacro selection in the panel, you will be able to see what code written from macro for you.

Itā€™s awesome isnā€™t it ? Donā€™t be late to try SwiftMacros for your own projects in the future šŸ§‘ā€šŸŽØ

Furthermore, letā€™s make our rules get more complex. Now weā€™re changing our scenario to user click count on the image to be able to present the tip.

Add an event to track our user click count into the UserWelcomeTip struct to be able to configure our rules depends on this event therefore we will have a chance to add click count condition rule depends on the user click count to present our tip

struct UserWelcomeTip: Tip {
@Parameter
static var isActive: Bool = false

// we define an event to capture user click count
static let numberOfTimesShowedOption: Event = Event(id: "dev.alkincakiralar.tip.numberOfTimesShowedOption")

var title: Text {
Text("Welcome to The App")
}

var message: Text? {
Text("Enjoy it, discover and fun !")
}

var asset: Image? {
Image(systemName: "info.circle.fill")
}

var actions: [Action] {
[
Tip.Action(
id: "learn-about-me",
title: "Learn About Me"
),
]
}

var rules: [Rule] = [
#Rule(Self.$isActive) { $0 == true },
// depends on the relevant capture event we add one more rule and
// we're checking the click count is equal and larger than 3
#Rule(Self.numberOfTimesShowedOption) { $0.donations.count >= 3 }
]
}

To be able to trigger that event we have to do call donate function from numberOfTimesShowedOption property wherever we want, it will kept in the storage that we defined in the DatastoreLocation. In this way, we can avoid to store these kind of rules in UserDefaults.

Button {
// we called the donate function every click of this button.
// when user clicked 3 times to that button the tips that we adjusted
// will be present immediately
UserWelcomeTip.numberOfTimesShowedOption.donate()
UserWelcomeTip.isActive = true
} label: {
Image(.profile)
.resizable()
.frame(width: 100, height: 100)
.clipShape(.circle)
.padding(.leading, 22)
}
.popoverTip(userWelcomeTip)
.task {
try? await Tips.configure {
DisplayFrequency(.immediate)
DatastoreLocation(.applicationDefault, shouldReset: true)
}
}

What about testing process ?

Apple gives us an opportunity to be able to test the tips scenarios in the app while making the UITests.

In the following methods will help you to test your tips easily.

  • Tips.showAllTips()
  • Tips.showTips([userWelcomeTip]
  • Tips.hideTips([userWelcomeTip]
  • Tips.hideAllTips()

Conclusion

This framework is a great chance to introduce our new features in the app. However, we should be aware of itā€™d be consequences when we use it more than itā€™s purpose. We should be avoid using this framework unnecessary times. Apple gives us some recommendations that when we should use use this inside the app. Here are the recommendations and not recommendations images from WWDC23 about TipKit.

References

--

--