Foundation Models у Swift: повний посібник з on-device AI для iOS 26

Як використовувати Foundation Models у Swift для iOS 26: генерація тексту, структурований вивід через @Generable, стрімінг, Tool Calling та найкращі практики on-device AI з робочими прикладами коду.

Вступ

Уявіть собі: ваш додаток генерує текст, аналізує вміст і витягує структуровані дані — і все це без жодного API-ключа, без хмарних витрат і навіть без інтернету. Звучить занадто добре? А от і ні. Саме це стало реальністю завдяки Foundation Models — фреймворку, який Apple анонсувала на WWDC25 і зробила доступним починаючи з iOS 26.

Foundation Models дає розробникам прямий доступ до on-device мовної моделі з приблизно 3 мільярдами параметрів — тієї самої, що живить Apple Intelligence. Фреймворк побудований навколо нативного Swift API, тож інтеграція AI у ваші додатки стає напрочуд простою. Усі дані залишаються на пристрої — а це конфіденційність, низька затримка і робота офлайн.

Далі ми розберемо всі ключові можливості Foundation Models: від базової генерації тексту до структурованого виводу через макрос @Generable, стрімінгу відповідей та Tool Calling. Кожен розділ містить робочі приклади коду, які можна використати у своїх проєктах вже зараз.

Системні вимоги та доступність

Перш ніж почати — переконайтеся, що ваше середовище відповідає вимогам:

  • Операційні системи: iOS 26+, iPadOS 26+, macOS 26 (Tahoe)+, visionOS 26+
  • Обладнання: Apple Silicon — A17 Pro або новіше для iPhone, M1 або новіше для Mac
  • Інструменти розробки: Xcode 26 з iOS 26 SDK
  • Налаштування користувача: Apple Intelligence має бути увімкнена в Налаштуваннях, а сама функція повинна бути доступна у вашому регіоні

Є кілька нюансів. Модель може потребувати часу на завантаження після ввімкнення Apple Intelligence — це нормально, просто зачекайте. Також Foundation Models не працює в режимі гри (Game Mode) та при критично низькому заряді батареї.

LanguageModelSession: перший запит до моделі

Робота з Foundation Models починається зі створення LanguageModelSession. Ця сесія зберігає стан між запитами, що дозволяє вести багатокрокові діалоги з моделлю.

Базовий приклад генерації тексту

Найпростіший спосіб отримати відповідь від моделі — передати текстовий промпт у метод respond(to:):

import FoundationModels

struct ContentView: View {
    @State private var session = LanguageModelSession()
    @State private var result = ""

    var body: some View {
        VStack {
            Text(result)
                .padding()
            Button("Генерувати") {
                Task {
                    let response = try await session.respond(
                        to: "Опиши три переваги SwiftUI в одному реченні кожну"
                    )
                    result = response.content
                }
            }
        }
    }
}

Оце і все — цього коду достатньо для отримання текстової відповіді від on-device моделі. Виклик respond(to:) є асинхронним, тому потрібен try await всередині Task.

Інструкції (System Prompt)

Якщо хочете точніше керувати поведінкою моделі, використовуйте параметр instructions при створенні сесії. По суті, це системний промпт — він задає контекст і правила для всіх наступних відповідей:

let session = LanguageModelSession(
    instructions: """
    Ти — досвідчений Swift-розробник.
    Відповідай коротко, з прикладами коду.
    Завжди використовуй сучасні API з iOS 26.
    """
)

А ще є декларативний InstructionsBuilder:

@State var session = LanguageModelSession {
    """
    Ти — помічник з кулінарії.
    Пропонуй рецепти на основі наявних інгредієнтів.
    Враховуй дієтичні обмеження користувача.
    """
}

Прогрівання моделі (Prewarm)

On-device модель керується операційною системою і може не перебувати в пам'яті, якщо довго не використовувалась. Виклик prewarm() завантажує модель заздалегідь — і, чесно кажучи, різниця відчутна: затримка першого токена скорочується до 40%.

struct ChatView: View {
    @State var session = LanguageModelSession()

