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 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ę:
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.
Cecha
popoverTip
TipView
Sposób prezentacji
Floating popover ze strzałką
Wbudowana karta w hierarchii widoków
Najlepsze zastosowanie
Wskazanie konkretnej kontrolki (przycisk, ikona)
Wyjaśnienie nowej sekcji ekranu lub listy
Wymagana szerokość
Min. ~120 pt
Min. ~280 pt
Zakrywa treść
Tak, czasowo
Nie, zajmuje stałe miejsce w layoucie
Animacja
Skalowanie + fade
Slide w hierarchii
Dobre dla iPada
Bardzo dobre
Lepsze 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:
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.
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.
Praktyczny przewodnik po frameworku App Intents w iOS 26. Pokazujemy, jak budować AppIntent, AppShortcut, AppEntity oraz nowość WWDC25 — Interactive Snippets, czyli interaktywne widoki SwiftUI w Siri i Shortcuts bez otwierania aplikacji.
Xcode 26.3 wprowadza kodowanie agentowe — AI, który nie tylko pisze kod, ale kompiluje, testuje i weryfikuje UI. Dowiedz się, jak skonfigurować Claude Agent, MCP i Skills, żeby pracować szybciej ze Swift i SwiftUI.
Przewodnik po makrach w Swift — od pierwszego makra freestanding po zaawansowane attached macros. Twórz, testuj i debuguj makra z SwiftSyntax, eliminując boilerplate w projektach iOS.