Вовлечение пользователей
Как создать сверхперсонализированный пейвол с помощью Apple Foundation Models
Неважно, хотите ли вы создать сверхперсонализированный пейвол или показать именно ту функцию, которая решает его конкретную задачу — локальный ИИ открывает новый способ адаптировать его под каждого человека.
Вы уже знаете, что персонализированные пейвол-экраны конвертируют лучше. Но что, если использовать всё, что приложение уже знает о пользователе, чтобы сделать персонализацию действительно глубокой — но при этом не перейти границу и не стать навязчивыми?
И время в этом играет решающее значение — согласно отчёту State of Subscription Apps 2026, 55% отмен трёхдневных пробных периодов происходят уже в День 0. Борьба за подписчика выигрывается или проигрывается в первую же сессию. Поэтому если пейвол получает только один шанс, он должен говорить именно с тем человеком, который на него смотрит.
Пример приложения
Chorus — приложение для определения птиц по голосу и отслеживания прогулок, которое я создаю для Shipaton 2026. Во время онбординга оно узнаёт, зачем пользователь вообще открыл приложение и что могло бы заставить его чаще выходить на прогулки. Приложение спрашивает о птице мечты, с кем человек обычно гуляет, когда он выходит на прогулку, что действительно могло бы мотивировать его чаще выходить из дома.
Эта статья — о том, что происходит дальше: как использовать локальный фреймворк Foundation Models от Apple, чтобы в момент предложения подписки вернуть пользователю его собственную мотивацию, прямо внутри удалённо конфигурируемого пейвола RevenueCat.
И самое приятное — это бесплатно.
Вот базовый пейвол без персонализации:

Базовый paywall Chorus без персонализации.
Он выполняет свою задачу. Нормально. Но по-настоящему к пользователю он не обращается. Давайте это изменим.
Пользовательские переменные в пейволе
Удаленно конфигурируемые пейволы в RevenueCat позволяют персонализировать экран через кастомные переменные. Но что, если пойти дальше? Что, если использовать всё, что приложение уже знает о пользователе, чтобы действительно сделать пейвол персональным?
Откройте редактор пейвола в RevenueCat. В левом меню выберите Paywall Logic, затем Variables. После этого нажмите Create Variable — появится форма для создания переменной.

Начнём с userName — возможно, самого простого варианта персонализации. В качестве значения по умолчанию я использую Birder. Chorus спрашивает имя пользователя во время онбординга, сохраняет его и передаёт в удалённый пейвол следующим образом:
struct ChorusPlusPaywall: View {
let userName: String? // asked for in onboarding; nil if they skipped it
var body: some View {
PaywallView(displayCloseButton: true)
// If there's no name, send nothing; the dashboard
// default ("Birder") fills the gap.
.customPaywallVariables(userName.map { ["userName": .string($0)] } ?? [:])
}
}
В результате получится пейвол примерно такого вида:

Paywall Chorus с использованием кастомных переменных.
Уже лучше. Но с Foundation Models можно пойти значительно дальше.
Foundation Models от Apple
Foundation Models — локальный фреймворк Apple для работы с большой языковой моделью, представленный в iOS 26. Он генерирует текст прямо на устройстве — без обращения к серверу, без сетевой задержки и без оплаты за токены. Apple говорит, что локальные модели хорошо подходят для множества задач генерации текста, включая суммаризацию, извлечение сущностей, понимание текста и изображений, улучшение текста, диалогов в играх, генерацию творческого контента и «большего». Для нашего сценария подходит идеально. При этом локальные модели значительно меньше облачных, поэтому у них есть ограничения. К ним мы ещё вернёмся.
Как я уже говорил, Chorus узнаёт пользователя во время онбординга — именно здесь и начинается самое интересное.
Создадим класс, отвечающий за всё взаимодействие с Foundation Models. Я назвал его PaywallCopyGenerator. В Chorus каждый экран онбординга знает об этом классе, а его функции вызываются в разных точках потока.
В самом начале welcome-экран вызывает prewarm(). Метод проверяет SystemLanguageModel.default.availability и заранее прогревает сессию. На устройствах без Apple Intelligence выполнение заканчивается прямо здесь: пользователь получает заранее написанный резервный текст, а модель вообще не вызывается.
func prewarm() {
guard warmSession == nil else { return }
guard case .available = SystemLanguageModel.default.availability else { return }
warmSession = LanguageModelSession(instructions: instructions)
warmSession?.prewarm()
}
Предварительный прогрев сокращает время ожидания сгенерированного результата. Лучше выполнять его как можно раньше.
Есть один нюанс: когда на устройстве пользователя впервые запрашивается соответствие требованиям, SystemLanguageModel может временно отображаться как недоступный, пока iOS загружает модель в фоне. Паниковать не нужно — состояние исправится само. В Chorus мне не требуется проверять доступность iOS 26, потому что приложение и так ориентировано на iOS 26. Если вы поддерживаете более старые версии системы, такая проверка понадобится.
Инструкции и промпты
Вы могли заметить, что в LanguageModelSession передаются системные инструкции. Apple рекомендует улучшать результат, задавая модели роль, персонажа и тон. Вот системные инструкции Chorus:
You are a warm British field naturalist writing paywall copy for Chorus, a bird-walking app. You are given notes about a walker from the app's onboarding: their dream bird, what would get them out walking more, and how they walk. Write two short sentences spoken to them as "you". 1. Conjure their next walk as one small imagined moment, built only from the notes: who is with them, when or where they walk, their dream bird singing somewhere in it. 2. Say what Chorus can help them do about it. Never invent people, places, times or history that are not in the notes. Mention Chorus exactly once, in the help sentence; never name features or prices. Never the walker's name. No questions, no exclamation marks, no em dashes. Thirty words at most. Write like a person talking, not an advert: plain, everyday words, said simply, nothing you would not say to a friend on a walk. The examples below are style only; never reuse their scenes, company or times of day for a different walker. When the notes say who walks with them, hand it back as theirs: "your partner", "your dog", "your kids", not "a partner" or "the kids". Example 1. Notes: Their dream bird: the goldfinch; they have never heard one; that is the dream. What would get them out more: knowing new birds were waiting. They walk at dawn, with their kids. They get out less than they'd like. Line: Imagine a dawn walk with your kids when a goldfinch strikes up, your first ever. Chorus can help you catch it the moment it sings. Example 2. Notes: Their dream bird: the wren; they hear them all the time and love them. What would get them out more: keeping the birds they've found close. They notice birds by ear first. Line: Picture the next wren that sings before you spot it, one more song worth keeping. Chorus can help you hold on to every one. Example 3. Notes: Their dream bird: the blackbird; they hear them all the time and love them. What would get them out more: calmer, quieter time outside. They walk at dusk, as the day's song winds down. Line: Imagine a quiet walk at dusk, just you and the blackbird seeing the day out. Chorus can help you make more of those.
Эти инструкции опираются на пять принципов из документации Apple.
- Персона. Chorus следует рекомендациям Apple по ролевому поведению модели. Формулировка «you are a warm British field naturalist» сразу задаёт и роль, и голос. Более того, инструкции написаны в том же стиле, который ожидается от ответа модели — вплоть до запрета длинного тире, которое сами инструкции тоже не используют.
- Пошаговая логика. Сложное описание задачи разбито на два последовательных шага. Для небольших моделей это снижает когнитивную нагрузку и помогает соблюдать нужную последовательность.
- Ограничение размера. Промпт остаётся компактным. Два абзаца инструкций и одна чёткая цель укладываются в рекомендуемый Apple объём.
- Безопасные few-shot-примеры. Три подготовленных примера задают стиль. Отдельное правило «examples are style only» предотвращает перенос деталей из примеров в другие сценарии. Без этого модель легко начинает придумывать утренние прогулки пользователям, которые ходят только ночью.
- Стратегическое повторение. Важные правила вроде «never invent» и «exactly once» повторяются. А runtime-промпт ещё раз подчёркивает главное ограничение.
Runtime-промпт работает иначе. Онбординг собирает данные о пользователе, а за несколько экранов до пейвола мы просим модель сгенерировать персонализированную строку. Такой запас времени важен: локальные модели пока не особенно быстрые. Если начать генерацию заранее, текст уже будет готов к моменту появления пейвола. Можно показывать загрузку в конце онбординга, но лучше не блокировать появление пейвола.
Apple рекомендует передавать модели именно факты, поэтому довольно длинная Swift-функция формирует runtime-промпт без условной логики для самой модели. То есть все if обрабатываются в детерминированном Swift-коде, а модель получает только те факты, которые действительно применимы.
Задача модели — просто превратить их в естественный текст:
// Chorus — the walker's brief. Apple's "turn conditional prompting into programming
// logic", shipped: deterministic Swift turns onboarding answers into prose notes, so
// the on-device model only ever sees the conditions that apply. The model's whole job
// is translating these notes into one warm line. From `PaywallCopyGenerator.swift`.
/// The walker's why, in prose: dream bird + history, what would get them out more,
/// how they notice birds, and every context line that exists. Empty when everything
/// was skipped — there is nothing to reflect, and the fallback line ships instead.
func brief(signals: OnboardingSignals) -> String {
var lines: [String] = []
if let bird = signals.dreamBird {
let heard: String = switch signals.dreamBirdHistory {
case .often: "they hear them all the time and love them"
case .yearsAgo: "they heard one once, years ago"
case .unsure: "they aren't sure they've ever heard one"
default: "they have never heard one; that is the dream"
}
let seen: String? = switch signals.dreamBirdSeen {
case .often: "they see them all the time"
case .yearsAgo: "they saw one once, years ago"
case .unsure: "they couldn't say if they've seen one"
case .never: "they have never seen one"
case nil: nil
}
// Short name — briefing "the Common Nightingale" teaches stilted formal names.
lines.append("Their dream bird: the \(bird.shortName); \(heard)"
+ (seen.map { ", and \($0)" } ?? "") + ".")
}
switch signals.desiredPerk {
case .whereNext: lines.append("What would get them out more: knowing new birds were waiting.")
case .cameraScan: lines.append("What would get them out more: being able to name what they see.")
case .wearBird: lines.append("What would get them out more: keeping the birds they've found close.")
case .knowNow: lines.append("What would get them out more: knowing what's singing around them, right there and then.")
case .everyDay: lines.append("What would get them out more: a reason to get out every day.")
case .calm: lines.append("What would get them out more: calmer, quieter time outside.")
case .learnSongs: lines.append("What would get them out more: learning to know the songs themselves.")
case nil: break
}
switch signals.spotting {
case .ear: lines.append("They notice birds by ear first.")
case .eye: lines.append("They see birds but often can't name them.")
default: break
}
switch signals.knowTheBirds {
case .everyOne: lines.append("They want to know every bird they've heard.")
case .special: lines.append("They'd like to know the special ones at least.")
case .justListening: lines.append("They're happy just listening.")
case nil: break
}
if let habitat = signals.habitat {
switch habitat {
case .gardenAndStreet: lines.append("They walk gardens and streets close to home.")
case .park: lines.append("Their walks loop the local park.")
case .woodland: lines.append("Their walks run under trees more often than not.")
case .water: lines.append("Their walks keep close to water.")
case .openCountry: lines.append("They walk open country where song carries far.")
case .allOver: break
}
}
switch signals.walkTime {
case .dawn: lines.append("They walk at dawn, when the chorus is loudest.")
case .dusk: lines.append("They walk at dusk, as the day's song winds down.")
default: break
}
switch signals.companion {
case .alone: lines.append("They walk alone.")
case .partner: lines.append("They walk with their partner.")
case .dog: lines.append("They walk with their dog.")
case .kids: lines.append("They walk with their kids.")
default: break
}
switch signals.walkFrequency {
case .mostDays: lines.append("They walk most days.")
case .fewTimesAWeek: lines.append("They walk a few times a week.")
case .nowAndThen: lines.append("They get out now and then.")
case .lessThanLike: lines.append("They get out less than they'd like.")
case nil: break
}
return lines.joined(separator: " ")
}
OnboardingSignals — это структура, которая постепенно заполняется по мере прохождения пользователем онбординга.
Последняя часть — собственно генерация ответа:
func generate(signals: OnboardingSignals) {
guard task == nil else { return }
let brief = self.brief(signals: signals)
guard !brief.isEmpty else { return } // everything skipped → fallback line
working = true
task = Task { [weak self] in
defer { self?.working = false }
guard let self else { return }
self.generatedLine = await self.requestLine(
prompt: "Notes: \(brief)\nUse only these notes. Write the line.")
}
}
private func requestLine(prompt: String) async -> String? {
guard let session = warmSession else { return nil } // no model → fallback
let response = try? await session.respond(
to: prompt,
generating: GeneratedLine.self,
options: GenerationOptions(temperature: 0.85, maximumResponseTokens: 110))
return response?.content.line
}
Runtime-промпт гораздо проще системных инструкций. Мы просим модель заполнить тип @Generable:
@Generable
private struct GeneratedLine {
@Guide(description: "Both sentences: the reflection, then the Chorus help line. Nothing else")
var line: String
}
Здесь можно добавить дополнительную защиту. Например проверять результат на запрещённые слова, повторно генерировать ответ при нарушении правил или удалять лишние пробелы. Иногда модель оставляет в строке артефакты JSON, поэтому я очищаю результат так:
let line = raw.trimmingCharacters(in: .whitespacesAndNewlines)
.trimmingCharacters(in: CharacterSet(charactersIn: "{}\"\u{201C}\u{201D}"))
.trimmingCharacters(in: .whitespacesAndNewlines)
После этого передаём сгенерированную строку в пейвол так же, как раньше. И Foundation Models начинает выдавать персонализированный текст для конкретного пользователя.