    var body: some View {
        // ... UI чату ...
        .onAppear {
            session.prewarm()
        }
    }
}

Найкраще викликати prewarm() у момент, коли є чіткий сигнал, що користувач збирається взаємодіяти з AI. Наприклад, коли він відкриває екран чату або починає вводити текст у поле промпту.

Структурований вивід з @Generable та @Guide

А тепер — одна з найцікавіших можливостей Foundation Models. Замість того, щоб парсити неструктурований текст (і молитися, що модель поверне щось схоже на JSON), ви отримуєте типобезпечні Swift-об'єкти напряму від моделі. Apple називає це Guided Generation — керована генерація.

Як працює @Generable

Макрос @Generable застосовується до структур і перелічень. Під час компіляції він генерує JSON-схему та ініціалізатор, що дозволяє моделі повертати дані у форматі вашого Swift-типу. Це так зване обмежене декодування (constrained decoding) — модель буквально змушена генерувати валідний вивід, що відповідає вашій структурі на рівні токенів. Тобто ніяких «а раптом поверне щось не те».

import FoundationModels

@Generable
struct BookReview {
    let title: String
    let author: String
    @Guide(description: "Оцінка від 1 до 5")
    let rating: Int
    @Guide(description: "Короткий відгук в 2-3 речення")
    let summary: String
}

let session = LanguageModelSession()
let review = try await session.respond(
    to: "Створи рецензію на книгу 'Чистий код' Роберта Мартіна",
    generating: BookReview.self
).content

print(review.title)   // "Чистий код"
print(review.rating)  // 5
print(review.summary) // Структурований відгук

Обмеження значень через @Guide

Макрос @Guide дозволяє задавати опис властивості та обмежувати допустимі значення. Модель гарантовано поверне лише одне зі вказаних значень — без жодних сюрпризів:

@Generable
struct MovieRecommendation {
    let title: String
    @Guide(description: "Короткий опис в одному реченні")
    let summary: String
    @Guide(.anyOf(["PG", "PG-13", "R", "G"]))
    let rating: String
    @Guide(description: "Жанр фільму")
    let genre: String
}

let session = LanguageModelSession()
let movie = try await session.respond(
    to: "Порекомендуй екшн-фільм 2020-х років",
    generating: MovieRecommendation.self
).content

print(movie.rating) // Гарантовано одне з: "PG", "PG-13", "R", "G"

Порядок властивостей має значення

Ось важливий нюанс, який легко пропустити: модель генерує значення послідовно, відповідно до порядку оголошення властивостей. Якщо одна властивість залежить від іншої — вона має бути оголошена після неї. Наприклад, explanation (пояснення) залежить від answer (відповіді), тому має йти після:

@Generable
struct QuizQuestion {
    @Guide(description: "Питання вікторини")
    let question: String
    @Guide(description: "Правильна відповідь")
    let answer: String
    @Guide(description: "Пояснення відповіді з посиланням на факти")
    let explanation: String // Залежить від answer, тому йде після
}

Стрімінг відповідей

Чекати, поки модель згенерує повну відповідь, не завжди зручно. Foundation Models підтримує потокову генерацію через метод streamResponse — і це робить UI значно чуйнішим. Користувач бачить текст у процесі генерації, що суб'єктивно відчувається швидше.

Стрімінг тексту

struct StreamingView: View {
    @State private var session = LanguageModelSession()
    @State private var output = ""

    var body: some View {
        ScrollView {
            Text(output)
                .padding()
        }
        .task {
            let stream = session.streamResponse(
                to: "Напиши короткий вірш про Swift-розробку"
            )
            for await partial in stream {
                output = partial.content
            }
        }
    }
}

Стрімінг структурованих даних

Макрос @Generable автоматично створює тип PartiallyGenerated — по суті, дзеркало вашої структури, де кожна властивість стає опціональною. Завдяки цьому можна відображати часткові результати в міру генерації:

