TipKit w SwiftUI: kompletny przewodnik po wskazówkach onboardingowych w iOS 26

Praktyczny przewodnik po TipKit w SwiftUI na iOS 26. Konfiguracja TipsCenter, reguły eligibility, popoverTip vs TipView, grupy wskazówek i testowanie z gotowymi przykładami kodu.

TipKit SwiftUI iOS 26: Przewodnik 2026

Zaktualizowano: 25 maja 2026

TipKit to natywny framework Apple, który pokazuje kontekstowe wskazówki w aplikacjach iOS, iPadOS, macOS, watchOS i tvOS bez budowania własnego systemu onboardingowego. W iOS 26 framework oferuje deklaratywne API dla SwiftUI, reguły eligibility oparte na zdarzeniach i parametrach użytkownika, a także automatyczną synchronizację stanu między urządzeniami przez iCloud. Ten przewodnik pokazuje, jak zaimplementować TipKit w SwiftUI krok po kroku: od konfiguracji TipsCenter, przez definiowanie reguł, po grupowanie wskazówek i testowanie w trakcie developmentu.

  • TipKit działa od iOS 17, lecz w iOS 26 wprowadza nowy modyfikator popoverTip(_:arrowEdge:) i lepszą integrację z Liquid Glass.
  • Każda wskazówka to typ implementujący protokół Tip z polami title, message, image i opcjonalnymi actions.
  • Tips.configure() w App.init() jest wymagane do uruchomienia silnika reguł i przechowywania stanu na dysku.
  • Reguły #Rule bazują na Tips.Event oraz Tips.Parameter i pozwalają wyświetlać podpowiedź po spełnieniu warunków behawioralnych.
  • TipKit ma natywne integracje SwiftUI (.popoverTip, TipView) oraz odpowiedniki w UIKit (TipUIPopoverViewController).
  • Grupy wskazówek (TipGroup) gwarantują, że tylko jedna podpowiedź z sekwencji jest aktywna naraz.

Czym jest TipKit i do czego służy?

TipKit to wprowadzony na WWDC 2023 framework, który ujednolica sposób pokazywania kontekstowych wskazówek w ekosystemie Apple. Zamiast ręcznie tworzyć alerty, popovery czy banery onboardingowe, deweloper opisuje wskazówkę jako wartościowy typ implementujący protokół Tip, a system sam decyduje, kiedy ją pokazać i jak długo trzymać widoczną. W iOS 26 framework jest w pełni zintegrowany z systemem Liquid Glass, więc popovery automatycznie dziedziczą efekty głębi i kolorystyki bez dodatkowego kodu.

Najważniejszą wartością TipKit jest silnik reguł. Możesz powiedzieć systemowi: „pokaż tę wskazówkę dopiero, gdy użytkownik trzykrotnie otworzył ekran, ale nigdy nie kliknął przycisku eksportu". Cała ta logika jest deklaratywna, a stan reguł jest persystowany i opcjonalnie synchronizowany przez iCloud. Dzięki temu wskazówka raz odrzucona przez użytkownika nie pojawia się na drugim urządzeniu. Dla zespołów product growth oznacza to mniej kodu i jeden spójny system pomiaru.

TipKit jest też świadomy Human Interface Guidelines: ogranicza częstotliwość wyświetlania (domyślnie jedna wskazówka co 24 godziny w skali aplikacji), respektuje ustawienia dostępności i pozwala użytkownikowi „wyciszyć" daną podpowiedź globalnie z poziomu Ustawień systemu w iOS 26. Zanim sięgniesz po custom overlay, zawsze sprawdź, czy TipKit nie pokrywa twojego scenariusza. Szczerze, w ostatnim projekcie zaoszczędziło mi to dwa sprinty pracy nad własnym systemem onboardingu.

Konfiguracja TipsCenter w aplikacji SwiftUI

Pierwszym krokiem jest wywołanie Tips.configure() przy starcie aplikacji. Bez tej linii silnik reguł nie wystartuje i żadna wskazówka nie zostanie wyświetlona. Konfigurację najczęściej umieszcza się w init() głównej struktury App:

import SwiftUI
import TipKit

