Skip to content

Location Intelligence for Apps with Foundation Models

Free

Starter

Standard

Professional

With the release of iOS 26, Apple now includes a Foundation Models framework. The new framework gives developers access to an on-device LLM capable of classification, structured content generation, and of course, chat-style conversations.

Apple's Foundation Models are not a replacement for the "big" LLMs like GPT or Claude, but are part of an increasingly popular trend of smaller, more focused LLMs that can be run locally. These models are available on most recent Apple devices, and run on-device without sending everything over the internet to a third party.

LLMs can be useful for broad general knowledge, but they don't know about the current time or how far a hotel is from the airport. In this guide, we'll show how to use tools to overcome these gaps, and reduce the risk of hallucinations when building novel user experiences with AI.

Prerequisites

The Foundation Models are available on recent Apple hardware running iOS 26, macOS 26, and iPadOS 26. The requirements can be a bit confusing, so we've put together a quick checklist.

First, here's what you need to develop using the new frameworks:

  • A Mac with Xcode 26 or greater installed
  • Either a device (iPhone, iPad, Vision Pro) running OS version 26, or a Mac running macOS 26

Second, only specific devices are capable of using the Foundation Models framework. Here's a checklist for users (including your test device) of the Foundation Models:

  • At least version 26 of the respective OS (macOS, iOS, etc.)
  • Apple Intelligence enabled (note: not all languages are supported)

How did they fit an LLM on a phone?

Apple's models are a LOT smaller than ChatGPT, or even other "locally runnable" LLMs like Llama. They are also broadly single-language models, and require your device to be set to a supported language. This has some tradeoffs, but it allows Apple to ship a more capable model for each language given the minimal size budget (at launch in September 2026, the US English model was around 3GB).

To save space and allow updates, the models are not directly bundeld with the OS, but are downloaded automatically in the background by Apple Intelligence (when enabled).

Meet the Models

You can access the on-device models using the SystemLanguageModel class. The .default model is pre-configured for general use with a prompt-based workflow. The defaults performed the best for all the demos we've built internally, so we recommend starting there before experimenting.

Check for model availability

