-
Notifications
You must be signed in to change notification settings - Fork 3
TipKit
Package
SwiftBindings.Apple.TipKit· Version26.2.6Auto-published fromapple-frameworks/TipKit/TIPKIT-GUIDE.md.
SwiftBindings.Apple.TipKit exposes Apple's TipKit framework — in-app tips for surfacing features — to C# through .NET 10's native Swift interop. These are direct Swift calls, not Objective-C proxy wrappers. TipKit's model is non-obvious: a tip is a Swift type conforming to the Tip protocol, its eligibility is governed by display rules and donated events, and its current status (pending / available / invalidated) is what you check before showing UI. This guide maps that model to the generated C# surface and is explicit about which parts can be driven from pure C# versus which require a Swift companion.
- Requirements & install
- Naming conventions
- The TipKit model in C#
- Quick start: configure and check status
- Configuring the datastore
- Tip status & invalidation
- Display-frequency options
- Donations & events
- Displaying tips (UIKit)
- Tip actions
- Errors
- Known limitations
- Memory & threading
- Reference links
- .NET 10.0+
- iOS 26.2+, macOS 26.2+, Mac Catalyst 26.2+, tvOS 26.2+
- macOS host for development
dotnet add package SwiftBindings.Apple.TipKit
using TipKit;| Swift | C# | Rule |
|---|---|---|
Tips.configure(_:) |
Tips.Configure(IEnumerable<Tips.ConfigurationOption>) |
static methods are PascalCase; the trailing default-empty form gets a no-arg overload Tips.Configure()
|
enum Status { case available } |
Tips.Status class with .Tag (a CaseTag enum) + static singletons |
Swift enums-with-payload project to a class with a CaseTag and TryGet* accessors; payload-less cases are static properties (Tips.Status.Available) |
enum InvalidationReason: Int (plain) |
C# enum : int
|
plain Swift enums become ordinary C# enums |
.maxDisplayCount(3) (a Tips.Rule.Option) |
new Tips.MaxDisplayCount((nint)3) |
option cases project to constructible types |
Tips.DonationTimeRange.minutes(5) |
Tips.DonationTimeRange.Minutes(5) |
enum-with-associated-value factories become static methods |
nested Tips.ConfigurationOption.DisplayFrequency.daily
|
Tips.ConfigurationOption.DisplayFrequency.Daily (static property) |
nested config presets are static singletons |
Most of the consumer-facing types are nested under the static
Tipsclass (Tips.Status,Tips.ConfigurationOption,Tips.MaxDisplayCount,Tips.DonationTimeRange,Tips.Action, …). A few live at theTipKitnamespace root:TipKitError,TipGroup,AnyTip, theITipinterface, and the UIKit view types (TipUIView,TipUIPopoverViewController,TipUICollectionViewCell,TipUICollectionReusableView).
The single most important fact: you cannot define a Tip type in pure C#. In Swift you write struct MyTip: Tip { ... }; that protocol conformance (and the result-builder rules it carries) is compiler-synthesized and has no C#-constructible equivalent. The binding instead gives you:
-
ITip— the projectedTipprotocol. Because you can't define a Tip in pure C#, everyITipyou hold in practice points at a Swift-backedTipProxy(the generated existential wrapper). On that proxy,Id,Title, andOptionsdispatch correctly via wrapper thunks.MessageandImagethrowNotSupportedExceptionbecause their optional SwiftUI types aren't witness-dispatchable across the existential boundary (SB0003); access them on the concrete Swift conforming type.StatusandInvalidate(reason:)are not part of theITipinterface — they live on the concrete Swift conforming type only; expose entry points from your Swift companion if you need them from C#. -
AnyTip— a type-erased wrapper around an existing Swift tip, exposing read-only metadata (Id,Title,Message,Image,Options). It has no public constructor; you obtain one from Swift viaAnyTip.FromTipKit_AnyTip(...).
So the supported pattern from C# is: define your tips in a small Swift companion target, then drive configuration, display, donations, and presentation from C#. Everything in the rest of this guide — datastore configuration, status enums, option/frequency types, donation time ranges, the UIKit presentation controller, actions, and errors — is fully usable from C#. The rule-builder DSL itself is the one piece that must originate in Swift.
using TipKit;
// 1. Configure the TipKit datastore once, at app startup.
// The no-arg overload uses defaults; pass options to customize (see below).
try
{
Tips.Configure();
}
catch (Exception ex)
{
// Configure throws TipKitError.TipsDatastoreAlreadyConfigured if called twice.
}
// 2. Inspect a tip's current status. (`tip` is an ITip provided by your
// Swift companion target — see "The TipKit model in C#".)
ITip tip = GetMyTipFromSwift();
// Work with the type-erased metadata:
using var anyTip = AnyTip.FromTipKit_AnyTip(/* a Swift AnyTip */);
string title = anyTip.Title.ToString();
// 3. Compare status singletons / tags:
if (Tips.Status.Available.Tag == Tips.Status.CaseTag.Available)
{
// a tip in the Available state is eligible to be shown
}
// 4. Testing helpers force every tip visible / hidden regardless of rules:
Tips.ShowAllTipsForTesting();
Tips.HideAllTipsForTesting();
// 5. Reset stored donation/event/display state (e.g. between test runs):
Tips.ResetDatastore();Tips.Configure initializes TipKit's persistent store. Two overloads:
Tips.Configure(); // defaults
Tips.Configure(IEnumerable<Tips.ConfigurationOption> options); // customizedBuild the options from the static factories on Tips.ConfigurationOption, each of which wraps a nested preset type:
var options = new[]
{
Tips.ConfigurationOption.DisplayFrequencyMethod(
Tips.ConfigurationOption.DisplayFrequency.Daily),
Tips.ConfigurationOption.DatastoreLocationMethod(
Tips.ConfigurationOption.DatastoreLocation.ApplicationDefault),
};
Tips.Configure(options);Tips.ConfigurationOption factories:
| Factory | Argument | |
|---|---|---|
DisplayFrequencyMethod(DisplayFrequency) |
a frequency preset | how often tips may appear globally |
DatastoreLocationMethod(DatastoreLocation) |
a store location | where state is persisted |
CloudKitContainerMethod(CloudKitContainer?) |
a CloudKit container | sync tip state across devices |
Tips.ConfigurationOption.DisplayFrequency presets (static properties): Immediate, Hourly, Daily, Weekly, Monthly.
Tips.ConfigurationOption.DatastoreLocation: ApplicationDefault (static), plus GroupContainer(string identifier) and Url(Foundation.NSUrl url) factories.
Tips.ConfigurationOption.CloudKitContainer: Automatic (static) and Named(string containerName).
Tips.Status is a projected Swift enum. Discriminate with .Tag; the Invalidated case carries a reason.
public enum Tips.Status.CaseTag : uint
{
Invalidated = 0,
Pending = 1,
Available = 2,
}Tips.Status status = /* a status value */;
switch (status.Tag)
{
case Tips.Status.CaseTag.Available:
// eligible to show
break;
case Tips.Status.CaseTag.Pending:
// rules not yet satisfied
break;
case Tips.Status.CaseTag.Invalidated:
if (status.TryGetInvalidated(out Tips.InvalidationReason reason))
{
// reason tells you why it was dismissed
}
break;
}Cached singletons for the payload-less cases:
Tips.Status p = Tips.Status.Pending;
Tips.Status a = Tips.Status.Available;
// the payload case is a factory:
Tips.Status inv = Tips.Status.Invalidated(Tips.InvalidationReason.TipClosed);Tips.InvalidationReason (plain enum : int):
| Case | Value |
|---|---|
ActionPerformed |
0 |
DisplayCountExceeded |
1 |
DisplayDurationExceeded |
2 |
TipClosed |
3 |
Invalidating a tip from C# goes through the concrete tip's
Invalidate(reason:), which lives on your Swift conforming type and is not part of theITipinterface. Expose an invalidation entry point from your Swift companion if you need to invalidate programmatically from C#.
These per-tip options correspond to Swift's Tips.Rule.Option/@Parameter-adjacent option cases and are constructible directly. They configure how an individual tip is gated.
var maxCount = new Tips.MaxDisplayCount((nint)3); // show at most 3 times
var maxDuration = new Tips.MaxDisplayDuration(30.0); // seconds on screen
var ignoresFreq = new Tips.IgnoresDisplayFrequency(true); // bypass global frequency| Type | Constructor | |
|---|---|---|
Tips.MaxDisplayCount |
new Tips.MaxDisplayCount(nint maxDisplayCount) |
cap total displays |
Tips.MaxDisplayDuration |
new Tips.MaxDisplayDuration(double maxDisplayDuration) |
cap on-screen time (seconds) |
Tips.IgnoresDisplayFrequency |
new Tips.IgnoresDisplayFrequency(bool) |
exempt from global DisplayFrequency
|
All three conform to ITipOption. Like configuration options, they're consumed by tips declared in Swift; constructing them in C# lets you supply values to a Swift-side tip factory.
TipKit's eligibility engine counts event donations — "the user did X N times". The supporting value types are bound, even though the rule-builder DSL that consumes them is Swift-only (see Known limitations).
// A time window for donation-count rules:
Tips.DonationTimeRange lastWeek = Tips.DonationTimeRange.Week; // preset
Tips.DonationTimeRange last5Min = Tips.DonationTimeRange.Minutes(5);
Tips.DonationTimeRange last2Days = Tips.DonationTimeRange.Days(2);
// A cap on how much donation history to retain:
using var limit = new Tips.DonationLimit(maximumCount: 100);
int cap = limit.MaximumCount; // 100Tips.DonationTimeRange — presets (static): Minute, Hour, Day, Week; factories: Minutes(nint), Hours(nint), Days(nint), Weeks(nint) (each also has an int convenience overload). It supports value equality and a DecodeFromJson(byte[]) helper.
Tips.DonationLimit — new Tips.DonationLimit(nint maximumCount, Tips.DonationTimeRange? maximumAge = null), exposing .MaximumCount.
Tips.ParameterOption.Transient (static singleton) marks an event/parameter as non-persisted.
The
Tips.Event<TDonationInfo>,Tips.Event.Donation, andTips.Parameter<TValue>types are generic over Swift-defined info types and are primarily reachable from a Swift companion that declares the concrete event/parameter; the surrounding value types above bind cleanly in C#.
TipKit's SwiftUI views (TipView, PopoverTipView) are not usable from C# (see Known limitations). The supported presentation path is UIKit, via TipUIPopoverViewController — a UIViewController you present from a source view/bar item.
using TipKit;
using UIKit;
// `tip` is an ITip from your Swift companion target.
ITip tip = GetMyTipFromSwift();
var popover = new TipUIPopoverViewController(
tip,
sourceItem, // any UIPopoverPresentationControllerSourceItem (e.g. a UIView / UIBarButtonItem)
action => // Action<Tips.Action> — invoked when the user taps a tip action
{
// handle the tapped action (see "Tip actions")
action.Handler();
});
PresentViewController(popover, animated: true, completionHandler: null);The other UIKit types are also bound for collection-view-based tip layouts:
| Type | Base | |
|---|---|---|
TipUIPopoverViewController |
UIKit.UIViewController |
present a tip as a popover; ctor takes (ITip tip, object sourceItem, Action<Tips.Action> actionHandler)
|
TipUIView |
UIKit.UIView |
inline tip view; obtain via TipUIView.FromTipKit_AnyTip(AnyTip)
|
TipUICollectionViewCell |
UIKit.UICollectionViewCell |
new TipUICollectionViewCell(CGRect frame) |
TipUICollectionReusableView |
UIKit.UICollectionReusableView |
reusable supplementary view |
A tip can declare action buttons. When the user taps one, your presentation handler receives a Tips.Action.
// Inside the actionHandler passed to TipUIPopoverViewController:
action =>
{
string id = action.Id; // the action's identifier
int? index = action.Index; // its position, if ordered
action.Handler(); // invoke the action's handler
}Tips.Action members: Id (string), Index (int?), Handler (System.Action). You can also construct one directly:
var custom = new Tips.Action(
id: "learn-more",
handler: () => OpenHelp(),
label: () => /* Swift.SwiftUI.Text */);Throwing TipKit calls (notably Tips.Configure) surface a TipKitError. It exposes payload-less static singletons:
| Singleton | When |
|---|---|
TipKitError.TipsDatastoreAlreadyConfigured |
Configure called more than once |
TipKitError.InvalidPredicateValueType |
a rule predicate used an unsupported value type |
TipKitError.MissingGroupContainerEntitlements |
DatastoreLocation.GroupContainer(...) without the App Group entitlement |
try
{
Tips.Configure();
}
catch (Exception ex)
{
// Compare against the singletons, or just log — Configure throwing on a
// second call is expected and usually safe to ignore.
}TipKitError supports value equality (==, Equals), but unlike Tips.Status it has no CaseTag discriminator (no .Tag, no TryGet* accessors) — its cases are exposed only via three static singleton properties (InvalidPredicateValueType, MissingGroupContainerEntitlements, TipsDatastoreAlreadyConfigured).
TipGroup (root-level) bundles multiple tips with a presentation priority: TipGroup.Priority (enum : int) is FirstAvailable = 0 or Ordered = 1.
-
You cannot define a
Tipin pure C#. Conforming to theTipprotocol (and its synthesized rule/parameter machinery) requires Swift. From C# you getITip(always backed by a Swift-resident proxy in practice) andAnyTip(read-only, no public ctor). Author tips in a small Swift companion target and drive everything else from C#. -
The
@Rule/Tips.Rule(...) { ... }result-builder DSL is unreachable from C#. Those entry points are@_alwaysEmitIntoClientin the Swift standard library — they're inlined into each Swift caller and export no stable ABI symbol, so there is no call target to bind. Donation-tracking, parameter rules, and event-count rules built through the DSL must be declared in Swift. (You can publish concreteTips.Rulevalues aspublic static letfrom your Swift companion; ordinary stored-property symbols bind cleanly.)Tips.Rule,Tips.RuleBuilder,Tips.ActionBuilder,Tips.OptionsBuilder, andTips.GroupBuilderare present as types but their builder methods are part of this DSL surface. -
SwiftUI views are not bound.
TipViewandPopoverTipVieware detected only as SwiftUI bridge templates that require manual completion — they are not generated as usable C# types. Use the UIKit path (TipUIPopoverViewController,TipUIView) instead. -
Several
ITipmembers are not dispatchable on Swift-backedTipProxyvalues. On aTipProxywrapping a Swift-side conforming type,Id,Title, andOptionsdispatch correctly via wrapper thunks.MessageandImagethrowNotSupportedExceptionbecause their optional SwiftUI types aren't witness-dispatchable across the existential boundary (SB0003); access them on the concrete Swift type.StatusandInvalidate(reason:)are not part of theITipinterface — they live only on the concrete conforming Swift type; expose them from your Swift companion if needed.
What does work from C#, fully: Tips.Configure / ResetDatastore / ShowAllTipsForTesting / HideAllTipsForTesting, the Tips.Status enum and its singletons, Tips.InvalidationReason, all the configuration presets, the display-frequency option types, Tips.DonationTimeRange / Tips.DonationLimit / Tips.ParameterOption, Tips.Action, the UIKit presentation types, TipKitError, and TipGroup.Priority. On a Swift-backed TipProxy, Id, Title, and Options dispatch correctly via wrapper thunks; Message and Image throw (SB0003 — reach them through the concrete Swift type).
Generated types implement ISwiftObject / IDisposable. using var is the recommended pattern for deterministic cleanup, including the configuration option value-structs (Tips.DonationLimit, Tips.IgnoresDisplayFrequency, Tips.MaxDisplayCount, Tips.MaxDisplayDuration). They are SwiftSafeHandle-backed with proper Dispose + finalizer, so the standard pattern applies — pass them to Tips.Configure(...) first, then let the using scope release them.
using var limit = new Tips.DonationLimit(100);
using var anyTip = AnyTip.FromTipKit_AnyTip(/* … */);-
Do not dispose static singletons.
Tips.Status.Pending,Tips.DonationTimeRange.Week,Tips.ConfigurationOption.DisplayFrequency.Daily, theTipKitErrorsingletons, etc. are cached — treat them as shared values. -
Configure on the main thread at launch.
Tips.Configureinitializes the persistent datastore; call it once during app startup before presenting any UI. The UIKit presentation types areUIViewController/UIViewsubclasses and must be used on the main thread like any UIKit object. -
Sendablevalue types. Several TipKit value types are marked[SwiftSendable]and may be shared across threads without external synchronization.
- Apple — TipKit — upstream documentation and full API semantics
- Apple — Displaying tips in your app
-
ActivityKit —
SwiftBindings.Apple.ActivityKitv26.2.6 -
CryptoKit —
SwiftBindings.Apple.CryptoKitv26.2.6 -
FamilyControls —
SwiftBindings.Apple.FamilyControlsv26.2.6 -
LiveCommunicationKit —
SwiftBindings.Apple.LiveCommunicationKitv26.2.6 -
Matter —
SwiftBindings.Apple.Matterv26.2.6 -
MatterSupport —
SwiftBindings.Apple.MatterSupportv26.2.6 -
MusicKit —
SwiftBindings.Apple.MusicKitv26.2.6 -
ProximityReader —
SwiftBindings.Apple.ProximityReaderv26.2.6 -
RealityFoundation —
SwiftBindings.Apple.RealityFoundationv26.2.6 -
RealityKit —
SwiftBindings.Apple.RealityKitv26.2.6 -
RoomPlan —
SwiftBindings.Apple.RoomPlanv26.2.6 -
StoreKit2 —
SwiftBindings.Apple.StoreKit2v26.2.6 -
TipKit —
SwiftBindings.Apple.TipKitv26.2.6 -
Translation —
SwiftBindings.Apple.Translationv26.2.6 -
WeatherKit —
SwiftBindings.Apple.WeatherKitv26.2.6 -
WorkoutKit —
SwiftBindings.Apple.WorkoutKitv26.2.6