@main
struct CrafterApp: App {
    init() {
        do {
            try Tips.configure([
                .displayFrequency(.immediate),
                .datastoreLocation(.applicationDefault)
            ])
        } catch {
            print("Nie udało się skonfigurować TipKit: \(error)")
        }
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Dwa parametry warto zrozumieć od razu. displayFrequency kontroluje, jak często TipKit pozwala globalnie pokazać jakąkolwiek wskazówkę. Wartość .immediate jest świetna podczas developmentu, ale w produkcji rozsądny jest .daily, by nie męczyć użytkownika. datastoreLocation wskazuje, gdzie przechowywać stan reguł (domyślnie w katalogu Application Support), ale można wymusić własną lokalizację, np. współdzielony App Group dla rozszerzeń.

Jeśli chcesz włączyć synchronizację z iCloud, dodaj uprawnienie CloudKit do projektu i wywołaj Tips.configure(...) z opcją .cloudKitContainer(.named("iCloud.com.firma.app")). TipKit automatycznie zarządza rekordami i konfliktami, więc programista nie musi pisać własnej logiki synchronizacji. Więcej szczegółów znajdziesz w oficjalnej dokumentacji TipKit.

Tworzenie pierwszej wskazówki TipKit

Wskazówka w TipKit to po prostu struktura zgodna z protokołem Tip. Minimalna implementacja wymaga jedynie tytułu, choć w praktyce zawsze dodaje się komunikat i ikonę:

import TipKit

struct FavoriteRecipeTip: Tip {
    var title: Text {
        Text("Zapisz przepis na później")
    }

    var message: Text? {
        Text("Stuknij ikonę gwiazdki, aby dodać przepis do ulubionych.")
    }

    var image: Image? {
        Image(systemName: "star.fill")
    }

    var actions: [Action] {
        Action(id: "learn-more", title: "Dowiedz się więcej")
    }
}

Następnie tworzysz instancję wskazówki i pokazujesz ją w widoku SwiftUI. W iOS 26 najwygodniejszym sposobem jest modyfikator .popoverTip(_:arrowEdge:), który dba o pozycjonowanie i animację:

struct RecipeDetailView: View {
    private let favoriteTip = FavoriteRecipeTip()

    var body: some View {
        HStack {
            Text("Tarta cytrynowa")
            Spacer()
            Button {
                toggleFavorite()
            } label: {
                Image(systemName: "star")
            }
            .popoverTip(favoriteTip, arrowEdge: .top)
        }
    }

    private func toggleFavorite() {
        favoriteTip.invalidate(reason: .actionPerformed)
    }
}

Po wywołaniu invalidate(reason:) system zapamiętuje, że użytkownik wykonał oczekiwaną akcję i nie pokaże tej wskazówki ponownie. Inne wartości InvalidationReason to .userPerformedAction, .tipClosed i .displayCountExceeded. Jeżeli pracujesz z bardziej reaktywnymi modelami stanu, warto zerknąć na nasz praktyczny przewodnik po migracji na @Observable, bo TipKit świetnie współgra z nowym makro @Observable.

Reguły eligibility, zdarzenia i parametry

Prawdziwa siła TipKit ujawnia się przy regułach. Każda wskazówka może zdefiniować listę warunków, które muszą być spełnione, zanim system w ogóle rozważy jej pokazanie. Reguły bazują na dwóch konstruktach: zdarzeniach (Tips.Event) zliczanych w czasie i parametrach (Tips.Parameter) trzymających stałą wartość.

struct ExportTip: Tip {
    static let appOpenEvent = Event(id: "app-open")
    static let isPremium = Parameter<Bool>("is-premium", false)

    var title: Text { Text("Eksportuj jako PDF") }
    var message: Text? { Text("Premium pozwala wyeksportować pełen dokument w jednym kliknięciu.") }

    var rules: [Rule] {
        #Rule(Self.appOpenEvent) { event in
            event.donations.count >= 3
        }
        #Rule(Self.isPremium) { $0 == true }
    }
}

Aby zarejestrować zdarzenie, wywołaj await ExportTip.appOpenEvent.donate() w odpowiednim miejscu (np. w task głównego widoku). Parametry ustawia się raz, gdy zmienia się stan: ExportTip.isPremium.wrappedValue = user.hasPremium. Silnik TipKit nasłuchuje zmian i automatycznie aktywuje wskazówkę, gdy wszystkie reguły są spełnione.

Reguły mogą być też ograniczone czasowo. Wyrażenie event.donations.filter { $0.date > .now.addingTimeInterval(-7 * 24 * 3600) }.count >= 5 oznacza „użytkownik wykonał akcję co najmniej pięć razy w ostatnim tygodniu". Tego rodzaju targetowanie eliminuje konieczność integracji z zewnętrznym narzędziem A/B testowym dla prostych scenariuszy growth.

popoverTip vs TipView: kiedy używać którego?

SwiftUI udostępnia dwie podstawowe metody renderowania wskazówek: popover (.popoverTip) i inline (TipView). Wybór zależy od kontekstu i ilości miejsca na ekranie.

CechapopoverTipTipView
Sposób prezentacjiFloating popover ze strzałkąWbudowana karta w hierarchii widoków
Najlepsze zastosowanieWskazanie konkretnej kontrolki (przycisk, ikona)Wyjaśnienie nowej sekcji ekranu lub listy
Wymagana szerokośćMin. ~120 ptMin. ~280 pt
Zakrywa treśćTak, czasowoNie, zajmuje stałe miejsce w layoucie
AnimacjaSkalowanie + fadeSlide w hierarchii
Dobre dla iPadaBardzo dobreLepsze dla list w sidebarach

W praktyce popoverTip dominuje przy onboardingu kontrolnym (zwróć uwagę na ten przycisk), a TipView sprawdza się jako baner edukacyjny na górze listy. Łatwo je mieszać, ale używaj jednego rodzaju na ekran, by nie przytłaczać użytkownika. Jeśli chcesz pokazać wskazówkę w pełnoekranowym sheet, owijaj TipView w sekcję Section w List, żeby zachować spójność wizualną. Zauważyłem, że mieszanie obu typów na jednym widoku praktycznie zawsze rodzi konflikty pozycjonowania na mniejszych iPhone'ach.

Grupy wskazówek i kolejkowanie

Gdy w aplikacji jest wiele wskazówek dla powiązanych funkcji, łatwo wpaść w pułapkę pokazywania ich wszystkich naraz. TipKit rozwiązuje ten problem za pomocą TipGroup. Grupa zapewnia, że tylko jedna podpowiedź z listy jest aktywna w danym momencie, a kolejne pojawią się dopiero po inwalidacji poprzedniej.

@TipGroup var onboardingGroup: TipGroup<any Tip> {
    WelcomeTip()
    FavoriteRecipeTip()
    ExportTip()
}

struct HomeView: View {
    @TipGroup var group: TipGroup<any Tip> {
        WelcomeTip()
        FavoriteRecipeTip()
        ExportTip()
    }

