All skills

Code generator skills that produce production-ready Swift code for common app components. Use when user wants to add logging, analytics, onboarding, review prompts, networking, authentication, paywalls, settings, persistence, error monitoring, CI/CD pipelines, localization, push notifications, deep linking, testing, accessibility, widgets, feature flags, app icons, image caching, pagination, HTTP caching, share cards, social export, subscription lifecycle, referral systems, watermarks, streak tracking, milestone celebrations, what's new screens, lapsed user re-engagement, usage insights, variable rewards, consent flows, account deletion, permission priming, force updates, state restoration, debug menus, offline queues, feedback forms, announcement banners, quick win sessions, Spotlight indexing, App Clips, screenshot automation, background processing, app extensions, data export, or SwiftUI preview sample data and variant matrices.

Use this Skill: https://skilld.dev/gh/rshankras/claude-code-apple-skills/generators

This session only. Nothing lands on disk.

tipkit-generatortemplates.md

≈5k tokens on demand. Your agent reads this file only when SKILL.md points to it.

TipKit Code Templates

Tips.configure() Setup

Basic Configuration

import TipKit

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .task {
                    try? Tips.configure([
                        .displayFrequency(.daily),
                        .datastoreLocation(.applicationDefault)
                    ])
                }
        }
    }
}

Configuration with Testing Support

import TipKit

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .task {
                    #if DEBUG
                    // Reset tips on every launch during development
                    try? Tips.resetDatastore()
                    #endif

                    try? Tips.configure([
                        .displayFrequency(.daily),
                        .datastoreLocation(.applicationDefault)
                    ])
                }
        }
    }
}

Display Frequency Options

// Tips can show as soon as eligible
.displayFrequency(.immediate)

// At most one tip per hour
.displayFrequency(.hourly)

// At most one tip per day (recommended for most apps)
.displayFrequency(.daily)

// At most one tip per week
.displayFrequency(.weekly)

// At most one tip per month
.displayFrequency(.monthly)

DataStore Location Options

// Default location managed by the system
.datastoreLocation(.applicationDefault)

// Custom URL (useful for isolating test data)
.datastoreLocation(.url(FileManager.default
    .urls(for: .cachesDirectory, in: .userDomainMask)
    .first!
    .appending(path: "tips-store")))

Tip Protocol Conformance

Minimal Tip (Title Only)

import TipKit

struct SearchTip: Tip {
    var title: Text {
        Text("Try searching")
    }
}

Tip with Title and Message

import TipKit

struct SearchTip: Tip {
    var title: Text {
        Text("Search your library")
    }

    var message: Text? {
        Text("Quickly find any item by name, tag, or date.")
    }
}

Tip with Title, Message, and Image

import TipKit

struct SearchTip: Tip {
    var title: Text {
        Text("Search your library")
    }

    var message: Text? {
        Text("Quickly find any item by name, tag, or date.")
    }

    var image: Image? {
        Image(systemName: "magnifyingglass")
    }
}

Tip with Actions

import TipKit

struct UpgradeTip: Tip {
    var title: Text {
        Text("Unlock more features")
    }

    var message: Text? {
        Text("Upgrade to Pro for unlimited access.")
    }

    var actions: [Action] {
        Action(id: "learn-more", title: "Learn More")
        Action(id: "dismiss", title: "Not Now")
    }
}

Handling actions in the view:

TipView(upgradeTip) { action in
    if action.id == "learn-more" {
        showUpgradeSheet = true
    }
    // Tip auto-dismisses after any action tap
}

Parameter-Based Rules

Boolean Parameter

import TipKit

struct AdvancedSearchTip: Tip {
    @Parameter
    static var hasUsedBasicSearch: Bool = false

    var title: Text {
        Text("Try advanced search")
    }

    var message: Text? {
        Text("Use filters to narrow your results.")
    }

    var rules: [Rule] {
        #Rule(Self.$hasUsedBasicSearch) { $0 == true }
    }
}

Setting the parameter from elsewhere in the app:

func onBasicSearchPerformed() {
    AdvancedSearchTip.hasUsedBasicSearch = true
}

Numeric Parameter

import TipKit

struct PowerUserTip: Tip {
    @Parameter
    static var itemsCreated: Int = 0

    var title: Text {
        Text("You are on a roll")
    }

    var message: Text? {
        Text("Try using templates to create items even faster.")
    }

    var rules: [Rule] {
        #Rule(Self.$itemsCreated) { $0 >= 5 }
    }
}

Incrementing the parameter:

func onItemCreated() {
    PowerUserTip.itemsCreated += 1
}

Event-Based Rules

Basic Event Rule

import TipKit

struct FilterTip: Tip {
    static let listViewed = Tips.Event(id: "listViewed")

    var title: Text {
        Text("Filter your results")
    }

    var message: Text? {
        Text("Tap the filter icon to narrow down what you see.")
    }

    var rules: [Rule] {
        #Rule(Self.listViewed) { $0.donations.count >= 3 }
    }
}

Donating the event:

struct ListView: View {
    var body: some View {
        List { /* ... */ }
            .onAppear {
                FilterTip.listViewed.donate()
            }
    }
}

Event with Associated Value

import TipKit

struct ShareTip: Tip {
    static let photoViewed = Tips.Event(id: "photoViewed")

    var title: Text {
        Text("Share this photo")
    }

    var message: Text? {
        Text("Tap the share button to send this to friends.")
    }

    var rules: [Rule] {
        #Rule(Self.photoViewed) {
            $0.donations.count >= 2
        }
    }
}

Combining Multiple Rules

Parameter + Event

import TipKit

struct ExportTip: Tip {
    @Parameter
    static var hasCreatedDocument: Bool = false

    static let documentEdited = Tips.Event(id: "documentEdited")

    var title: Text {
        Text("Export your work")
    }

    var message: Text? {
        Text("Tap Export to save as PDF or share with others.")
    }

    var image: Image? {
        Image(systemName: "square.and.arrow.up")
    }

    var rules: [Rule] {
        #Rule(Self.$hasCreatedDocument) { $0 == true }
        #Rule(Self.documentEdited) { $0.donations.count >= 2 }
    }
}

All rules must be satisfied (logical AND) for the tip to become eligible.


Tip Options

MaxDisplayCount

import TipKit

struct SwipeToDeleteTip: Tip {
    var title: Text {
        Text("Swipe to delete")
    }

    var message: Text? {
        Text("Swipe left on any item to delete it.")
    }

    // Tip will be shown at most 3 times across app sessions
    var options: [TipOption] {
        MaxDisplayCount(3)
    }
}

IgnoresDisplayFrequency

import TipKit

struct CriticalTip: Tip {
    var title: Text {
        Text("Important update")
    }

    var message: Text? {
        Text("Your data has been migrated. Review your settings.")
    }

    // This tip ignores the global .displayFrequency setting
    var options: [TipOption] {
        IgnoresDisplayFrequency(true)
    }
}

Combining Options

var options: [TipOption] {
    MaxDisplayCount(5)
    IgnoresDisplayFrequency(true)
}

Centralized Event Definitions

When multiple tips share events, centralize them:

// TipEvents.swift
import TipKit

enum TipEvents {
    static let appLaunched = Tips.Event(id: "appLaunched")
    static let listViewed = Tips.Event(id: "listViewed")
    static let itemCreated = Tips.Event(id: "itemCreated")
    static let searchPerformed = Tips.Event(id: "searchPerformed")
}

Reference from tip structs:

struct FilterTip: Tip {
    var title: Text {
        Text("Filter your results")
    }

    var rules: [Rule] {
        #Rule(TipEvents.listViewed) { $0.donations.count >= 3 }
    }
}

Inline Tips (TipView)

Basic Inline Tip

import SwiftUI
import TipKit

struct SearchView: View {
    let searchTip = SearchTip()

