Intelligently educate your users about the right features at the right time with TipKit

Pinned Posts

Posts under TipKit tag

39 Posts
Sort by:
Post not yet marked as solved
0 Replies
59 Views
I recently integrated TipKit's .popoverTip to hint users about key features in my app, but find they are too easily oversaw and dismissible: as soon as they appear if i tap outside or scroll they disappear and many users don't event realise they're here and skip them, without ever seing them again since they have been dismissed. Is there a way to make them more persistent as the TipView is? I tried using TipView but it doesn't fit my use case since the feature I want to tip about is a button in the top right side of a toolbar, TipView's arrow doesn't point to the feature so there isn't a lot of context between the tip and the button itself.
Posted Last updated
.
Post not yet marked as solved
0 Replies
65 Views
Hi all, I work on a macOS app that's actually split into two apps: one primary app and one "helper" app that lives in the menubar and runs in the background when the primary app is quit. Recently, I've been integrating TipKit into these apps, and I'd like to have one unified TipKit database shared between them. I set up TipKit in both apps' AppDelegate classes with the datastoreLocation set to the apps' shared Group Containers folder. I've verified with a SQLite DB viewer that both apps can store event donations and parameters as well as tip status in that shared database. However, updates to the TipKit database are not being propagated between the two apps. For example, I have a TipKit event that only the "helper" app donates to, and I use that event for a TipKit rule in a Tip displayed in the primary app, but the tip only displays after I restart the primary app instead of immediately once the rule requirements are met (I've verified that the helper is properly making donations to that event). Unfortunately, combining both apps into one app is out of the question (in the near term, anyways). Is there anything I'm missing here to get cross-app TipKit communication to work? Here's the relevant code (truncated and with variable and class names altered for IP reasons): TipKitConstants.swift (accessible by both apps in a shared framework) import TipKit // MARK: - DataStore @available(macOS 14.0, *) extension Tips.ConfigurationOption.DatastoreLocation { public static let sharedGroupContainer = Tips.ConfigurationOption.DatastoreLocation.url(NSURL.sharedGroupContainer()) // This NSURL extension points to a location in group containers that both apps can write to } // MARK: - Events @available (macOS 14, *) public struct TipKitEvents { ... public static let helperEvent = Tips.Event(id: "helperEvent") ... } ... PrimaryAppDelegate+TipKit.swift (app delegate is in obj-c, hence the extension) import TipKit extension PrimaryAppDelegate { @objc func setupTips() { if #available(macOS 14, *) { ... try? Tips.configure([ .displayFrequency(.immediate), .datastoreLocation(.sharedGroupContainer) ]) } } } HelperAppDelegate+TipKit.swift (app delegate is in obj-c, hence the extension) extension HelperAppDelegate { @objc func setupTips() { if #available(macOS 14, *) { try? Tips.configure([ .displayFrequency(.immediate), .datastoreLocation(.sharedGroupContainer) ]) } } } HelperClass+TipKit.swift (this is the class where the event donation happens) import CommonFramework extension HelperClass { @objc func donateHelperEvent() { if #available(macOS 14, *) { Task(priority: .background) { await TipKitEvents.helperEvent.donate() } } } ... } ExampleTip.swift (exists in the primary app) @available(macOS 14, *) struct ExampleTip: Tip { ... // All Tip protocol requirements are implemented above var rules: [Rule] { [#Rule(TipKitEvents.helperEvent) { $0.donations.count >= 3 }] } ... } PrimaryAppWindowController.h @interface EditorWindowController : NSWindowController ... // TipKit types are not representable in Objective-C, hence all the "id" types here @property id templateCreationTip; @property id templateCreationTipObservationTask; @property id templateCreationTipPopover; ... PrimaryAppWindowController.m @implementation PrimaryAppWindowController ... - (void)windowDidLoad { [self setUpTips]; } ... PrimaryAppWindowController+TipKit.swift @available(macOS 14, *) extension PrimaryAppWindowController { @objc func setUpTips() { if exampleTip == nil { exampleTip = ExampleTip() } exampleTipObservationTask = exampleTipObservationTask ?? Task { @MainActor in if let tip = exampleTip as? ExampleTip { for await shouldDisplay in tip.shouldDisplayUpdates { if shouldDisplay { showExampleTip() } else { (exampleTipPopover as? TipNSPopover)?.close() exampleTipPopover = nil } } } } } @objc func showExampleTip() { guard let exampleTip = exampleTip as? ExampleTip, let buttonView = window?.toolbar?.items.filter({ $0.itemIdentifier.rawValue == ItemIdentifier.button }).first?.view else { return } exampleTipPopover = TipNSPopover(exampleTip) (exampleTipPopover as? TipNSPopover)?.show(relativeTo: buttonView.bounds, of: buttonView, preferredEdge: .maxY) ... } }
Posted
by cgreene.
Last updated
.
Post not yet marked as solved
5 Replies
2.0k Views
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.
Posted
by melsam.
Last updated
.
Post not yet marked as solved
0 Replies
122 Views
I implemented TipKit in my app, but I ran into this problem: I want every tap that happens when a tip is open only close the tip and nothing else. Only the next taps should interfere with the View that called the tip. I have a game where you tap on a sort of hidden object, so when you tap on a wrong place it generates an error. When the tip is open and you accidentally tap on the game (or want to close the tip that way) the game reads this as intentional game tap and counts that as an error. With other overlaying views I have a Bool property that prevents this behavior, because the game 'knows' that a tap is only legit if this Bool is false. But I can't figure out how to 'let the game know' that a tip is open. Is there a way how I can read if a tip is shown or already invalidated?
Posted
by Daffe.
Last updated
.
Post not yet marked as solved
1 Replies
143 Views
TipKit stores all Event donations including their Date locally. I am wondering if it's still good practise to have an Event for a action that happens a lot of times in my app (the main feature), since Power-Users could get over a million triggers of this event. This would result into millions of entries, but I only need a rule counting if the count is higher then 3. Would it be smarter to think of a different approach for this use-case to save storage (and maybe query speed), or is it still advised to use Events for something like this?
Posted
by seemayr.
Last updated
.
Post not yet marked as solved
1 Replies
257 Views
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.
Posted Last updated
.
Post not yet marked as solved
0 Replies
310 Views
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() } } }
Posted
by patrick.
Last updated
.
Post not yet marked as solved
0 Replies
317 Views
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)) ])
Posted Last updated
.
Post not yet marked as solved
1 Replies
458 Views
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.
Posted Last updated
.
Post not yet marked as solved
1 Replies
525 Views
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.
Posted
by rschluet.
Last updated
.
Post not yet marked as solved
1 Replies
464 Views
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?
Posted
by lijiaxu.
Last updated
.
Post not yet marked as solved
2 Replies
409 Views
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.
Posted
by cab3272.
Last updated
.
Post not yet marked as solved
2 Replies
343 Views
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.
Posted
by rschluet.
Last updated
.
Post not yet marked as solved
0 Replies
483 Views
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.
Posted Last updated
.
Post not yet marked as solved
4 Replies
654 Views
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
Posted Last updated
.
Post marked as solved
13 Replies
1.4k Views
My TipKit views are showing in Previews, when I run on a Simulator, and on a physical device (running iOS 17 Beta), but if I archive and upload to TestFlight, I do not see them (on devices running iOS 17 Beta)... Anyone else encounter this? I am using the Xcode 15 GM (15A240d). I tried manually adding the framework into the target. I am worried that if I submit for Review it will not show on devices when released... Am I just supposed to trust that it will work via the App Store? 🫣
Posted Last updated
.
Post marked as solved
1 Replies
400 Views
Hi there, I am quite new to SwiftUI and got this problem: I want to implement TipKit in my app but as soon as I put DisplayFrequency and datastoreLocation into my code, I get this error message: I know that I somehow could look it up in the documentation but unfortunately I am still so new, that I don't understand it. Could someone help? Best Laurin
Posted
by rinuality.
Last updated
.
Post marked as solved
4 Replies
580 Views
I'm using UIKit and setting up TipKit with the suggested defaults try? Tips.configure([ .displayFrequency(.immediate), .datastoreLocation(.applicationDefault) ]) within func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { } Every time I swipe out of the app the tips start reappearing again - even the ones that have been invalidated. Has this happened for anyone else and does anyone else have a solution? Here's an example of one of my tips struct createTabTipHomePage: Tip { var title = styleTipTitle("Title") var message = styleTipMessage("Message") var asset: Image { Image(systemName: "plus.square") .resizable() } var rules: [Rule] { [ #Rule(TipActions.$homeTrandingScrolledToCreateTrigger) { $0 == true } ] } var options: [TipOption] { [Tip.MaxDisplayCount(1)] } func showAction() { TipActions.homeTrandingScrolledToCreateTrigger.toggle() } }
Posted Last updated
.