    var body: some View {
        VStack {
            HeaderView()
                .popoverTip(group.currentTip)
            RecipeList()
        }
    }
}

Grupy wspierają również priorytet. Wskazówki są wyświetlane w kolejności zadeklarowanej, więc nawet jeśli trzy są jednocześnie „uprawnione", system wybierze pierwszą z grupy i wstrzyma pozostałe. Dla bardziej dynamicznych scenariuszy growth warto połączyć TipKit z App Intents w iOS 26, by aktywować odpowiednią podpowiedź po wykonaniu komendy Siri.

TipKit w UIKit: integracja z UIViewController

Wbrew popularnemu przekonaniu TipKit nie jest frameworkiem wyłącznie dla SwiftUI. W aplikacjach UIKit używasz TipUIPopoverViewController dla popoverów oraz TipUIView dla inline. Wzorzec jest niemal identyczny, lecz wymaga ręcznej obserwacji stanu wskazówki:

import UIKit
import TipKit

final class RecipeDetailViewController: UIViewController {
    private let favoriteTip = FavoriteRecipeTip()
    private var tipObservation: Task<Void, Never>?

    override func viewDidLoad() {
        super.viewDidLoad()
        tipObservation = Task { @MainActor in
            for await shouldDisplay in favoriteTip.shouldDisplayUpdates {
                if shouldDisplay {
                    let controller = TipUIPopoverViewController(
                        favoriteTip,
                        sourceItem: favoriteButton
                    )
                    present(controller, animated: true)
                } else if presentedViewController is TipUIPopoverViewController {
                    dismiss(animated: true)
                }
            }
        }
    }
}

Mechanizm shouldDisplayUpdates jest sekwencją AsyncSequence zwracającą zmiany eligibility w czasie rzeczywistym. Dzięki temu nawet w UIKit nie musisz pisać własnych obserwatorów NotificationCenter. Pamiętaj o anulowaniu Task w deinit, by uniknąć wycieków pamięci (na tym sam się sparzyłem przy migracji jednego z ekranów). Jeśli dopiero portujesz aplikację z UIKit, warto przejrzeć nasz przewodnik po Approachable Concurrency w Swift 6.2, bo TipKit silnie polega na nowym modelu współbieżności.

Jak testować TipKit podczas developmentu?

Najczęstsze pytanie nowych użytkowników brzmi: „dlaczego moja wskazówka się nie pokazuje?". TipKit jest celowo restrykcyjny, a w czasie developmentu zwykle chcesz to nadpisać. Apple udostępnia klasę Tips z kilkoma metodami pomocniczymi:

#if DEBUG
Tips.showAllTipsForTesting()
Tips.resetDatastore()
Tips.hideTip(FavoriteRecipeTip.id)
#endif

showAllTipsForTesting() ignoruje reguły i częstotliwość, więc każda wskazówka pojawi się natychmiast. resetDatastore() czyści cały stan, co przydaje się przy testach instalacji „od zera". W Xcode 26 można też ustawić zmienną środowiskową TIPKIT_SHOW_ALL w schemacie debugowym, co zwalnia z konieczności modyfikowania kodu.

Do testów jednostkowych użyj swojego Tips.configure(_:) z parametrem .datastoreLocation(.url(temporaryDirectoryURL)) w setUp() i przywróć domyślną wartość po teście. W ten sposób testy nie zaśmiecają katalogu aplikacji deweloperskiej. Dla pełnej automatyzacji warto połączyć to z naszym przewodnikiem po Swift Testing, by korzystać z parametryzowanych testów reguł.

Najczęstsze błędy i jak ich uniknąć

Pierwszy problem to brak wywołania Tips.configure() przed pierwszym renderowaniem TipView. W praktyce wskazówka po prostu się nie pokazuje, a w konsoli pojawia się ostrzeżenie. Zawsze umieszczaj konfigurację w init() typu App, a nie w onAppear widoku. Wtedy moment startu jest zbyt późny dla pierwszego widoku.

Drugi częsty błąd to trzymanie instancji Tip jako lokalnej zmiennej w body. Każde przebudowanie widoku tworzy wtedy nowy obiekt i resetuje wewnętrzny licznik. Rozwiązanie? Deklaruj wskazówkę jako private let w strukturze widoku albo trzymaj ją w obiekcie zarządzającym stanem (np. w @Observable serwisie). Trafiłem na ten bug shipując pierwszą wersję onboardingu i dwa dni szukałem przyczyny.

Trzeci błąd dotyczy pomijania invalidate(reason:). Jeśli użytkownik wykonał akcję, ale system nie został o tym poinformowany, wskazówka pojawi się ponownie przy kolejnym uruchomieniu. Inwalidacja po akcji to zasada numer jeden. Pełna lista reasonów znajduje się w dokumentacji InvalidationReason.

Czwarty, subtelny problem to konflikt z NavigationStack. Popover otwarty w widoku, który zaraz zostanie zdejmowany, może spowodować awarię na iPadzie. Rozwiązaniem jest sprawdzenie presentationMode lub użycie .popoverTip wewnątrz stabilnej hierarchii, np. toolbar. Apple aktywnie pracuje nad tym scenariuszem (pełna lista poprawek znajduje się w release notes iOS 26).

Najczęściej zadawane pytania

Od jakiej wersji iOS działa TipKit?

TipKit jest dostępny od iOS 17, iPadOS 17, macOS Sonoma 14, watchOS 10 i tvOS 17. W iOS 26 framework otrzymał odświeżone API, w tym modyfikator popoverTip(_:arrowEdge:), lepszą integrację z Liquid Glass oraz wbudowane reguły grup. Starsze wersje wymagają fallbacku do własnych komponentów.

Czy TipKit można używać w UIKit?

Tak. Apple dostarcza TipUIPopoverViewController oraz TipUIView dla aplikacji UIKit. Mechanizm reguł i datastore jest identyczny jak w SwiftUI; różnica polega na ręcznym zarządzaniu prezentacją kontrolera i obserwacji shouldDisplayUpdates.

Jak sprawić, by wskazówka pokazała się tylko raz?

Wywołaj tip.invalidate(reason: .actionPerformed) po wykonaniu przez użytkownika oczekiwanej akcji. TipKit zapisze ten stan w datastore i nie wyświetli tej samej wskazówki ponownie, nawet po restarcie aplikacji. Można też ustawić maksymalną liczbę wyświetleń w Options implementowanych w typie Tip.

Jak testować TipKit lokalnie bez czekania na reguły?

Użyj Tips.showAllTipsForTesting() w bloku #if DEBUG, aby zignorować reguły i częstotliwość. Do czyszczenia stanu między uruchomieniami użyj Tips.resetDatastore(). W Xcode 26 możesz też ustawić zmienną środowiskową TIPKIT_SHOW_ALL w schemacie debugowym.

Czy TipKit synchronizuje stan między urządzeniami?

Tak, opcjonalnie. Dodaj uprawnienie CloudKit i przekaż .cloudKitContainer(.named("iCloud.com.example")) do Tips.configure(...). Wskazówki odrzucone lub spełnione na iPhonie nie pojawią się ponownie na iPadzie tego samego użytkownika. Dla aplikacji multi-device to znacząco poprawia UX.

O Autorze Tomasz Wojcik

Tomasz is a Krakow-based iOS engineer with 11 years of Swift experience. He spent four years at Revolut on the Wealth team, where he rewrote the trading charts in SwiftUI and shaved 40% off cold-start time by lazy-loading the analytics SDK. Before Revolut he was at Allegro, Poland's largest e-commerce platform, on the Seller Center iOS team. His specialty is iOS performance work: Instruments deep-dives, memory-graph debugging, and figuring out why your scroll view drops frames only on iPhone SE 2nd-gen. He has contributed patches to swift-syntax and writes a quarterly newsletter for iOS engineers that covers under-discussed APIs like BackgroundTasks and NSFileCoordinator. Tomasz holds the iOS App Development with Swift certification from Apple and occasionally runs paid workshops on Swift concurrency for in-house engineering teams in Europe.