    var body: some View {
        VStack {
            TipView(searchTip)
            SearchBar()
            ResultsList()
        }
    }
}

Inline Tip with Custom Arrow Direction

TipView(searchTip, arrowEdge: .bottom)

Inline Tip with Action Handling

TipView(upgradeTip) { action in
    switch action.id {
    case "learn-more":
        showUpgradeSheet = true
    case "dismiss":
        break // Tip auto-dismisses
    default:
        break
    }
}

Popover Tips

Basic Popover

import SwiftUI
import TipKit

struct ToolbarView: View {
    let filterTip = FilterTip()

    var body: some View {
        HStack {
            Spacer()
            Button("Filter", systemImage: "line.3.horizontal.decrease.circle") {
                showFilters.toggle()
            }
            .popoverTip(filterTip)
        }
    }
}

Popover with Custom Arrow Direction

Button("Sort") {
    // action
}
.popoverTip(sortTip, arrowEdge: .top)

Tip Invalidation

Invalidate on Action Performed

struct SearchView: View {
    let searchTip = SearchTip()

    var body: some View {
        VStack {
            TipView(searchTip)
            SearchBar(onSearch: { query in
                performSearch(query)
                searchTip.invalidate(reason: .actionPerformed)
            })
        }
    }
}

Invalidation Reasons

// User performed the action the tip describes
tip.invalidate(reason: .actionPerformed)

// Tip was shown enough times (reached MaxDisplayCount)
// This happens automatically -- you do not call this manually

// User closed the tip via the X button
// This happens automatically when the user taps dismiss

Invalidate from Elsewhere

Hold a reference to the same tip instance, or define a static method:

struct SearchTip: Tip {
    var title: Text {
        Text("Try searching")
    }

    // Call this when the feature is discovered
    static func markAsDiscovered() {
        // Create a temporary instance to invalidate all displays of this tip
        SearchTip().invalidate(reason: .actionPerformed)
    }
}

TipGroup (Ordered Tips)

Basic TipGroup

import SwiftUI
import TipKit

struct OnboardingTipsView: View {
    @State var tipGroup = TipGroup(.ordered) {
        CreateItemTip()
        EditItemTip()
        ShareItemTip()
    }

    var body: some View {
        VStack {
            // Only the current tip in the group is shown
            TipView(tipGroup.currentTip!)
        }
    }
}

TipGroup with Priority Ordering

// .ordered: tips appear in the order listed, one at a time
// Each tip must be dismissed or invalidated before the next appears
@State var tipGroup = TipGroup(.ordered) {
    WelcomeTip()       // Shows first
    SearchTip()        // Shows second, after WelcomeTip is dismissed
    FilterTip()        // Shows third
}

TipGroup in a View

struct ContentView: View {
    @State var tipGroup = TipGroup(.ordered) {
        WelcomeTip()
        SearchTip()
        FilterTip()
    }

    var body: some View {
        VStack {
            if let currentTip = tipGroup.currentTip {
                TipView(currentTip)
            }

            MainContent()
        }
    }
}

Testing Utilities

TipsConfiguration Helper

// TipsConfiguration.swift
import TipKit

enum TipsConfiguration {
    /// Call once at app startup
    static func configure(frequency: Tips.ConfigurationOption.DisplayFrequency = .daily) {
        try? Tips.configure([
            .displayFrequency(frequency),
            .datastoreLocation(.applicationDefault)
        ])
    }

    /// Show all tips regardless of rules and frequency (debug only)
    static func showAllForTesting() {
        Tips.showAllTipsForTesting()
    }

    /// Show only specific tips for testing
    static func showForTesting(_ tips: [any Tip.Type]) {
        Tips.showTipsForTesting(tips)
    }

    /// Reset all tip state (debug only)
    static func resetAll() {
        try? Tips.resetDatastore()
    }
}

Debug Menu Integration

