Sharing a TipKit database between two

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)
      ...
   }
}