DEV Community

ArshTechPro
ArshTechPro

Posted on

On-Device AI in SwiftUI Apps

Running AI models directly on a user's device used to be impractical. Today, with Apple's frameworks and the Neural Engine in modern iPhones and iPads, you can run useful machine learning models locally, no server round-trip required. This article walks through what on-device AI means, why it matters, and how to build it into a SwiftUI app.

Why On-Device AI?

There are three big reasons to keep AI on the device:

Privacy. User data never leaves the phone. For health, finance, or messaging apps, this is often a requirement, not a nice-to-have.

Speed. No network latency. Inference happens in milliseconds on the Neural Engine.

Offline support. The feature works on a plane, in a tunnel, or anywhere without a connection.

The tradeoff is that on-device models are smaller and less capable than large cloud models. The trick is matching the right task to the right tool.

The Apple Frameworks You'll Use

Apple gives you a few layers to work with:

  • Core ML — runs trained models (.mlmodel / .mlpackage) on-device.
  • Vision — image analysis: classification, object detection, text recognition.
  • Natural Language — tokenization, language ID, sentiment analysis.
  • Create ML — trains custom models on your Mac without writing model code.
  • Foundation Models (iOS 26+) — Apple's on-device large language model, accessible via the new framework.

Example 1: Text Classification with the Natural Language Framework

Let's start simple. The Natural Language framework ships with built-in capabilities, no model file needed. Here's sentiment analysis wired into a SwiftUI view.

import SwiftUI
import NaturalLanguage

struct SentimentView: View {
    @State private var text = ""
    @State private var sentiment = "Type something..."

    var body: some View {
        VStack(spacing: 20) {
            TextField("Enter a sentence", text: $text)
                .textFieldStyle(.roundedBorder)

            Button("Analyze") {
                sentiment = analyzeSentiment(text)
            }
            .buttonStyle(.borderedProminent)

            Text(sentiment)
                .font(.title2)
        }
        .padding()
    }

    func analyzeSentiment(_ input: String) -> String {
        let tagger = NLTagger(tagSchemes: [.sentimentScore])
        tagger.string = input

        let (sentimentTag, _) = tagger.tag(
            at: input.startIndex,
            unit: .paragraph,
            scheme: .sentimentScore
        )

        guard let scoreString = sentimentTag?.rawValue,
              let score = Double(scoreString) else {
            return "Could not analyze"
        }

        switch score {
        case 0.3...1.0:   return "Positive (\(score))"
        case -0.3..<0.3:  return "Neutral (\(score))"
        default:          return "Negative (\(score))"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The score ranges from -1.0 (very negative) to 1.0 (very positive). All of this runs locally, instantly.

Example 2: Image Classification with Core ML and Vision

For image tasks, you load a Core ML model and feed images through Vision. You can download a ready-made model like MobileNetV2 from Apple's model gallery and drag the .mlmodel file into Xcode. Xcode auto-generates a Swift class for it.

import SwiftUI
import CoreML
import Vision

@MainActor
class ImageClassifier: ObservableObject {
    @Published var result = "No prediction yet"

    func classify(_ image: UIImage) {
        guard let model = try? VNCoreMLModel(for: MobileNetV2().model),
              let cgImage = image.cgImage else {
            result = "Model or image failed to load"
            return
        }

        let request = VNCoreMLRequest(model: model) { [weak self] request, _ in
            guard let observations = request.results as? [VNClassificationObservation],
                  let top = observations.first else { return }

            Task { @MainActor in
                let confidence = Int(top.confidence * 100)
                self?.result = "\(top.identifier)\(confidence)%"
            }
        }

        let handler = VNImageRequestHandler(cgImage: cgImage)
        DispatchQueue.global(qos: .userInitiated).async {
            try? handler.perform([request])
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

And the view that drives it:

struct ClassifierView: View {
    @StateObject private var classifier = ImageClassifier()
    private let sampleImage = UIImage(named: "sample")!

    var body: some View {
        VStack(spacing: 20) {
            Image(uiImage: sampleImage)
                .resizable()
                .scaledToFit()
                .frame(height: 250)

            Text(classifier.result)
                .font(.headline)

            Button("Classify") {
                classifier.classify(sampleImage)
            }
            .buttonStyle(.borderedProminent)
        }
        .padding()
    }
}
Enter fullscreen mode Exit fullscreen mode

Vision handles the image resizing and pixel format conversion that Core ML expects, which saves you a lot of boilerplate.

Example 3: Apple's On-Device LLM (iOS 26+)

The newest option is the Foundation Models framework, which exposes Apple's on-device language model. It's ideal for summarization, rewriting, and structured generation without sending anything to a server.

import SwiftUI
import FoundationModels

@MainActor
class SummarizerViewModel: ObservableObject {
    @Published var summary = ""
    @Published var isWorking = false

    func summarize(_ text: String) async {
        isWorking = true
        defer { isWorking = false }

        let session = LanguageModelSession()
        let prompt = "Summarize the following in one sentence:\n\(text)"

        do {
            let response = try await session.respond(to: prompt)
            summary = response.content
        } catch {
            summary = "Error: \(error.localizedDescription)"
        }
    }
}

struct SummarizerView: View {
    @StateObject private var vm = SummarizerViewModel()
    @State private var input = ""

    var body: some View {
        VStack(spacing: 16) {
            TextEditor(text: $input)
                .frame(height: 150)
                .border(.gray.opacity(0.3))

            Button("Summarize") {
                Task { await vm.summarize(input) }
            }
            .buttonStyle(.borderedProminent)
            .disabled(vm.isWorking)

            if vm.isWorking {
                ProgressView()
            } else {
                Text(vm.summary)
            }
        }
        .padding()
    }
}
Enter fullscreen mode Exit fullscreen mode

Check device availability before relying on this. Not every device supports the model, so guard against the unavailable case and fall back gracefully (for example, to a cloud API or a simpler local feature).

Practical Tips

A few things worth knowing before you ship:

Run inference off the main thread. Core ML predictions can block the UI. Use background queues or async/await, then hop back to the main actor to update state, as the examples above do.

Watch your app size. Core ML models can be large. Consider on-demand resources or model compression (quantization) to keep download size reasonable.

Profile on real hardware. The Simulator doesn't use the Neural Engine. Always measure performance on a physical device.

Handle the cold start. The first inference loads the model into memory and is slower. Warm it up early if latency matters for the first interaction.

Choosing the Right Approach

On-device AI has moved from experimental to genuinely practical. With these frameworks, you can add intelligent features that respect privacy, work offline, and feel instant, all without standing up a backend.

Top comments (0)