I'd like to offer an option to let my users see tips again.
Is there a way to do this either globally, or per tip ?
right now the only option I see is to have initialization login to call Tips.resetDatastore() and have the user restart the app.
I'm currently using events and rules to only show each tip once.
TipKit
RSS for tagIntelligently educate your users about the right features at the right time with TipKit
Posts under TipKit tag
33 Posts
Sort by:
Post
Replies
Boosts
Views
Activity
I have created a tip with a parameter (Bool) I have tried to set it to true in a sheet which is presented modally over the view which should present the popover tip. After the sheet gets dismissed the popover tip is never presented. If I restart the app, the popover tip appears. Is there any way to trigger the presentation of a popover tip manually?
I have created a little demo app to demonstrate my problem:
Setup TipKit on app start:
import SwiftUI
import TipKit
@main
struct TipKitDemoApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.task {
try? Tips.configure()
}
}
}
}
Simple tip:
import Foundation
import TipKit
struct DemoTip: Tip {
@Parameter
static var enabled: Bool = false
var title: Text {
Text("Demo Tip")
}
var rules: [Rule] {
[
#Rule(Self.$enabled) { $0 == true }
]
}
}
Content view which includes the popover tip and displays the sheet where the tip can be enabled:
import SwiftUI
struct ContentView: View {
@State private var presentDetail = false
let demoTip = DemoTip()
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
.popoverTip(demoTip)
Button("Present Details") {
presentDetail.toggle()
}
}
.padding()
.sheet(isPresented: $presentDetail) {
DetailView()
}
}
}
In the detail view the tip gets enabled, but if I dismiss this view, the tip only appears after I restart the app:
import SwiftUI
struct DetailView: View {
@Environment(\.dismiss) private var dismiss
var body: some View {
Button("Enable demo tip") {
DemoTip.enabled = true
}
Button("Dismiss") {
dismiss()
}
}
}
Hi - I use TipKit in my App and AppClip. TipKit is configured with the app group's datastore. The tips show in the App, but on the AppClip, with the same rules/state, the tips do not display. Is this expected? TipKit is not listed as one of the frameworks unavailable to AppClips.
try? Tips.configure([
Tips.ConfigurationOption.displayFrequency(.hourly),
Tips.ConfigurationOption.datastoreLocation(.groupContainer(identifier: BuildConfiguration.shared.userDefaultsSuite))
])
Context: I maintain an app that has a UITabBarController at its root, and the tabs are UIHostingControllers to (mostly) SwiftUI views. I have a UINavigationController I present from that tab bar controller, and the rootViewController of that presented nav is a UIHostingController with SwiftUI views inside (not using NavigationView in there, but I am using .toolbar and NavigationLink).
I'm trying to use TipKit in this presented SwiftUI view, and its led to a couple unexpected issues:
When I dismiss a tip, the entire presented view controller hierarchy is dismissed (like calling window.rootViewController.dismiss())
somehow, configuring the tooltip inside of a ButtonStyle resolves this issue
When I show a tip pointing at a Button inside of the .toolbar
sometimes that tip is presented in the top left corner of the window, not pointing to the view properly
when the tip is dismissed, that button then stops calling its action and does nothing
Has anyone else experienced this/anything like it? Because of this (and other issues from the past) I get the sense that Apple is of the mind that you should either have a fully UIKit application, or a fully SwiftUI application, since issues with bridging between the two are not uncommon and solutions are unclear/undocumented. Curious what others think about this, and about maintaining UIKit/SwiftUI hybrid apps in general.
import SwiftUI
import TipKit
struct ChatRoomView: View {
@StateObject private var socketManager = SocketIOManager()
@State private var inputText: String = ""
@StateObject var viewModel = SignInWithAppleViewModel()
@Binding var isCall: Bool
@State private var isSheet = false
@State private var ShowView = false
var learnlisttip = KeyTip()
@Binding var showShareSheet: Bool
@Binding var codeshar: String
var body: some View {
NavigationStack{
VStack {
if let roomCode = socketManager.roomCode {
ZStack{
VStack{
HStack{
Text("Room Key: \(roomCode)")
.font(.title)
.onAppear{
codeshar = roomCode
self.isCall = true
}
Button(action:{
self.showShareSheet = true
}, label:{
Image(systemName: "square.and.arrow.up.fill")
.accessibilityLabel("Share")
})
}
.padding(20)
TipView(learnlisttip, arrowEdge: .top)
.glassBackgroundEffect()
.offset(z: 20)
Spacer()
}
List(socketManager.messages, id: \.self) { message in
Text(message)
}
TextField("input", text: $inputText)
Button("send") {
socketManager.sendMessage(roomCode: roomCode, message: inputText)
inputText = ""
}
}
.sheet(isPresented: $showShareSheet) {
let shareContent = "Open SpatialCall, Join this Room, Key is: \(codeshar)"
ActivityView(activityItems: [shareContent])
}
} else {
HStack{
Button(action:{
withAnimation{
socketManager.createRoom()
}
}, label: {
VStack{
Image(systemName: "phone.circle.fill")
.symbolRenderingMode(.multicolor)
.symbolEffect(.appear, isActive: !ShowView)
.font(.largeTitle)
Text("Add Room")
.font(.title3)
}
})
.buttonStyle(.borderless)
.buttonBorderShape(.roundedRectangle)
.padding(.horizontal, 30)
.glassBackgroundEffect()
.offset(z: 20)
.scaleEffect(1.5)
.padding(60)
Button(action:{
withAnimation{
self.isSheet = true
}
}, label: {
VStack{
Image(systemName: "phone.badge.checkmark")
.symbolRenderingMode(.multicolor)
.symbolEffect(.appear, isActive:!ShowView)
.font(.largeTitle)
Text("Join Room")
.font(.title3)
}
})
.buttonStyle(.borderless)
.buttonBorderShape(.roundedRectangle)
.padding(.horizontal, 30)
.glassBackgroundEffect()
.offset(z: 20)
.scaleEffect(1.5)
.padding(70)
}
}
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
withAnimation {
self.ShowView = true
}
}
}
.sheet(isPresented: $isSheet){
VStack{
Text("Join Room")
.font(.largeTitle)
Text("You need to get the key to the room.")
TextField("Key", text: $inputText)
.padding(30)
.textFieldStyle(.roundedBorder)
Button(action:{
socketManager.joinRoom(roomCode: inputText)
self.isSheet = false
}, label: {
Text("Join Room")
.font(.title3)
})
.padding(50)
}
.padding()
}
.sheet(isPresented: $socketManager.showRoomNotFoundAlert) {
Text("The room does not exist. Please check whether the Key you entered is correct.")
.font(.title)
.frame(width: 500)
.padding()
Button(action:{
self.socketManager.showRoomNotFoundAlert = false
}, label: {
Text("OK")
.font(.title3)
})
.padding()
}
}
}
}
In the above code (this is a visionOS project), when I click Share, it can't display Sheet normally, and TipView can't be displayed either. Why?
try Tips.resetDatastore()
try Tips.configure(
[
// Reset which tips have been shown and what parameters have been tracked, useful during testing and for this sample project
.datastoreLocation(.applicationDefault),
// When should the tips be presented? If you use .immediate, they'll all be presented whenever a screen with a tip appears.
// You can adjust this on per tip level as well
.displayFrequency(.immediate)
]
)
struct UserTip: Tip {
static let hoverEvent: Event = Event(id: "hoverEvent")
@Parameter var isHovering: Bool = false
static var tipCountKey = "UserTipCount"
var title: Text
var message: Text?
var image: Image?
var tipShownLimit: Int
var options: [Option] {
// Show this tip 5 times.
[
Tips.MaxDisplayCount(5),
Tips.IgnoresDisplayFrequency(true)
]
}
var rules: [Rule] {
#Rule($isHovering) {
$0 == true
}
}
}
struct ShowPopoverTip: View {
@State private var tip = UserTip(
title: Text("the title"),
message: Text("the message here"),
image: Image(systemName: "volleyball.fill"),
tipShownLimit: 10
)
var body: some View {
Button(action: {
}) {
Text("Hover over me")
}
.popoverTip(tip)
.onAppear {
}
.onHover { hovering in
if hovering {
tip.isHovering = true
print("tip.status: \(tip.status)")
print("tip.isHovering: \(tip.isHovering)")
print("tip.shouldDisplay: \(tip.shouldDisplay)")
}else{
tip.isHovering = false
print("tip.isHovering: \(tip.isHovering)")
}
}
}
}
The popover only works once, even though I have set it to Tips.MaxDisplayCount(5)
Either the Tip is getting invalidated or popovers only show once.
debug output:
tip.isHovering: true
tip.shouldDisplay: false
tip.isHovering: false
tip.status: pending
tip.isHovering: true
tip.shouldDisplay: false
tip.isHovering: false
btw, if I remove the Tips.resetDatastore(), it still shows once each time I launch the app.
static let hoverEvent: Event = Event(id: "hoverEvent")
/// Parameters-Rules
@Parameter
static var isHovering: Bool = false
static var tipCountKey = "UserTipCount"
var title: Text
var message: Text?
var image: Image?
var tipShownLimit: Int
@ObservedObject var buttonState: ButtonState
var rules: [Rule] {
#Rule(Self.hoverEvent) {
$0.donations.count < tipShownLimit
}
}
}
error: Event Rules require a count comparison.
If I replace tipShownLimit like this:
var rules: [Rule] {
#Rule(Self.hoverEvent) {
$0.donations.count < 3
}
}
it compiles fine. the error is: Event Rules require a count comparison.
If I have some tips in an onboarding flow and want to allow my user to restart the onboarding experience, how can I reset specific tips? I know there is Tips.resetDatastore() but I may not want to reset every tip, just some subset of them.
In iOS 17.1 (and 17.2 beta), the arrowEdge parameter of the SwiftUI popoverTip doesn't work anymore.
This code
button
.popoverTip(tip, arrowEdge: .bottom)
looks like this on iOS 17.0
and like this on 17.1 and up.
I checked permittedArrowDirections of the corresponding UIPopoverPresentationController (via the Memory Graph): It's .down on iOS 17.0 and .any (the default) on 17.1. It seems the parameter of popoverTip is not properly propagated to the popover controller anymore.
Hi. We want to implement TipKit, and specifically popovertips, in our app but it does not seem to be a way to customize these as you can with TipView and custom TipViewStyles. I noticed the documentation references MiniTipStyle but it only contains the standard minitipStyle. Is there a way to fully customize popovertips or is it coming in a future update?
Best regards
Johannes
Hello!
I've tested/implemented TipKit in SwiftUI and UIKit but it seems that the close, i.e. X, button doesn't work in UIKit but does in SwiftUI. Not sure if this is a bug or I have to do something different about it in UIKit.
Testing with Xcode 15 Beta 8
Thanks!
Hi folks,
there's currently a known issue in TipKit due to which it won't show popover tips on buttons that are inside a SwiftUI ToolbarItem. For example, if you try this code, the popover tip will not appear:
ToolbarItem {
Button(action: {...}) {
Label("Tap here", systemImage: "gear")
}
.popoverTip(sampleTip)
}
There's an easy workaround for this issue. Just apply a style to the button. It can be any style. Some examples are bordered, borderless, plain and borderedProminent. Here's a fixed version of the above code:
ToolbarItem {
Button(action: {...}) {
Label("Tap here", systemImage: "gear")
}
.buttonStyle(.plain) // Adding this line fixes the issue.
.popoverTip(sampleTip)
}
Hope this helps anyone running into this issue.
Hello! Based on the lack of forum posts, I think I'm one of the first people to really be diving into TipKit. :) I'm trying to use a tip to coax users toward a button in the toolbar of a NavigationView. The docs say to put the TipView "close to the content", but the best I can do for the NavigationView toolbar is to put it in one of the views inside the Navigation View itself. I'm using a TipView with an arrowEdge: .top parameter, which results in this:
I'd love to be able to move the arrow tip under the plus button. Is that possible in this early beta stage? Do I need to restructure my view hierarchy somehow?