As noted above, Foundation Models are not available on all devices. You should check the availability property before using the model in your app. (There is also an isAvailable boolean, but that is far less informative the availability enum tells you why it's not available).

The most common way to interact with the models is via a LanguageModelSession. Back-and-forth interactions with LLMs rely on building up a shared context of prompts and responses from each side. The LanguageModelSession handles all this for you in an easy-to-use interface. It also lets you configure tools and instructions: natural language text that influence the model's behavior.

Here's a typical constructor call:

let session = LanguageModelSession(
    model: .default,
    tools: [CoarseGeocode(), GetCurrentTime()],
    instructions: "Use tools to search for location coordinates and get the current time info.")

Building Tools

Now that we've mentioned tools a few times, and why they are useful, let's dig into the practical bits.

A tool is code that the model can call while generating a response to a prompt. In the Foundation Models framework, we specify what tools are available when creating a new LanguageModelSession.

You've probably seen the memes of an LLM not knowing what time it is. This is because LLMs don't really "know" anything outside the statistical relationships established during their training. That is to say, they can't give you useful information about many things including the time of day or the "best" cycling route to work.

Tools are one solution to this problem. A tool can access dynamic sources that are good at answering specific questions outside the training set.

Your First Tool: Telling the Time

To build a tool for the Foundation Models framework, define a class or struct, just like you would when writing other functionality in Swift. The only requirement is that it conforms to the Tool protocol. The tool can be stateless, or change its behavior based on past calls; there is plenty of room for creativity.

Let's continue with the example above to build our first tools. LLMs can't tell the current time without a tool. So let's build one that helps with that!

import FoundationModels
import StadiaMaps

struct GetCurrentTime: Tool {
    // Apple recommends concise descriptions (~1 sentence) due to context limits
    let description = "Find the time and UTC offsets at any point on earth."

    @Generable
    struct Arguments {
        @Guide(description: "The latitude coordinate")
        let latitude: Double
        @Guide(description: "The longitude coordinate")
        let longitude: Double
    }

    // Sadly, Dates don't conform to the required protocol,
    // so we return strings.
    func call(arguments: Arguments) async throws -> String {
        do {
            let res = try await GeospatialAPI.tzLookup(lat: arguments.latitude, lng: arguments.longitude)

            let dstOffset = if res.dstOffset != 0 {
                "The effective offset for DST/summer time is \(res.dstOffset)"
            } else {
                "No special offsets are in effect."
            }

            return "The current time is \(res.localRfc2822Timestamp). \(dstOffset)"
        } catch StadiaMaps.ErrorResponse.error(let code, _, _, _) where code == 401 {
            return "You need to configure a Stadia Maps API key in Foundation_ModelsApp.swift"
        }
    }
}

Breaking Down the Tool Implementation

The tool starts off with a description. This is one of the most important parts of effective tool design. Apple recommends you keep it short since the models have a relatively small context window (you can think of this like a model's short-term memory or attention span).

Next come the arguments. The skeleton is a basic struct outlining what you need from the model. Your call function will receive one of these with values filled in based on the prompt and conversation history. The model uses the structure you define to generate the arguments in a structured form. Pretty cool, right?

The easiest way to specify your tool's arguments is using the @Generable and @Guide macros. The @Guide macro lets you annotate each field of your arguments struct with a description. As with the tool description, you should keep this short and to the point.

Finally, the call method takes the Arguments struct you specified, and produces a value. We've used a string here, but you can produce other values here too as long as they conform to the ConvertibleFromGeneratedContent protocol. This tool simply makes an API call to the Stadia maps Time Zone API using our Swift SDK.

Adding a Second Tool: Coarse Geocoding

You may have noticed that this tool operates on raw latitude and longitude. But what if you want to handle natural language place names too? Let's build a second tool to translate a place name like "Tallinn" into coordinates.

struct CoarseGeocode: Tool {
    let description = "Looks up coordinates for countries, cities, states, and other broad areas."

    @Generable
    struct Arguments {
        @Guide(description: "The name of the place (country, city, state, etc.)")
        let query: String
    }

    func call(arguments: Arguments) async throws -> String {
        do {
            let res = try await GeocodingAPI.searchV2(text: arguments.query, layers: [.coarse])

            return res.features.compactMap { feature in
                guard let point = feature.geometry else {
                    return nil
                }
                return "\(feature.properties.name)\n\tlat: \(point.coordinates[1]), lon: \(point.coordinates[0])"
            }.joined(separator: "\n===\n")
        } catch StadiaMaps.ErrorResponse.error(let code, _, _, _) where code == 401 {
            return "You need to configure a Stadia Maps API key in Foundation_ModelsApp.swift"
        }
    }
}

This is pretty similar to the first tool. The output of this is a bit more complex though, so let's look at how the call method structures its result. It's actually just a big string!

There are several approaches to returning data to the model, but we have found that LLMs are large language models after all. They actually do very well with loosely structured plain text. So here we just return multiple places and a minimally interesting set of attributes in a format like you'd see in many terminal programs.

Recap

To quickly recap this section, a tool consists of three main parts: a string description, an Arguments struct, and a call method.

The first two attributes teach the model what your tool does and what arguments to provide. The model is still in control of if it will call your tool to fulfill a request, but if it does, it will definitely have the Arguments you specified.

The call method does the actual work and produces a response. The method is async, and we take advantage of that to make network calls to the Stadia Maps API, but you can execute any Swift code here.

The LanguageModelSession, your main interface to the models, remembers the conversational context and initializes the model with any tools you give it during init.

Bringing it all together, given the above tool definitions, a simple conversation might look something like this:

// Create a session (this might be a @State var on a view in a SwiftUI app)
let session = LanguageModelSession(
    // Use the default model
    model: .default,
    // Set up the two tools from above
    tools: [CoarseGeocode(), GetCurrentTime()],
    instructions: "Use tools to search for location coordinates and get the current time info.")

// Not necessarily in the same method...
do {
    let res = try await session.respond(to: "What time is it in Tallinn?")
    result = res.content
} catch {
    result = "Error: \(error)"
}

Effective tool design

Tool design raises some interesting questions and tradeoffs. If you've designed tools for other SDKs, including for MCPs, you have a head start already. Here are a few lessons we've learned developing with the Foundation Models.

Composability vs Single Tools?

When designing tools for the these models, focus on simplicity. Each tool should have a clear and narrow purpose. But that doesn't always mean splitting things on external API boundaries!

In other styles of software development, you may be used to designing small units for composability and modularity. That instinct isn't always correct when designing for LLMs: you can have too many tools. With the Foundation Models, the practical limit is lower than that of larger models.

A good rule of thumb is "am I likely to need these tools in isolation?" Using the example above, if you know that you'll probably never need the coordinate-based lookup separately, you should probably combine the two tools into a single, simpler one. On the flip side, having multiple tools can enable the model to make some combinations you may not have designed for upfront. If you're unsure, stick with a single tool when targeting these on-device models.

Getting the Context Right

The description and Arguments affect how effective a model is at using your tool. You may need to go through several revisions of the description to weed out edge cases where the model doesn't call your tool. With Arguments, there is a bit less to say beyond "less is more." The fewer chances there are for mistakes (the argument values are generated), the better.

Minimize the Output

When designing a tool, think carefully about what data needs to go in the result. This is especially important when your results are coming from an external source such as an API. LLMs are capable of understanding JSON and other structured output, but don't abuse that. On-device models in particular will be overwhelmed by a full geocoding or routing response, so trim the output to just the bits you need.

Testing

While the Foundation Models require specific software versions and Apple Intelligence support, you can unit test them with XCTest and Swift Testing! Once you feel like you have a solid baseline, codify that in tests that exercise your prompts and verify the tool calls.

For example:

  • Write out 25 prompts that you think should trigger a tool.
  • Mock the tool call function.
  • Set up a test function using XCTestExpectation and waitForExpectations to verify that the tool gets called
  • [Optional] assert some properties about the output (this gets a bit fuzzy though)

Since LLMs are inherently nondeterministic, there are some limits to this approach, but with the right design, tests can help you work towards better tools.

Safety

Finally, no discussion of AI is complete without considering safety.

Apple has already gone to great lengths to ensure that the models have guardrails so they don't suggest something harmful or illegal. You don't have much control over this type of safety concern unless you work at Apple, but as a tool developer you do need to be careful that your code is safe for any input. Be sure to take appropriate care with the info your tool receives from the model.

Wrapping up

The Foundation Models framework brings on-device, private AI to millions of Apple devices. While the models are small, tools provide an easy extension point to bring in specialized, up-to-date data on demand.

When designing your next user experience, consider which specific slices of the full API are relevant to your users. Then design your tools around that.

Ready to take the next step? You can start building with our Swift SDK today for free; no credit card required.

Get Started With a Free Account