TipKit в SwiftUI: подсказки, правила, события и accessibility в iOS 26
TipKit в SwiftUI: как показать подсказки popover и inline, задать правила и события, донатить активность пользователя, тестировать и не сломать accessibility. Полный гайд для iOS 26 с примерами кода.
TipKit в SwiftUI это фреймворк Apple для показа контекстных подсказок, помогающих пользователю обнаружить неочевидные функции приложения. Начиная с iOS 17 и вплоть до iOS 26, TipKit позволяет декларативно описать подсказку через протокол Tip, задать правила её появления, привязать к событиям пользователя и вывести её как popover рядом с элементом или inline в контенте. В отличие от самописных туториалов, TipKit хранит частоту показов в собственном datastore, синхронизирует состояние через iCloud и корректно работает с VoiceOver и Dynamic Type.
TipKit требует iOS 17 и выше; в iOS 26 добавили анимации входа-выхода и более точный контроль частоты через DisplayFrequency.
Подсказка описывается типом, соответствующим протоколу Tip, со свойствами title, message, image, rules, actions и options.
Стиль popover привязывает подсказку к элементу через модификатор .popoverTip(_:), а inline показывает её через TipView(_:) прямо в иерархии.
События (Tips.Event) донатятся через donate() и позволяют показать подсказку только после определённой активности пользователя.
Тестирование ускоряется через Tips.showAllTipsForTesting(), а сброс состояния делается методом Tips.resetDatastore().
Accessibility работает из коробки: VoiceOver читает заголовок и сообщение, а кнопки действий получают роли автоматически.
Что такое TipKit и когда его использовать
TipKit это декларативный фреймворк, представленный Apple на WWDC 2023 и с тех пор ставший стандартным способом рассказать пользователю о новой или скрытой функции интерфейса. Раньше каждая команда пилила собственный слой «онбординга»: полупрозрачные оверлеи, TapTargetView, самопальные бабл-подсказки поверх UIWindow. Они падали при повороте, не дружили с VoiceOver и хранили статус показа в UserDefaults, что превращалось в кашу при обновлениях. TipKit решает всё это разом: подсказка становится обычным SwiftUI-типом, а частотой, местом хранения и синхронизацией занимается сам фреймворк.
Так, где TipKit уместен? Используйте его, когда нужно указать на кнопку в тулбаре, объяснить жест в списке, подсветить контекстное меню или ввести пользователя в новый экран после апдейта. Не используйте TipKit для критичных диалогов (согласий, ошибок оплаты), для маркетинговых промо-блоков и для многошаговых туториалов, там уместнее Sheet, Alert или отдельный экран онбординга. Ознакомьтесь с официальной документацией TipKit, чтобы увидеть весь список ограничений и рекомендаций Human Interface Guidelines.
Настройка проекта и Tips.configure
TipKit доступен на iOS 17+, iPadOS 17+, macOS 14+, watchOS 10+ и tvOS 17+. Минимум, что нужно сделать, это импортировать модуль и вызвать Tips.configure() при старте приложения. В iOS 26 конфигурация расширена: можно указать DisplayFrequency, кастомный datastore для симулятора и включить логирование через параметр logging.
import SwiftUI
import TipKit
@main
struct SwiftCraftedApp: App {
init() {
// Настраиваем TipKit один раз при старте.
// .immediate: не ждать интервала между показами (удобно в разработке).
// .default: рекомендованное значение для релиза.
try? Tips.configure([
.displayFrequency(.immediate),
.datastoreLocation(.applicationDefault)
])
}
var body: some Scene {
WindowGroup { ContentView() }
}
}
Параметр datastoreLocation определяет, где будет храниться состояние подсказок. По умолчанию используется контейнер приложения, но для App Group или тестового прогона можно передать произвольный URL. Обратите внимание: try? нужен, потому что configure() бросает при повторном вызове или недоступной директории. Пусть вызов будет ровно один, в init() корневого App.
Как показать первую подсказку в SwiftUI
Подсказка в TipKit это обычная структура, соответствующая протоколу Tip. Минимально нужны свойства title и message. Оба принимают Text, что позволяет использовать локализацию, markdown-разметку и вложенные Image. Ниже подсказка «Свайпните влево, чтобы избавиться от статьи», которую мы покажем рядом с кнопкой сортировки в списке.
import TipKit
struct SortArticlesTip: Tip {
var title: Text {
Text("Отсортируйте статьи")
}
var message: Text? {
Text("Нажмите иконку сортировки, чтобы сгруппировать материалы по дате или автору.")
}
var image: Image? {
Image(systemName: "arrow.up.arrow.down.circle")
}
}
Теперь привяжем подсказку к кнопке. В SwiftUI это один модификатор, .popoverTip(_:). Он сам решает, когда показать popover (при первом рендере, если правила выполнены), сам ставит accessibility-фокус и сам скрывает подсказку при взаимодействии с элементом.
struct ArticlesToolbar: View {
private let sortTip = SortArticlesTip()
var body: some View {
Button {
// логика сортировки
} label: {
Image(systemName: "arrow.up.arrow.down.circle")
}
.popoverTip(sortTip, arrowEdge: .top)
}
}
Если подсказка нужна не привязанной к элементу, а как баннер в теле экрана, замените модификатор на TipView(sortTip). Это отдельная View, которую можно положить в List, ScrollView или VStack. Подробнее о том, как TipView работает с анимациями появления, я рассказывала в материале про анимации в SwiftUI. Те же spring-пружины лежат в основе входа TipView.
Popover или inline: какой стиль выбрать
TipKit предоставляет два визуальных стиля, и выбор между ними не про эстетику, а про информационную архитектуру. Честно говоря, я сама долго не могла выбрать, пока не составила себе эту таблицу и не начала показывать её на код-ревью, когда команда спорит, куда положить подсказку.
Критерий
Popover (.popoverTip)
Inline (TipView)
Привязка к элементу
Да, через arrow-edge к target
Нет, живёт в потоке контента
Занимает место в layout
Нет (плавает поверх)
Да (участвует в стеке)
Автоматическое закрытие
При взаимодействии с target
Только по крестику или правилу
Подходит для тулбара
Идеально
Плохо (обрезается)
Подходит для пустого состояния списка
Плохо
Идеально
VoiceOver-фокус
Прыгает к popover
Читается по порядку
Минимальная iOS
17.0
17.0
Мой рабочий шаблон такой: popover для точечных «эй, вот эта кнопка», и inline для контекстов вроде «а вы знали, что тут можно…», когда у экрана есть свободное вертикальное пространство. В iOS 26 у обоих стилей появился модификатор .tipBackground(_:), позволяющий подложить .regularMaterial или Liquid Glass. Про этот дизайн я писала в статье о Liquid Glass в SwiftUI.
Правила отображения: parameter и event
Правила (Rules) это то, что превращает TipKit из «показать один раз» в контекстную систему. У правил два вида: parameter-based (сравнение с булевой переменной) и event-based (реакция на факт события). Оба описываются через result builder @Parameter и @Event внутри вашего типа Tip.
struct MarkAsReadTip: Tip {
// Параметр: пользователь уже включил синхронизацию.
@Parameter static var syncEnabled: Bool = false
// Событие: пользователь открыл минимум три статьи.
static let articleOpened = Event(id: "articleOpened")
var title: Text { Text("Отмечайте прочитанное") }
var message: Text? { Text("Свайпните вправо, чтобы пометить статью как прочитанную и синхронизировать с другими устройствами.") }
var rules: [Rule] {
// Показываем подсказку только если синхронизация включена
// И пользователь открыл хотя бы 3 статьи в течение недели.
#Rule(Self.$syncEnabled) { $0 == true }
#Rule(Self.articleOpened) {
$0.donations.donatedWithin(.week).count >= 3
}
}
}
Макрос #Rule в Xcode 16 разворачивается в тип Rule, который TipKit опрашивает при каждой попытке показа. Правила комбинируются логическим AND, то есть подсказка появится, только если все они возвращают true. Если нужен OR, придётся описать промежуточное вычисляемое свойство и вернуть его булев результат.
Parameter-правила отлично работают для флагов из @AppStorage, статуса подписки или роли пользователя. Event-правила лучше подходят для «показать, когда что-то произошло N раз»: открытия, свайпы, редактирования. Про моделирование состояния приложения я рассказывала в гайде по @Observable, и TipKit хорошо ложится поверх модели, привязанной к @Observable-классу.
События, донаты и частота показов
Чтобы TipKit узнал о произошедшем событии, вызовите donate() в момент, когда оно случилось. Донат это лёгкая операция: TipKit просто добавит запись в свой datastore с текущей датой.
struct ArticleRow: View {
let article: Article
var body: some View {
NavigationLink(value: article) {
Text(article.title)
}
.task {
// Донатим событие при появлении строки на экране.
await MarkAsReadTip.articleOpened.donate()
}
}
}
Метод donatedWithin(.week) в правиле выше вернёт массив донатов за последние 7 дней. Доступны интервалы .hour, .day, .week, .month и произвольный .custom(_:). Комбинируя события, можно строить довольно сложную логику: «показать подсказку про экспорт после того, как пользователь три раза открыл статью и хоть раз включил ридер-режим».
За частоту показов отвечают Options. Самые полезные, на мой взгляд: MaxDisplayCount(3) (не показывать больше трёх раз за всё время), IgnoresDisplayFrequency(true) (не ждать глобальный интервал между разными подсказками) и DisplayFrequency(.daily) (глобальный интервал между показами любой подсказки).
struct ExportTip: Tip {
var title: Text { Text("Экспорт в PDF") }
var message: Text? { Text("Удерживайте статью и выберите «Экспорт», чтобы сохранить её как PDF.") }
var options: [any TipOption] {
MaxDisplayCount(3)
IgnoresDisplayFrequency(true)
}
}
Accessibility, VoiceOver и Dynamic Type
Если вы читаете мои статьи, то знаете: accessibility не приделывается сверху. TipKit в этом смысле сделан правильно. TipView и popoverTip автоматически объявляются VoiceOver с ролью alert: сразу после появления фокус переходит на подсказку, читаются title и message, а кнопки действий (если вы их описали в свойстве actions) получают роль button с корректной подсказкой «двойное касание для активации».
Что действительно стоит проверить самому:
Dynamic Type. Подсказка растёт вместе с настройкой текста пользователя. Не оборачивайте message в .font(.footnote), потеряете масштабирование. Используйте семантические стили или .dynamicTypeSize(.medium...(.accessibility3)).
Порядок фокуса. Popover-стиль перехватывает VoiceOver-фокус. Убедитесь, что рядом нет других алертов и что вы не показываете подсказку одновременно со Sheet, иначе VoiceOver прочитает только последнее.
Локализация. Тексты пропускайте через String(localized:) или Xcode String Catalogs. У TipKit нет собственной системы локализации, он полагается на вашу.
Reduce Motion. При включённом Reduce Motion TipKit автоматически убирает пружинную анимацию входа и заменяет её мягким fade. Проверьте, что ваш кастомный tipBackground не добавляет собственную анимацию поверх.
Обязательно проверьте подсказки с реальным VoiceOver в Accessibility Inspector. Apple подробно описывает практики в HIG по разделу Tips, а общие рекомендации по доступности собраны в Apple Accessibility Hub.
Тестирование, сброс и превью в Xcode 16
Главная боль при работе с TipKit звучит так: «а как посмотреть, как она выглядит, если я её уже видела?». Поскольку datastore живёт между запусками, стандартный Cmd+R не поможет. У TipKit есть три инструмента для этого.
Во-первых, Tips.showAllTipsForTesting(). Вызовите его в init приложения (обёрнутый в #if DEBUG), и все подсказки будут показываться, игнорируя правила и частоту.
Во-вторых, Tips.resetDatastore() удалит все донаты и историю показов. Полезно, когда вы хотите проверить «холодный» сценарий пользователя, только что установившего приложение. Я хватала этот баг на последнем релизе: забыла сбросить datastore после смены схемы Event, и все правила молча возвращали false.
В-третьих, в Xcode 16 макрос #Preview поддерживает изолированный datastore. Достаточно вызвать try? Tips.resetDatastore() и try? Tips.configure(...) внутри превью, и вы увидите подсказку сразу, не запуская симулятор. Если пишете юнит-тесты, посмотрите гайд по Swift Testing, там я показывала, как проверять донаты через #expect и как мокировать Tips.Event.
За два года производственного использования TipKit я собрала небольшой список граблей, на которые команды наступают одинаково:
Забыть про invalidate(reason:). Если пользователь уже воспользовался функцией, подсказку нужно пометить как выполненную вручную: sortTip.invalidate(reason: .actionPerformed). Иначе она вернётся при следующем запуске.
Хранить Tip как let в body. Каждый ререндер создаст новый инстанс, а вместе с ним новую регистрацию в TipKit. Держите инстанс в @State или как ленивое свойство контейнера.
Показ подсказки в .sheet. TipKit не видит modal-иерархию, и popover может отрисоваться под шитом. Показывайте подсказки на основном экране, а не поверх Sheet.
Игнорирование DisplayFrequency. В релизе оставляйте .default: пользователи не любят десять подсказок подряд. Immediate только для DEBUG.
Локальный текст в String без LocalizedStringKey. TipKit не будет ругаться, но вы потеряете локализацию. Всегда используйте Text("ключ") или явный String(localized:).
Часто задаваемые вопросы
Работает ли TipKit на iOS 16?
Нет. TipKit доступен только начиная с iOS 17, iPadOS 17, macOS 14, watchOS 10 и tvOS 17. На более старых версиях вам придётся использовать собственный UI на базе overlay или alert, либо ограничить показ TipKit-подсказок через #available(iOS 17, *).
Как сбросить TipKit-подсказки для повторного показа?
Вызовите Tips.resetDatastore(), метод очистит все донаты, историю показов и статусы invalidate. Если нужно сбросить только одну подсказку, используйте sortTip.invalidate(reason: .tipClosed), а затем удалите её через Tips.forgetAllTips() в тестовом окружении.
Чем popoverTip отличается от TipView?
popoverTip это модификатор, который прикрепляет подсказку к элементу управления и рисует её как всплывающий popover со стрелкой. TipView это обычная SwiftUI-View, которая живёт в потоке контента (например, в списке) и не привязана к точке. Popover лучше для точечных подсказок, inline для баннеров.
Может ли пользователь отключить TipKit-подсказки?
Отдельного системного переключателя нет. Пользователь может закрыть каждую подсказку крестиком, и TipKit запомнит это и не покажет её снова. Разработчику имеет смысл добавить настройку «Показывать подсказки» в свои Settings и вызывать Tips.resetDatastore() при её включении.
Синхронизируется ли TipKit через iCloud?
Да. Если у пользователя включён iCloud, TipKit автоматически синхронизирует статусы показов между устройствами: подсказка, увиденная на iPhone, не появится снова на iPad. Для отключения синхронизации передайте в Tips.configure параметр .cloudKitContainer(.none).
Полный разбор ScrollView в SwiftUI и iOS 26: paging и snapping через scrollTargetBehavior, отслеживание позиции через scrollPosition, фазовые анимации scrollTransition и адаптивные карусели. Замена UIScrollView без потери контроля.
Разбираем хаптик-фидбек в SwiftUI: когда хватает .sensoryFeedback, а когда пора идти в Core Haptics. Параметры intensity и sharpness, декларативные AHAP-паттерны, синхронизация с анимациями и правила accessibility для iOS 26.
Практический гайд по StoreKit 2 в Swift: загрузка продуктов, JWS-верификация транзакций, SubscriptionStoreView и серверная проверка через App Store Server API для iOS 26.