#if DEBUG
struct TipsDebugMenu: View {
    var body: some View {
        Section("Tips (Debug)") {
            Button("Reset All Tips") {
                TipsConfiguration.resetAll()
            }
            Button("Show All Tips") {
                TipsConfiguration.showAllForTesting()
            }
            Button("Show Search Tip Only") {
                TipsConfiguration.showForTesting([SearchTip.self])
            }
        }
    }
}
#endif

SwiftUI Preview Support

#Preview {
    SearchView()
        .task {
            try? Tips.resetDatastore()
            Tips.showAllTipsForTesting()
            try? Tips.configure()
        }
}

Complete Example: Feature Discovery Flow

This example shows a complete tip setup for an app with search, filter, and export features.

Tip Definitions

// SearchTip.swift
import TipKit

struct SearchTip: Tip {
    static let appOpened = Tips.Event(id: "appOpened")

    var title: Text {
        Text("Search your items")
    }

    var message: Text? {
        Text("Use the search bar to quickly find what you need.")
    }

    var image: Image? {
        Image(systemName: "magnifyingglass")
    }

    var rules: [Rule] {
        #Rule(Self.appOpened) { $0.donations.count >= 2 }
    }
}
// FilterTip.swift
import TipKit

struct FilterTip: Tip {
    @Parameter
    static var hasSearched: Bool = false

    var title: Text {
        Text("Narrow your results")
    }

    var message: Text? {
        Text("Tap the filter icon to show only what matters.")
    }

    var image: Image? {
        Image(systemName: "line.3.horizontal.decrease.circle")
    }

    var rules: [Rule] {
        #Rule(Self.$hasSearched) { $0 == true }
    }

    var options: [TipOption] {
        MaxDisplayCount(3)
    }
}
// ExportTip.swift
import TipKit

struct ExportTip: Tip {
    @Parameter
    static var hasFilteredResults: Bool = false

    static let itemViewed = Tips.Event(id: "itemViewed")

    var title: Text {
        Text("Export your results")
    }

    var message: Text? {
        Text("Share or save your filtered results as a PDF.")
    }

    var image: Image? {
        Image(systemName: "square.and.arrow.up")
    }

    var rules: [Rule] {
        #Rule(Self.$hasFilteredResults) { $0 == true }
        #Rule(Self.itemViewed) { $0.donations.count >= 3 }
    }
}

View Integration

// ContentView.swift
import SwiftUI
import TipKit

struct ContentView: View {
    let searchTip = SearchTip()
    let filterTip = FilterTip()
    let exportTip = ExportTip()

    @State private var searchText = ""
    @State private var showFilters = false

    var body: some View {
        NavigationStack {
            VStack {
                // Inline tip above search bar
                TipView(searchTip)

                SearchBar(text: $searchText, onCommit: {
                    searchTip.invalidate(reason: .actionPerformed)
                    FilterTip.hasSearched = true
                })

                ResultsList(searchText: searchText)
            }
            .toolbar {
                ToolbarItem(placement: .primaryAction) {
                    Button("Filter", systemImage: "line.3.horizontal.decrease.circle") {
                        showFilters.toggle()
                        filterTip.invalidate(reason: .actionPerformed)
                        ExportTip.hasFilteredResults = true
                    }
                    .popoverTip(filterTip)
                }

                ToolbarItem(placement: .secondaryAction) {
                    Button("Export", systemImage: "square.and.arrow.up") {
                        exportResults()
                        exportTip.invalidate(reason: .actionPerformed)
                    }
                    .popoverTip(exportTip)
                }
            }
            .onAppear {
                SearchTip.appOpened.donate()
            }
        }
    }

    private func exportResults() {
        // Export logic
    }
}

App Entry Point

// MyApp.swift
import SwiftUI
import TipKit

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .task {
                    #if DEBUG
                    try? Tips.resetDatastore()
                    #endif

                    try? Tips.configure([
                        .displayFrequency(.daily),
                        .datastoreLocation(.applicationDefault)
                    ])
                }
        }
    }
}

Patterns: Good vs Bad

Tip Definition