Довольно круто, правда?
Ограничения Foundation Models Apple для персонализированного текста пейвола
Foundation Models довольно маленькие. Я сравниваю их с ранними версиями GPT-3: примерно 3 миллиарда параметров против триллионов параметров у крупных серверных моделей. Результат не всегда идеален, а галлюцинации возможны. Именно поэтому проверки безопасности важны. По умолчанию Apple защищает от вредоносного контента вроде расизма, насилия и ненормативной лексики. Но Apple отдельно подчёркивает: ответственность за сгенерированный моделью контент всё равно лежит на разработчике.
О безопасности промпта: Chorus никогда не передаёт модели свободно введённый пользователем текст. Бриф полностью строится из фиксированных ответов онбординга, поэтому пользователь физически не может ввести что-то, что превратится в инструкцию для модели. Если вы всё же передаёте свободный текст, относитесь к нему как к недоверенным данным: встраивайте его в жёстко заданную структуру промпта, никогда не позволяйте ему становиться самостоятельной инструкцией.
Я также обнаружил, что модель пока недостаточно умна для более амбициозных сценариев. Например, она не очень хорошо справляется с выбором наиболее релевантной функции приложения для конкретного пользователя и акцентированием именно её. Но это только начало. Модели сильно улучшились с iOS 26 до iOS 27, и я ожидаю, что этот прогресс продолжится.
Есть ещё одна причина строить персонализацию на ответах самого пользователя. Данные SOSA 2026 показывают, что приложения с ИИ зарабатывают на 41% больше на одного платящего пользователя, но теряют подписчиков на 30% быстрее. ИИ-функции окупаются только тогда, когда создают реальную и долговременную ценность. Персонализация на основе того, что пользователь сам вам рассказал — как раз такой случай.
Как пойти ещё дальше с персонализированными ИИ пейволом
Начиная с iOS 27 локальную модель можно заменить на облачную через сам фреймворк. Claude от Anthropic уже поддерживает этот интерфейс, и, вероятно, другие крупные провайдеры со временем сделают то же самое.
Есть также Apple Private Cloud Compute, где работают значительно более крупные серверные модели. Использовать его можно бесплатно, если у приложения меньше двух миллионов первых загрузок за всё время и вы участвуете в App Store Small Business Program.
С более крупными моделями становятся возможны намного более амбициозные сценарии персонализации. Например, можно автоматически выбирать наиболее релевантную функцию для каждого пользователя, делать именно её центральной частью пейвола и показывать конкретное решение нужной задачи. Команды вроде Tinder вкладывают огромные ресурсы именно в такую релевантность пейвола. С этими API для экспериментов уже не обязательно иметь отдельную команду.
Неважно, хотите ли вы создать сверхперсонализированный пейвол, который упоминает любимую птицу пользователя, или показать именно ту функцию, которая решает его конкретную задачу (job-to-be-done) — локальный ИИ открывает новый способ адаптировать его под каждого человека. Причём именно в тот момент, когда вы просите его оформить подписку.
-
Новости4 недели назадВидео и подкасты о мобильной разработке 2026.31
-
Аналитика промо-кампаний4 недели назадКак проверить, упоминают ли нейросети ваш бренд: обзор пяти сервисов
-
Разработка4 недели назад50 вопросов о System Design, ответы на которые должен знать каждый Senior iOS-разработчик: часть 4
-
Новости3 недели назадВидео и подкасты о мобильной разработке 2026.32
