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)
...
}
}
Post
Replies
Boosts
Views
Activity
I have a View that defines a row to be displayed in a ScrollView in a macOS app. On the right side of the view is a button, which is custom styled. When the row view is on its own or embedded in another normal view, everything draws normally. However, when it's embedded in a ScrollView, the button appears shifted up and to the left from where it should be drawn. This happens regardless of whether the scrollbar is hidden or shown (with the showsIndicator: argument label or by turning it off manually in System Preferences).
Here's how the ScrollView is built up:
ScrollView {
		ForEach(permissions.indices) {
				PermissionCell(permissionUpdateBlock: self.calculateEnabledCount, permission: permissions[$0])
				.background(Color(($0 % 2 == 0 ? scrollViewBackgroundColor : scrollViewBackgroundAlternateColor) ?? .white))
		}
}
.padding(0)
.border(Color(isDarkTheme ? .darkGray : .lightGray))
.background(Color(scrollViewBackgroundColor))
Also, here's an abridged version of the row View:
struct PermissionCell: View {
	 var permissionUpdateBlock: (() -> ())
	 var permission: TSKPermission
	
	 @State private var permissionStatus = PermissionStatus.notDetermined
	
	 var body: some View {
			VStack {
				 HStack {
						if permission.icon != nil {
							 Image(nsImage: permission.icon!)
									.padding(.vertical, 15)
									.padding(.trailing, 10)
						}
						VStack(alignment: .leading) {
							 Text(permission.title)
									.fontWeight(.bold)
							 Text(permission.purpose)
									.font(.caption)
						}
						Spacer()
						if permissionStatus == .notDetermined {
							 Button(action: {
									self.permission.askForPermission {
										 self.updatePermissionStatus()
									}
							 })
							 {
									Text("Enable")
							 }
							.buttonStyle(TSKButtonStyle(backgroundColor: Color(TSKThemeManager.sharedInstance.snagitBlueColor()), textColor: .white))
							 .frame(width: 80, height: 25)
							 .border(Color.red)
						}
						else {
							 ...
						}
				 }
				 .padding(.horizontal, 15)
				 .frame(height: 70)
			}
			...
At imgur.com/a/FKa33bf (For some reason, I'm not allowed to actually link) is an Imgur album of screenshots of the previews of these views. I have a .border(Color(.red)) on the button for demonstration purposes.
Does anyone know what's going on? Or is this just a SwiftUI bug that may be patched up later?