@Generable
struct TravelItinerary {
    @Guide(description: "Назва подорожі")
    let title: String
    @Guide(description: "Опис подорожі")
    let description: String
    @Guide(description: "Список місць для відвідування")
    let places: [String]
}

// Стрімінг
let stream = session.streamResponse(
    to: "Створи маршрут на 3 дні по Києву",
    generating: TravelItinerary.self
)

for await partial in stream {
    // partial.title може бути nil, поки модель ще не згенерувала це поле
    if let title = partial.content.title {
        print("Назва: \(title)")
    }
}

Tool Calling: розширення можливостей моделі

Tool Calling — це механізм, що дозволяє моделі звертатися до вашого коду для отримання даних або виконання дій. Якщо коротко — ви даєте моделі «інструменти», а вона сама вирішує, коли ними скористатися. Це ідеальний спосіб з'єднати Foundation Models з іншими фреймворками Apple: CoreLocation, HealthKit, MapKit тощо.

Створення інструменту

Інструмент створюється через відповідність протоколу Tool. У кожного інструменту три ключові компоненти:

  • name — коротке ім'я для ідентифікації (без пробілів)
  • description — одне речення, що пояснює, навіщо цей інструмент
  • Arguments — вкладена @Generable-структура з параметрами
import FoundationModels

struct WeatherTool: Tool {
    let name = "getWeather"
    let description = "Отримує поточну погоду для вказаного міста."

    @Generable
    struct Arguments {
        @Guide(description: "Назва міста")
        let city: String
    }

    func call(arguments: Arguments) async throws -> String {
        // Тут інтеграція з WeatherKit або іншим API
        let weather = await fetchWeather(for: arguments.city)
        return "Погода в \(arguments.city): \(weather.temperature)°C, \(weather.condition)"
    }

    private func fetchWeather(for city: String) async -> (temperature: Int, condition: String) {
        // Реальна реалізація з WeatherKit
        return (22, "сонячно")
    }
}

Використання інструментів у сесії

Передайте масив інструментів при створенні сесії або при виклику методу відповіді:

let session = LanguageModelSession(
    instructions: "Ти — помічник подорожей. Використовуй інструменти для отримання актуальних даних.",
    tools: [WeatherTool()]
)

let response = try await session.respond(
    to: "Яка зараз погода в Києві та Львові?"
)

// Модель автоматично викличе WeatherTool двічі — для кожного міста
print(response.content)

Модель самостійно вирішує, коли і які інструменти використовувати — все залежить від контексту промпту. Фреймворк автоматично обробляє паралельні та послідовні ланцюжки викликів, тож вам не треба про це турбуватися.

Практичний приклад: інтеграція з HealthKit

struct StepCountTool: Tool {
    let name = "getStepCount"
    let description = "Отримує кількість кроків за сьогодні з Apple Health."

    @Generable
    struct Arguments {
        // Без аргументів — дані завжди за поточний день
    }

    func call(arguments: Arguments) async throws -> String {
        let steps = await HealthKitManager.shared.todaySteps()
        return "Кроків за сьогодні: \(steps)"
    }
}

struct CaloriesTool: Tool {
    let name = "getCalories"
    let description = "Отримує кількість спалених калорій за сьогодні."

    @Generable
    struct Arguments {}

    func call(arguments: Arguments) async throws -> String {
        let calories = await HealthKitManager.shared.todayCalories()
        return "Спалено калорій: \(calories)"
    }
}

// Комбінуємо інструменти
let session = LanguageModelSession(
    instructions: "Ти — фітнес-тренер. Аналізуй дані здоров'я та давай рекомендації.",
    tools: [StepCountTool(), CaloriesTool()]
)

let analysis = try await session.respond(
    to: "Як пройшов мій день з точки зору фізичної активності?"
)

Найкращі практики для Tool Calling

  • Давайте інструментам короткі, але зрозумілі імена — використовуйте дієслово, наприклад findContact, getWeather
  • Тримайте опис коротким — одне речення, без деталей реалізації
  • Пам'ятайте: ім'я та опис додаються до промпту як токени, тож довші рядки збільшують затримку
  • Використовуйте session.transcript для дебагінгу — там видно кожен виклик інструменту