// ✅ Good: One tip per file, clear naming, meaningful image
struct SearchTip: Tip {
    var title: Text { Text("Search your library") }
    var message: Text? { Text("Find items by name, tag, or date.") }
    var image: Image? { Image(systemName: "magnifyingglass") }
}
// ❌ Bad: Multiple tips crammed into one file with vague names
struct Tips {
    struct Tip1: Tip { /* ... */ }
    struct Tip2: Tip { /* ... */ }
    struct Tip3: Tip { /* ... */ }
}

Tips.configure() Placement

// ✅ Good: Configure early in the app lifecycle
@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .task {
                    try? Tips.configure([
                        .displayFrequency(.daily),
                        .datastoreLocation(.applicationDefault)
                    ])
                }
        }
    }
}
// ❌ Bad: Configure inside a deeply nested view (may run too late or multiple times)
struct SomeNestedView: View {
    var body: some View {
        Text("Hello")
            .task {
                try? Tips.configure()  // Too late, may miss early tips
            }
    }
}

Display Frequency

// ✅ Good: Use .daily or .weekly for production to avoid overwhelming users
.displayFrequency(.daily)
// ❌ Bad: Using .immediate in production -- users see every tip at once
.displayFrequency(.immediate)

Tip Invalidation

// ✅ Good: Invalidate the tip instance that is currently displayed
struct SearchView: View {
    let searchTip = SearchTip()

    var body: some View {
        VStack {
            TipView(searchTip)
            Button("Search") {
                performSearch()
                searchTip.invalidate(reason: .actionPerformed)
            }
        }
    }
}
// ❌ Bad: Creating a new instance to invalidate (does not affect the displayed tip)
Button("Search") {
    performSearch()
    SearchTip().invalidate(reason: .actionPerformed)  // New instance -- no effect on displayed tip
}

Parameter Updates

// ✅ Good: Update parameter at the point where the condition becomes true
func onSearchPerformed() {
    FilterTip.hasSearched = true
}
// ❌ Bad: Setting the parameter in the tip's own init (defeats the purpose of rules)
struct FilterTip: Tip {
    @Parameter static var hasSearched: Bool = false
    init() { Self.hasSearched = true }  // Always true -- rule is pointless
}

Event Donation

// ✅ Good: Donate events at natural user interaction points
List(items) { item in
    ItemRow(item: item)
}
.onAppear {
    FilterTip.listViewed.donate()
}
// ❌ Bad: Donating events in a timer or background task (inflates counts artificially)
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
    FilterTip.listViewed.donate()  // Fires every second -- meaningless
}

TipGroup Usage

// ✅ Good: Use TipGroup when tips should appear in a logical learning sequence
@State var onboardingTips = TipGroup(.ordered) {
    WelcomeTip()
    CreateItemTip()
    ShareItemTip()
}

var body: some View {
    VStack {
        if let tip = onboardingTips.currentTip {
            TipView(tip)
        }
        MainContent()
    }
}
// ❌ Bad: Manually tracking tip ordering with state variables
@State private var currentTipIndex = 0
// Then manually showing/hiding tips based on index -- fragile and error-prone

Source: SKILL.md on GitHub

1 alert16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The 'generators' skill provides a robust set of templates and workflows for generating Swift code for iOS and macOS applications. Security analysis identified a surface for indirect prompt injection because the skill reads local project files to adapt its generation logic. It also utilizes dynamic execution by generating and running local Swift scripts to create application icons and uses shell commands to process images and audit project code. All external references are to well-known, established services.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: MEDIUM · 1 issue

  • Runlayer7mo

    59/179 files flagged

  • ZeroLeaks5mo

    Scan incomplete

Signed by skilld at 30e898f. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Steadyupdated 2 months ago
What it can do
Reads files Edits files Runs commands
last_verified
2026-07-16
review_by
2027-06-22
os_version
iOS 27 / macOS 27
All 7 allowed tools
ReadWriteEditGlobGrepBashAskUserQuestion

README badge

README badge for rshankras/claude-code-apple-skills/generators