
This article covers the Foundation Models Framework: what it is, how it works under the hood, how to integrate it into a real-world app, and where its limitations lie. I’ve made an effort to avoid simply reiterating Apple’s documentation. Instead, I’ll focus on the non-obvious details that you won’t catch at first glance — the things I wish I had known before writing my first prototype. Why Do We Even Need Another AI Framework? Prior to iOS 26, an iOS developer looking to add intelligent features to their app had two viable paths. The first was utilizing a cloud API like OpenAI, Anthropic, Google, or something similar. While this works, it imposes several limitations: every request costs money, a stable internet connection is required, and user data physically leaves the device for a third-party server. That last point is particularly problematic for apps dealing with medical, financial, or highly personal data. In Europe, for instance, this immediately raises GDPR compliance issues. The second path was taking a pre-trained model in Core ML format and running it entirely on-device. Here, privacy is preserved, but a whole new set of challenges emerges: you have to select or train a model for your specific task, convert it to Core ML, optimize it for the Apple Neural Engine, and extensively test it across various devices with differing performance capabilities. For most development teams without a dedicated ML engineer, this approach is simply too costly and resource-intensive. The Foundation Models framework offers a third path that solves both problems simultaneously. Apple is providing direct programmatic access to the very same language models that power its system-level AI features, such as Writing Tools, Smart Reply in Messages, and Genmoji generation. Previously, this underlying model was entirely locked down; starting with iOS 26, you can seamlessly integrate it into your own applications. Press enter or click to view image in full size \ The fundamental difference from cloud APIs is that all inference happens locally on the device’s specialized AI chip. This completely shifts the economics of the feature (no more paying per token), its reliability (it works on an airplane, down in the subway, or out in the mountains), and the architecture (no need to worry about retry logic for network errors). A Minimal Working Example Before diving into the architecture, let’s take a look at a minimal working example. This code actually works in production: import FoundationModels let session = LanguageModelSession() let response = try await session.respond(to: "writing changelog") print(response.content) Four lines of meaningful code. No API keys, no network initialization, no subscriptions. But behind this simplicity lie several important nuances that become apparent the exact moment you try to use this in a real-world application rather than a Playground. That is exactly what we will discuss next. What Happens Under the Hood When you call session.respond(to:) , the request passes through several layers. Top to bottom: your Swift code calls the framework’s API. The FoundationModels framework itself is responsible for session management, handling Guided Generation (more on that below), tool calls, and streaming. Below that sits Apple’s own language model, weighing in at roughly 3 billion parameters. This is the exact same model used in the system’s AI features — meaning it’s not a stripped-down demo version, but the real deal. Finally, the actual computations are executed on the Neural Engine — a dedicated coprocessor within Apple Silicon purpose-built for neural network operations. An important point about the Neural Engine: it is neither a CPU nor a GPU. In the A17 Pro (iPhone 15 Pro), it’s a 16-core block delivering roughly 18 TOPS (trillions of operations per second). It’s precisely because of this hardware that a 3B parameter model can run on a phone with acceptable latency. On a standard CPU, inference would be agonizingly slow; on a GPU, it would devour the battery. The Neural Engine is the sweet spot, optimized specifically for neural network inference. This, by the way, leads to a crucial limitation: the framework only runs on devices equipped with a sufficiently modern Neural Engine. Specifically: iPhone 15 Pro and newer (A17 Pro chip and above), all Apple Silicon Macs (M1 and newer), and iPads with M-series chips. On the iPhone 14 and older, the API is unavailable, and any attempt to create a LanguageModelSession will result in an error. This is a vital detail to keep in mind—you'll need to architect a fallback to a cloud API or gracefully disable AI features for unsupported devices. Breaking Down the Code Line by Line Let’s return to our minimal example and dissect each line. This is worth doing because each line hides a choice that will ultimately impact your app’s architecture. Line 1: import FoundationModels Importing the framework. It sounds trivial, but you need to ensure compatibility both at build time and at runtime. At build time, simply bumping your minimum target to iOS 26 is enough; otherwise, the linker simply won't find the symbols. At runtime, you need to check LanguageModelSession.isAvailable : guard LanguageModelSession.isAvailable else { // The model is not available on this device. // A fallback should be implemented here — for example, hiding the AI feature // or using a cloud API. return } This guard is critical because your app might end up on a device that formally supports iOS 26 but lacks a sufficiently powerful Neural Engine. In my initial prototype, I forgot to include this check and got an unpleasant surprise when testing on an iPhone 14—the app crashed the moment it first tried to access the model. Line Two: Creating the Session LanguageModelSession() creates the session object. And this is where things get really interesting, because the session isn't just a simple wrapper around a single request. It is a fully stateful object that maintains the entire history of your prompts and the model's responses. This is important for two reasons. First, the model remembers the context: if you state “my name is Artem” in your first prompt, and then ask “what is my name” in the second, it will answer correctly. This is that exact conversational memory that, when working with cloud APIs, is typically handled by manually passing the entire message history along with every single request. Second — and far more importantly from a practical standpoint — creating a session is computationally expensive. Under the hood, this process involves loading the model’s weights into the Neural Engine’s memory and allocating a buffer for the context window. On my iPhone 15 Pro, this initialization took anywhere from 200 to 500 milliseconds. What this means is that you should instantiate the session once and retain it for reuse, rather than spinning up a new one for every prompt. In my prototype, I fell into this exact trap almost immediately. Initially, my function looked like this: \ // What NOT to do: creating a new session on every call func sendMessage(_ text: String) async throws -> String { let session = LanguageModelSession() let response = try await session.respond(to: text) return response.content } Under heavy usage, this brought the app to a crawl: every user message triggered a reload of the model into memory. The correct pattern is to keep the session as a property of your ViewModel or service: \ @Observable final class ChatViewModel { // Created once upon ViewModel initialization private let session = LanguageModelSession() func sendMessage(_ text: String) async throws -> String { // The session is reused across all requests // and retains the entire preceding conversational context let response = try await session.respond(to: text) return response.content } } Another nice bonus: when creating a session, you can set a system prompt via the instructions parameter. This is the equivalent of a system message in the ChatGPT API—instructions that the model will adhere to across all requests within that session: let session = LanguageModelSession( instructions: """ You are an assistant for iOS developers. Answer concisely, with Swift code examples.Do not use markdown formatting in your answers. """ ) In my chat assistant prototype, such instructions significantly improved the quality of the responses: without them, the model often resorted to generic phrases; with them, it provided structured answers alongside code snippets. Line 3: The Request and Error Handling try await session.respond(to:) is an asynchronous request to the model. The await part is straightforward: we wait for the model to generate a response. The try , however, is a different story entirely. This call can throw three types of errors, and each needs to be handled separately; otherwise, your app will start crashing in unexpected places. unsupportedLanguage : This occurs when the model cannot process the language of the prompt. As of iOS 26, 15 languages are supported, but if a user types in Thai, for instance, this is the exact error you will receive. contextWindowExceeded : The context window is the maximum amount of information the model can hold in memory simultaneously while generating a response. When the session history grows too long (for example, after twenty or thirty message exchanges), a new prompt might simply not fit into this window. At this point, you need to create a new session—ideally, passing a brief summary of the previous conversation into the instructions . guardrailViolation : The model has an internal safety system that blocks prompts and responses that violate specific rules: promoting violence, generating prohibited content, or jailbreak attempts. This isn't a bug; it's a deliberate feature. You get built-in safety without needing to write your own custom filters. However, you must handle this error gracefully—the user shouldn't see a raw, technical "guardrail violation" message; they need a polite "I cannot help with this request." The complete error handler looks something like this: \ do { let response = try await session.respond(to: userInput) updateUI(with: response.content) } catch LanguageModelError.unsupportedLanguage { showError("This language is not yet supported") } catch LanguageModelError.contextWindowExceeded { // The session is full — create a new one with a summary of the previous context await resetSessionWithSummary() } catch LanguageModelError.guardrailViolation { showError("I cannot help with this request, please try rephrasing") } catch { // Any other unexpected errors showError("Something went wrong") } Streaming is worth a separate mention. The respond(to:) method returns the final response—meaning the user has to wait until the model has completely generated the text. On short requests, this is barely noticeable, but if the model is generating a lengthy response (for instance, explaining a concept), a delay of a few seconds can result in a painful user experience. For these scenarios, there is streamResponse(to:) , which returns an AsyncSequence containing chunks of the response as they are being generated. This perfectly mimics the behavior of ChatGPT, where the characters appear progressively: \ @Observable final class StreamingViewModel { private let session = LanguageModelSession() var generatedText = "" func generate(prompt: String) async { generatedText = "" do { // Each iteration yields the next chunk of the response for await partial in session.streamResponse(to: prompt) { await MainActor.run { // Update the UI as chunks arrive generatedText += partial.text } } } catch { // error handling } } } From a UX perspective, the difference is massive. Without streaming, for a prompt like “explain SwiftUI,” the user stares at a spinner for 3–5 seconds, only to be hit with a massive wall of text all at once. With streaming, the first words appear in roughly 200 ms, and the text is “typed” right before their eyes. The perception shifts from “the app is frozen” to “the model is thinking out loud.” Line 4: The Response Content response.content is the string containing the model's answer. Besides content , the response object contains a few other useful properties. finishReason indicates why the generation stopped: .complete means normal termination, .length means the model hit the token limit and the text was truncated, and .stop means a stop token was encountered. usage contains token statistics: promptTokens , completionTokens , and totalTokens . This is incredibly useful for debugging and understanding the true scale of your prompts. Special attention should be paid to the finishReason == .length scenario. If this occurs frequently, it means your prompts or expected responses are simply too long. In this situation, the user sees an answer cut off mid-sentence, which inevitably looks like a bug. It's much better to detect this case and either break the prompt down into smaller chunks or explicitly inform the user that the response was truncated. Structured Output via @Generable This is arguably my favorite part of the Foundation Models framework — even more so than the fact that we have an on-device LLM. It solves a very real headache that absolutely anyone who has worked with language models has faced at some point. Let’s say we want to automatically analyze user reviews from the App Store. We need structured data from the model: the sentiment of the review, a rating from 1 to 5, a list of specific issues, and a brief summary. The classic approach is to explicitly ask the model to return a JSON string, and then parse that JSON on the app’s side. It usually looks something like this: // A fragile approach: requesting JSON and parsing it manually let response = try await session.respond(to: """ Analyze the review: \(userReview) Return JSON: { "sentiment": "...", "rating": ..., "issues": [...] } """) let data = response.content.data(using: .utf8)! let json = try JSONSerialization.jsonObject(with: data) // this can throw an error // Next comes type casting, field validation, error handling... This approach comes with a whole host of problems. The model might return invalid JSON — for example, using single quotes instead of double quotes, or leaving a trailing comma. It might forget a required field entirely or, conversely, hallucinate an extra one. It might mix up data types, like returning a rating as a string instead of an integer. It might even wrap the JSON in conversational explanatory text, which instantly breaks the parser. Any one of these edge cases immediately translates to a bug in production. The Foundation Models framework solves this exact problem using the @Generable macro and a technique known as constrained decoding. The concept is simple: we define the required structure as a standard Swift type, mark it with @Generable , and annotate each property with @Guide using a natural language description. Then, we pass this type into respond(to:generating:) , and the model populates it for us: \ @Generable struct AppReview { @Guide("The sentiment of the review: positive, negative, or neutral") var sentiment: String @Guide("A rating from 1 to 5, where 5 is excellent") var rating: Int @Guide("A list of specific issues mentioned in the review. An empty array if there are no issues.") var issues: [String] @Guide("A brief one-sentence summary") var summary: String } // Usage: let review = try await session.respond( to: "Analyze the review: \(userReview)", generating: AppReview.self ) // `review` is a ready-to-use Swift object, absolutely no parsing required print(review.sentiment) // "negative" print(review.rating) // 2 print(review.issues) // ["crashes on load", "confusing navigation"] What’s happening here is quite fascinating. When we write generating: AppReview.self , the Swift compiler utilizes the @Generable macro to generate a schema that describes the structure of the type: what properties it has, what their types are, and what constraints exist. This schema is passed to the model not as a mere suggestion within the prompt, but as a strict rulebook for the generation process itself. And here is the most interesting part: during generation, the model is strictly limited to tokens that are valid for its current position in the schema. If it is currently generating a value for the rating: Int property, it physically cannot return a word or a floating-point number—the very next token can only be a digit. This technique is known as constrained decoding . This approach offers two major advantages. The first is guaranteed validity: the model will never return a “broken” result or malformed payload because invalid tokens are simply mathematically unavailable to it. The second, somewhat unexpected advantage is speed: because the space of possible tokens is dramatically reduced at each step, fewer computations are required. Apple explicitly highlighted this in WWDC Session 286: Guided Generation simultaneously improves accuracy and accelerates inference. However, there is a nuance regarding the quality of the phrasing within your @Guide annotations. The more precise the description, the more accurate the result will be. Compare these two: \ // Vague phrasing @Guide("rating") var rating: Int // Precise phrasing @Guide("A rating from 1 to 5, where 1 is very bad and 5 is excellent. Take the overall tone of the review into account.") var rating: Int In the first case, the model might return a rating in any arbitrary range — 0, 100, you name it. In the second case, it is given explicit context: 1 means bad, 5 means excellent, and the overall tone must be factored in. Fundamentally, @Guide is a natural language instruction for the model bound to a specific property. Think of it as a prompt, but much more precise and highly localized. You can also nest structures and combine them with enums: @Generable enum Severity { case critical, high, medium, low } @Generable struct BugReport { @Guide("A brief, one-sentence description of the problem") var title: String @Guide("Steps to reproduce the problem") var reproSteps: [String] @Guide("The severity of the problem") var severity: Severity @Guide("The probable cause, if obvious. nil if not obvious.") var probableCause: String? } In my bug tracker prototype, this structure replaced roughly fifty lines of manual parsing code and error handling. More importantly, it completely eliminated all those “random crash on an empty array” bugs. Tool Calling: Connecting the Model to Real Data A language model operates solely on the context provided in the prompt. It doesn’t know tomorrow’s weather forecast, current exchange rates, your daily schedule, nor does it have access to your database. If you ask it to answer such questions directly, it will either honestly reply “I don’t know,” or it will start hallucinating — spitting out a plausible but entirely fabricated answer. Tool Calling is the mechanism that solves this problem. You describe functions (tools) that your code knows how to execute, and the model itself decides when and with what arguments to invoke them. Let’s look at a concrete example. Suppose we’re building a tool to search for restaurants: \ struct RestaurantSearchTool: Tool { // The name of the tool — the model uses this internally let name = "searchRestaurants" // The description for the model: when to use this tool let description = "Searches for restaurants based on parameters and returns a list of matching options" // Arguments are defined using @Generable @Generable struct Arguments { @Guide("Cuisine type: italian, japanese, russian, etc.") var cuisine: String @Guide("Number of guests") var guests: Int @Guide("Date in YYYY-MM-DD format") var date: String @Guide("Minimum rating from 1 to 5, defaults to 4") var minRating: Double? } // The function itself: receives the arguments, returns the result func call(arguments: Arguments) async throws -> ToolOutput { let results = await RestaurantAPI.search( cuisine: arguments.cuisine, guests: arguments.guests, date: arguments.date, minRating: arguments.minRating ?? 4.0 ) let text = results.map { "\($0.name) — \($0.rating)⭐" }.joined(separator: "\n") return ToolOutput(text) } } Now, let’s connect the tool to the session and write a natural language request: \ let session = LanguageModelSession(tools: [RestaurantSearchTool()]) let answer = try await session.respond( to: "Find an Italian restaurant for 4 people for next Friday") print(answer.content) \ Here is what happens internally. The model receives the prompt, sees that it has the searchRestaurants tool, reads its description, and realizes it is exactly what's needed. It then autonomously extracts the parameters from the user's request: "italian" for the cuisine, 4 for the number of guests, and converts "next Friday" into a specific date. It invokes call(arguments:) , receives the result, and formulates the final answer for the user. How does this fundamentally differ from the classic approach using intents and regular expressions? Previously, implementing this kind of functionality required writing complex user intent parsing logic: calculating a timestamp from “next Friday,” extracting the cuisine type via keyword matching or NLP, and handling endless phrasing variations. With Tool Calling, you simply describe what your app can do, and the model figures out how to interpret the request on its own. It is a vastly more declarative approach. You can pass multiple tools into a single session, and the model will choose the appropriate one based on the context, or even combine them: \ let session = LanguageModelSession(tools: [ RestaurantSearchTool(), WeatherTool(), CalendarTool() ] ) // For the prompt "Find a restaurant for Friday if the weather is good" // the model might first call WeatherTool to check the forecast, // then CalendarTool to confirm the date, and finally RestaurantSearchTool \ Another neat little detail I liked: ToolOutput supports specifying a data source via ToolOutput.Source . If your tool fetches data from a specific API, you can pass the URL and the source name, and the model will seamlessly include this attribution in its final response. It acts as built-in fact-checking—the user doesn't just see "here is a restaurant," but rather "here is a restaurant, according to [Source]." When to Use What: Choosing the Right Framework for the Job The Foundation Models framework is a powerful tool, but it is far from the only AI framework available on iOS. And this is perhaps the single most important point I want to convey in this article: do not drag an LLM into a problem that can be solved by a simpler, more purpose-built tool. The principle I adhere to is this: use the highest-level API that solves the problem. The higher the abstraction level, the less code you have to write, the better the system integration, and the less ML-related responsibility falls on your team. If the goal is to allow the user to edit text using AI (rewriting, correcting errors, or summarizing), the right tool for the job is Writing Tools. This is a system-wide Apple feature accessible via the context menu in any text field. If your application relies on a standard UITextView with UITextInteraction , Writing Tools are already functional—without a single line of code on your part. Apple made this integration completely automatic. However, if you have a custom text editor (for example, a Markdown editor powered by its own text engine), iOS 26 introduces the Coordinator API for integrating Writing Tools. By utilizing UIWritingToolsCoordinator , you can achieve full-fledged integration, complete with fluid rewriting animations, inline spell-checking, and follow-up prompts from the user: \ class CustomEditor: UIView { var writingToolsCoordinator: UIWritingToolsCoordinator? override func awakeFromNib() { super.awakeFromNib() // Create the coordinator and bind it to our engine let coordinator = UIWritingToolsCoordinator(delegate: self) self.writingToolsCoordinator = coordinator } } extension CustomEditor: UIWritingToolsCoordinatorDelegate { func writingToolsCoordinator( _ coordinator: UIWritingToolsCoordinator, replace originalText: String, with newText: String, in range: NSRange ) { // Apply the rewritten text to our engine. // The rewriting animation will be added automatically. myTextStorage.replaceCharacters(in: range, with: newText) } } If the task is to offer the user ready-made reply options in a chat (like it’s done in Messages), then use the Smart Reply API . The model analyzes the conversation context and returns 3–5 suitable short responses. If you need to analyze an image — recognize objects, read text, find tables — that’s the Vision Framework . By the way, in iOS 26, Vision introduced an important updated API: VNRecognizeDocumentRequest , which understands the structure of the document as a whole—tables, lists, headers, paragraphs—rather than just recognizing individual lines of text like the old OCR did. If the task is speech recognition, especially for long recordings like meetings or lectures, iOS 26 recommends using the new SpeechAnalyzer instead of the deprecated SFSpeechRecognizer . The new API is specifically optimized for long audio, supports streaming via AsyncSequence , and works well with distant speakers (when the person speaking isn't right next to the microphone). And only if the task is something more custom and text-related: analyzing reviews, extracting structured data from arbitrary text, generating personalized content, or implementing your own chat assistant — then you use Foundation Models . This is the right tool specifically for flexible textual logic that cannot be reduced to a fixed system function. What Not to Expect from a 3B Parameter Model The Foundation Models framework relies on a compact model. In terms of size, it is roughly 500 times smaller than GPT-4. This fact must be taken into account when choosing which tasks to delegate to it. What this model does well: classifying short text, extracting structured information from arbitrary text, rewriting short fragments, summarization, and answering simple questions based on the context provided in the prompt. In other words, everything I refer to as “mid-level text logic.” What it does poorly or not at all: Mathematical calculations — it often fails even simple arithmetic. This is a known limitation of all language models in general, and Foundation Models are no exception. For calculations, use standard code. Long, multi-step reasoning — the model gets confused, loses intermediate results, and the context window overflows. It’s much better to break a complex task into a chain of short prompts, where each subsequent prompt receives the result of the previous one as context. Factual questions about the real world, especially regarding recent events — here the model will hallucinate with absolute confidence. For such cases, you need Tool Calling connected to a reliable data source. You have to adapt your prompting for this kind of model. Here are a few practical observations from my prototype: First — explicitly state the expected response length. “Explain SwiftUI” yields a vague, lengthy wall of text. “Explain SwiftUI in three bullet points, each no longer than two sentences” yields a concrete and applicable response. Second — specify the format if you need a specific one. “Describe the steps” can result in anything. “Describe the steps as a numbered list, no more than 5 items” gives a structured response. (Though for rigidly structured tasks, it is obviously better to use @Generable ). Third — break down complex tasks. If you want to use a single prompt to do a code review, suggest refactoring, write tests, and generate documentation, the model will get overwhelmed. A chain works much better: first ask about bugs, then ask for refactoring taking the found bugs into account, and finally ask for tests for the refactored code. To iterate on prompts in Xcode 26, there is a handy new tool — the #playground macro, which executes code right in the IDE without recompiling the main project: \ #playground { let session = LanguageModelSession() let result = try await session.respond(to: "Your prompt here") print(result.content) } // Cmd+Return to run, the result is visible immediately When iterating on a prompt, this kind of workflow is a real time-saver. You tweak the phrasing, hit Cmd+Return, and instantly see how the response changes — without needing to spin up the simulator and navigate all the way to the screen containing the model. A Complete SwiftUI Integration Example Let’s bring everything we’ve covered together into a single, working ViewModel . This is code you can drop directly into a real project and adapt to your specific needs: \ @Observable final class AIAssistantViewModel { // The session is created once — during ViewModel initialization private let session = LanguageModelSession(instructions: "You are an assistant for iOS developers. Answer concisely, with code examples." ) var response = "" var isLoading = false var errorMessage: String? func ask(_ prompt: String) async { // Check that the model is available on this device guard LanguageModelSession.isAvailable else { errorMessage = "AI is not available on this device" return } isLoading = true errorMessage = nil response = "" do { // Use streaming for better UX for await partial in session.streamResponse(to: prompt) { await MainActor.run { response += partial.text isLoading = false // the first chunk has arrived — hide the spinner } } } catch LanguageModelError.unsupportedLanguage { errorMessage = "This language is not yet supported" } catch LanguageModelError.contextWindowExceeded { // In a real app, you should create a new session here // with a summary of the previous conversation in the instructions errorMessage = "The conversation is too long, please start over" } catch LanguageModelError.guardrailViolation { errorMessage = "I cannot help with this request" } catch { errorMessage = "Something went wrong. Please try again." } isLoading = false } } And here is the SwiftUI view that uses this ViewModel \ struct AIAssistantView: View { @State private var viewModel = AIAssistantViewModel() @State private var input = "" var body: some View { VStack(spacing: 16) { ScrollView { if viewModel.isLoading { ProgressView() .frame(maxWidth: .infinity) } Text(viewModel.response) .frame(maxWidth: .infinity, alignment: .leading) .animation(.easeInOut, value: viewModel.response) } if let error = viewModel.errorMessage { Text(error) .foregroundStyle(.red) .font(.caption) } HStack { TextField("Ask something...", text: $input) .textFieldStyle(.roundedBorder) Button("Send") { let prompt = input input = "" Task { await viewModel.ask(prompt) } } .disabled(input.isEmpty || viewModel.isLoading) } }.padding() } } \ This code implements a basic chat assistant: you type a message, see the streaming response, and errors are handled and presented to the user in a human-readable format. It is a few steps away from a production-ready solution — you would still need to handle long conversations with automatic summarization, persist history between app launches, and refine the UI during streaming. But the core structure looks exactly like this. Pitfalls I Encountered in the Prototype Here are a few practical observations that might save someone some time: The context window is finite , and it runs out unexpectedly. In my typical short dialogues, it held up for 20–30 message exchanges before hitting contextWindowExceeded . If your app is designed for long conversations, you need a mechanism to automatically summarize the older parts of the history and create a new session with this summary in the instructions . Without this, the user will get a confusing error right in the middle of a chat. Generation speed depends on the device. On my iPhone 15 Pro, the model generated about 30–50 tokens per second for short answers. It should be faster on newer devices, but the numbers heavily depend on the system load — if another Neural Engine workload is running in parallel, the speed drops. This is a strong argument for streaming: it smooths out the perceived latency, even if the total generation time remains the same. guardrailViolation sometimes triggers on surprising things. For example, in my tests, the model refused to discuss certain medical topics even in a strictly educational context. You just have to live with this—built-in guardrails cannot be overridden. If your use case is heavily tied to edge-case topics, Foundation Models might not be the right fit. Performance degrades with context size. The larger the session history, the slower each subsequent request becomes, because the model has to process the entire context every time. This is another argument for periodically “resetting” the session while keeping only a relevant summary. Debugging errors is inconvenient. If something goes wrong inside the model, you get a rather generic error message. In iOS 26, there is no dedicated debug mode for the model that would show you exactly how it interpreted the prompt. This is likely the flip side of strict privacy — but during development, you sometimes crave more observability. The only workaround is iterating quickly via #playground . In Conclusion Foundation Models is not a “revolution” or a “ChatGPT killer,” as it is sometimes pitched in the news. It is a highly practical tool for one specific niche: tasks involving mid-level text logic that need to be solved locally, without the cloud, with guaranteed privacy, and with zero infrastructure costs. For those kinds of tasks, it is arguably the best thing available on iOS right now. You get basic functionality in three lines of code, @Generable for type-safe structured output without writing JSON parsers, Tool Calling to hook up real-world data, and solid error handling and safety out of the box. Everything is on-device, and everything is free for you as a developer. In my prototype of an iOS developer chat assistant, Foundation Models covered an estimated 80% of what I would expect from a cloud API. The remaining 20% were cases requiring fresh factual data or deeper reasoning, and for those, I would have had to either integrate Tool Calling with external APIs or switch to a hybrid architecture with a cloud fallback. The main piece of advice I would give myself before starting: don’t try to use Foundation Models for absolutely everything. First, see if your task can be solved by a higher-level API like Writing Tools, Smart Reply, or Vision. If it can, use it — you’ll write less code and get better system integration. Only when you genuinely need flexible text logic should you reach for Foundation Models, paired with correctly applied @Generable structures and Tool Calling. If you already have experience working with Foundation Models, please share what pitfalls you’ve stumbled upon. I am especially interested in how you solve the problem of context window overflow in long dialogues: I have tried several approaches with automatic summarization, but none have yielded consistently good results so far. \ \ \n \ \ \ \ \ \ \n \ \n \n \ \ \ \ \n \
View original source — Hacker Noon ↗