Керування токенами та продуктивністю

On-device модель Apple має обмеження в 4096 токенів на сесію. Це загальний бюджет — інструкції, промпти, відповіді, все разом. Не так вже й багато, якщо чесно. Ось як ефективно працювати в цих рамках.

Оптимізація токенів

  • Тримайте інструкції стислими — кожне зайве слово з'їдає ваш бюджет токенів
  • Створюйте нові сесії для незв'язаних запитів, замість того щоб перевикористовувати одну сесію з довгим транскриптом
  • Використовуйте includeSchemaInPrompt: false, якщо модель вже розуміє формат відповіді — це зберігає токени та зменшує затримку
// Економимо токени, коли модель вже знає формат
let response = try await session.streamResponse(
    to: prompt,
    generating: MyType.self,
    includeSchemaInPrompt: false
)

Контроль креативності через GenerationOptions

Параметр temperature контролює «креативність» відповідей. Значення від 0 (максимально передбачувані відповіді) до 1 (максимальна різноманітність). Для більшості задач зі структурованим виводом раджу тримати temperature ближче до 0:

let options = GenerationOptions(temperature: 0.3)
let response = try await session.respond(
    to: "Перелічи три принципи SOLID",
    options: options
)

Обробка помилок та захисні механізми

Foundation Models має вбудовану систему захисних механізмів (guardrails), що фільтрує небезпечний вміст. Про це варто знати і враховувати при розробці:

do {
    let response = try await session.respond(to: userInput)
    displayResult(response.content)
} catch let error as LanguageModelSession.GenerationError {
    switch error {
    case .guardrailViolation:
        showAlert("Запит містить вміст, що не може бути оброблений.")
    case .tokenLimitExceeded:
        showAlert("Перевищено ліміт токенів. Спробуйте коротший запит.")
    default:
        showAlert("Помилка генерації: \(error.localizedDescription)")
    }
} catch {
    showAlert("Невідома помилка: \(error.localizedDescription)")
}

Щоб мінімізувати хибні спрацювання guardrails:

  • Формуйте комплексний промпт, де введення користувача є лише малою частиною загального контексту
  • Де можливо, використовуйте UI з обмеженим вибором замість вільного текстового поля
  • Ретельно тестуйте промпти на різних вхідних даних — краще витратити час на тестування зараз, ніж ловити баги потім

Обмеження: що варто знати

Foundation Models — це круто, але давайте будемо реалістами. Ось що треба мати на увазі:

  • Потужність моделі: on-device модель з 3B параметрів значно поступається хмарним LLM від OpenAI, Google чи Anthropic. Вона оптимізована для конкретних задач — сумаризації, витягування сутностей, класифікації — і не підходить для генерації коду або складних математичних обчислень
  • Лише текстовий вхід: на відміну від мультимодальних хмарних моделей, Foundation Models наразі приймає тільки текст. Ні зображень, ні аудіо, ні PDF
  • Обмежений контекст: 4096 токенів — це суттєво менше, ніж у хмарних моделей, що обмежує складність діалогів
  • Мовна підтримка: найкращі результати англійською. Також підтримуються французька, німецька, іспанська, португальська, китайська, японська та корейська
  • Оновлення моделі: Apple оновлює модель без попередження, і зафіксувати конкретну версію неможливо — це може вплинути на стабільність вашого додатку між оновленнями ОС

Повний приклад: AI-помічник для аналізу тексту

Час зібрати все докупи. Ось практичний приклад — додаток, який аналізує текст і повертає структурований результат. Тут ми використовуємо @Generable, інструкції, prewarm() та обробку помилок:

import SwiftUI
import FoundationModels

// MARK: - Моделі даних

@Generable
struct TextAnalysis {
    @Guide(description: "Головна тема тексту")
    let mainTopic: String
    @Guide(description: "Тональність: positive, negative або neutral")
    @Guide(.anyOf(["positive", "negative", "neutral"]))
    let sentiment: String
    @Guide(description: "Список ключових слів (до 5)")
    let keywords: [String]
    @Guide(description: "Стисле резюме в 1-2 речення")
    let summary: String
}

// MARK: - Інструмент підрахунку слів

struct WordCountTool: Tool {
    let name = "countWords"
    let description = "Підраховує кількість слів у тексті."

    @Generable
    struct Arguments {
        @Guide(description: "Текст для підрахунку")
        let text: String
    }

    func call(arguments: Arguments) async throws -> String {
        let count = arguments.text.split(separator: " ").count
        return "Кількість слів: \(count)"
    }
}

// MARK: - View

struct TextAnalyzerView: View {
    @State private var inputText = ""
    @State private var analysis: TextAnalysis?
    @State private var isAnalyzing = false
    @State private var session = LanguageModelSession(
        instructions: """
        Ти — експерт з аналізу тексту.
        Аналізуй текст ретельно та об'єктивно.
        """
    )

    var body: some View {
        NavigationStack {
            Form {
                Section("Текст для аналізу") {
                    TextEditor(text: $inputText)
                        .frame(minHeight: 100)
                }

                Button(action: analyzeText) {
                    if isAnalyzing {
                        ProgressView()
                    } else {
                        Text("Аналізувати")
                    }
                }
                .disabled(inputText.isEmpty || isAnalyzing)

                if let analysis {
                    Section("Результат") {
                        LabeledContent("Тема", value: analysis.mainTopic)
                        LabeledContent("Тональність", value: analysis.sentiment)
                        LabeledContent("Резюме", value: analysis.summary)
                    }

                    Section("Ключові слова") {
                        ForEach(analysis.keywords, id: \.self) { keyword in
                            Text(keyword)
                        }
                    }
                }
            }
            .navigationTitle("AI Аналізатор")
            .onAppear {
                session.prewarm()
            }
        }
    }

    private func analyzeText() {
        isAnalyzing = true
        Task {
            do {
                let result = try await session.respond(
                    to: "Проаналізуй цей текст: \(inputText)",
                    generating: TextAnalysis.self
                )
                analysis = result.content
            } catch {
                print("Помилка аналізу: \(error)")
            }
            isAnalyzing = false
        }
    }
}

Часті запитання (FAQ)

Чи працює Foundation Models без інтернету?

Так, повністю. Модель працює на пристрої, тому інтернет не потрібен. Усі дані залишаються локальними — конфіденційність гарантована. Єдине — модель має бути попередньо завантажена, що відбувається автоматично після ввімкнення Apple Intelligence.

Які пристрої підтримують Foundation Models?

iPhone з чипом A17 Pro або новішим (тобто iPhone 15 Pro і новіше), iPad та Mac з чипом M1+. Потрібна iOS 26, iPadOS 26 або macOS 26 відповідно. І, звісно, Apple Intelligence має бути увімкнена в налаштуваннях.

Чим Foundation Models відрізняється від ChatGPT або Claude API?

Головна різниця — все працює on-device, без хмарних серверів. Це дає конфіденційність, нульову вартість та офлайн-доступ. Але (і це важливо) on-device модель з 3B параметрів значно менш потужна за хмарні LLM. Вона найкраще підходить для сумаризації, витягування даних, класифікації та коротких діалогів. Для складного аналізу або генерації коду хмарні API залишаються кращим вибором.

Чи можна використовувати @Generable з вкладеними типами?

Так. Усі збережені властивості @Generable-типу повинні самі бути generable. Примітивні типи (String, Int, Bool), масиви та опціональні значення підтримуються автоматично. Для вкладених структур просто додайте макрос @Generable і до них.

Як обійти ліміт у 4096 токенів?

Ліміт стосується однієї сесії. Найпростіші стратегії: створюйте нові сесії для незалежних запитів, тримайте інструкції стислими, використовуйте includeSchemaInPrompt: false де можливо, та розбивайте складні завдання на менші підзадачі з окремими сесіями.

Про Автора Editorial Team

Our team of expert writers and editors.