How to Fall Back Gracefully When Apple Intelligence Isn't Available

Published: (February 20, 2026 at 07:24 PM EST)
9 min read
Source: Dev.to

Source: Dev.to

Introduction

Apple Intelligence is one of the most exciting developments for iOS developers in years. The Foundation Models framework gives you direct, on‑device access to a large language model—no API costs, no network calls, and full privacy.

But the hard truth is that a large chunk of your users can’t run it. If you just drop a LanguageModelSession() into your app without any checks, you’ll ship broken experiences to many devices.

Below is a clean, ready‑to‑copy guide that shows:

  • Which devices can’t use Apple Intelligence
  • The three availability states the framework reports
  • How to handle each case in the UI
  • A simple, reusable architecture for keeping your feature code tidy

Who Can’t Use Apple Intelligence?

iOS versionMinimum hardwareApple Intelligence requirement
iOS 26.3 (Feb 11 2026)iPhone 11 + (A13 or newer)A17 Pro or newer

Only the following devices meet the hardware requirement:

  • iPhone 15 Pro / iPhone 15 Pro Max
  • iPhone 16 / 16 Plus / 16 Pro / 16 Pro Max
  • iPhone 16e
  • iPhone 17 / 17 Pro / 17 Pro Max / iPhone Air

All other devices—including the standard iPhone 15—run iOS 26.3 but receive zero Foundation Models access.

Even on eligible hardware you still need:

  1. Apple Intelligence enabled in Settings (opt‑in)
  2. ≥ 7 GB of free storage
  3. Device and Siri language set to a supported language
  4. The model fully downloaded (it downloads in the background after enablement)

Bottom line: You cannot assume the model is available just because the OS version is recent.


The Three Unavailability Cases

SystemLanguageModel.default.availability can be one of three unavailable cases (plus the happy‑path .available):

import FoundationModels

switch SystemLanguageModel.default.availability {
case .available:
    // Good to go
case .unavailable(.deviceNotEligible):
    // A13 or older chip – Foundation Models will never work here
case .unavailable(.appleIntelligenceNotEnabled):
    // Compatible device, but user hasn't turned on Apple Intelligence
case .unavailable(.modelNotReady):
    // Compatible + enabled, but model is still downloading
@unknown default:
    // Future‑proof: handle any new cases Apple might add
    break
}

Each case requires a different UX response.


Building a Proper Fallback Strategy

Think of the three cases as three separate UX problems.

Case 1 – Device Not Eligible

Permanent limitation. The hardware will never support Apple Intelligence.
Do not show spinners or “check back later” messages. Instead, present a fully functional, non‑AI version of the feature.

case .unavailable(.deviceNotEligible):
    // Serve a non‑AI version of the feature
    showBasicTextSummarizer()

Example: In a smart‑journaling app you might skip auto‑tagging and let the user tag manually. In a writing assistant you could offer preset templates instead of generated suggestions.

Case 2 – Apple Intelligence Not Enabled

Hardware is capable, but the user hasn’t opted in. You can prompt them, but be gentle:

case .unavailable(.appleIntelligenceNotEnabled):
    showEnablementBanner(
        message: "Enable Apple Intelligence in Settings to unlock AI‑powered suggestions.",
        settingsURL: URL(string: UIApplication.openSettingsURLString)!
    )
    // Still show the basic version of the feature below the banner
  • The banner appears once and explains the benefit clearly.
  • Tapping it opens Settings → Apple Intelligence & Siri via UIApplication.openSettingsURLString.
  • You cannot deep‑link directly to that sub‑screen, so the user must navigate there themselves.

Case 3 – Model Not Ready

Temporary state while the model is downloading after enablement. Here you should wait and retry, not fall back permanently.

case .unavailable(.modelNotReady):
    showLoadingState(message: "AI features are warming up. This only takes a moment.")
    scheduleAvailabilityCheck()

A simple retry implementation:

func scheduleAvailabilityCheck() {
    Task {
        try? await Task.sleep(for: .seconds(10))   // 10‑second delay
        await checkAndUpdateAvailability()
    }
}

Poll no more often than every 10–30 seconds while the user stays on the screen.


Putting It Together: A Clean Architecture

Below is a minimal, reusable pattern that centralises all availability handling and keeps your UI code tidy.

import FoundationModels
import SwiftUI

// MARK: - Availability State

enum AIAvailabilityState {
    case available
    case unsupportedDevice
    case notEnabled
    case modelLoading
}

// MARK: - Feature Manager

@Observable
final class AIFeatureManager {
    private(set) var state: AIAvailabilityState = .modelLoading

    init() {
        refreshAvailability()
    }

    /// Checks the system availability and updates `state`.
    func refreshAvailability() {
        switch SystemLanguageModel.default.availability {
        case .available:
            state = .available
        case .unavailable(.deviceNotEligible):
            state = .unsupportedDevice
        case .unavailable(.appleIntelligenceNotEnabled):
            state = .notEnabled
        case .unavailable(.modelNotReady):
            state = .modelLoading
        @unknown default:
            state = .modelLoading
        }
    }

    /// Periodically re‑checks availability (used for the “model not ready” case).
    func scheduleRecheck() {
        Task {
            try? await Task.sleep(for: .seconds(15))
            await MainActor.run { refreshAvailability() }
        }
    }
}

// MARK: - SwiftUI View Example

struct AIFeatureView: View {
    @StateObject private var manager = AIFeatureManager()

    var body: some View {
        VStack {
            switch manager.state {
            case .available:
                AIEnabledView()               // Your full AI‑powered UI
            case .unsupportedDevice:
                BasicFeatureView()           // Non‑AI fallback
            case .notEnabled:
                VStack {
                    BasicFeatureView()
                    EnableBanner()
                }
            case .modelLoading:
                LoadingView(message: "AI is warming up…")
                    .onAppear { manager.scheduleRecheck() }
            }
        }
        .animation(.default, value: manager.state) // smooth transitions
    }
}

// MARK: - UI Helpers (simplified)

struct EnableBanner: View {
    var body: some View {
        Button {
            if let url = URL(string: UIApplication.openSettingsURLString) {
                UIApplication.shared.open(url)
            }
        } label: {
            Text("Enable Apple Intelligence in Settings")
                .font(.subheadline)
                .padding()
                .background(Color.blue.opacity(0.1))
                .cornerRadius(8)
        }
    }
}

struct LoadingView: View {
    let message: String
    var body: some View {
        VStack(spacing: 12) {
            ProgressView()
            Text(message)
                .font(.footnote)
                .foregroundColor(.secondary)
        }
        .padding()
    }
}

How it works

  1. AIFeatureManager encapsulates all system checks and exposes a single state value.
  2. The SwiftUI view reacts to state and shows the appropriate UI:
    • Full AI feature (AIEnabledView)
    • Basic fallback (BasicFeatureView)
    • Enable‑banner + basic UI (EnableBanner)
    • Loading spinner while the model downloads (LoadingView)
  3. When the model is still downloading, the view triggers scheduleRecheck() to poll again after a short delay.

You can reuse AIFeatureManager across multiple screens or even wrap it in a @EnvironmentObject if many parts of your app need the same availability information.


TL;DR Checklist

SituationWhat to showAction
Device not eligibleBasic, non‑AI UINo prompts, just fallback
Apple Intelligence not enabledBasic UI + one‑time banner linking to SettingsOffer enablement, don’t block
Model not readyLoading spinner + retry logicPoll SystemLanguageModel.default.availability until .available
All goodFull AI‑powered UIUse LanguageModelSession as intended

By detecting the exact unavailability reason and responding appropriately, you keep every user’s experience smooth—whether they have the latest iPhone 17 Pro Max or an iPhone 12. Happy coding!

Handling Apple Intelligence Availability

When you integrate Apple Intelligence (or any foundation model) you’ll encounter three possible unavailability states:

Unavailability ReasonMeaningTypical UI Response
deviceNotEligibleThe device can’t run the model (e.g., older iPhone)Show a fallback UI that works everywhere
appleIntelligenceNotEnabledThe user has disabled Apple Intelligence in SettingsPrompt the user to enable it
modelNotReadyThe model is still loading on the deviceShow a loading indicator and retry later

Below is a concise way to handle these states in SwiftUI.

1️⃣ Model‑layer: Detecting Availability

import SwiftUI
import FoundationModels

final class AIFeatureManager: ObservableObject {
    enum State {
        case available
        case unsupportedDevice
        case notEnabled
        case modelLoading
    }

    @Published private(set) var state: State = .unsupportedDevice

    init() {
        refreshAvailability()
    }

    func refreshAvailability() {
        switch SystemLanguageModel.default.availability {
        case .available:
            state = .available
        case .unavailable(.deviceNotEligible):
            state = .unsupportedDevice
        case .unavailable(.appleIntelligenceNotEnabled):
            state = .notEnabled
        case .unavailable(.modelNotReady):
            state = .modelLoading
            scheduleRetry()
        @unknown default:
            // Future‑proofing: treat unknown cases as unsupported
            state = .unsupportedDevice
        }
    }

    private func scheduleRetry() {
        Task {
            try? await Task.sleep(for: .seconds(15))
            refreshAvailability()
        }
    }
}

2️⃣ View‑layer: Rendering the Correct UI

struct SmartFeatureView: View {
    @StateObject private var aiManager = AIFeatureManager()

    var body: some View {
        switch aiManager.state {
        case .available:
            AIEnhancedView()
        case .unsupportedDevice:
            BasicFallbackView()
        case .notEnabled:
            EnablePromptView {
                aiManager.refreshAvailability()
            }
        case .modelLoading:
            LoadingView(message: "AI features are getting ready…")
        }
    }
}

Each state gets its own dedicated view, keeping the UI thin and maintainable. When the model becomes available, refreshAvailability() updates state and SwiftUI automatically re‑renders.


What Your Fallback UI Should Actually Do

A fallback isn’t just “hide the AI button.” It should still deliver value without the model. Here are common patterns:

FeatureFallback Idea
Smart text summarizationShow a character‑count preview or a “show more/less” toggle.
Auto‑tagging / content classificationLet the user pick from a curated list of tags manually, or rely on keyword search.
AI‑generated suggestionsProvide a set of hand‑written preset options.
Contextual chat assistantSwitch to an FAQ‑style interface or a link to help docs.

Goal: A user on an iPhone 14 should open your app and see a working, useful feature—not a broken screen or a wall of text explaining why their device isn’t good enough.


A Note on @unknown default

Because Apple’s API is still evolving, always include @unknown default in your switch. If a new unavailability reason appears in a future OS version, the compiler will warn you, and you can safely treat it as “unsupported device.”


Testing Without an Eligible Device

Simulating the three unavailability states can be done as follows:

StateHow to Simulate
deviceNotEligibleRun the app on an older simulator (e.g., iPhone 14).
appleIntelligenceNotEnabledOn a supported simulator, go to Settings → Apple Intelligence & Siri and toggle it off.
modelNotReadyMock the availability in AIFeatureManager for testing.

Making Availability Mockable

protocol LanguageModelAvailabilityChecker {
    var availability: SystemLanguageModel.Availability { get }
}

struct LiveChecker: LanguageModelAvailabilityChecker {
    var availability: SystemLanguageModel.Availability {
        SystemLanguageModel.default.availability
    }
}

struct MockChecker: LanguageModelAvailabilityChecker {
    var availability: SystemLanguageModel.Availability
}

Inject LiveChecker in production and MockChecker in unit tests. This lets you verify UI behavior for every availability state without a physical device.


The Bigger Picture

Great apps treat foundation models as progressive enhancements—they improve the experience for capable devices while still functioning perfectly on older hardware.

  1. Build the baseline (fallback UI) first.
  2. Layer the intelligence on top once the baseline is solid.

Requirements

  • iOS 26+
  • Xcode 26+

Apple Intelligence‑compatible devices

  • iPhone 15 Pro / Pro Max
  • All iPhone 16 and iPhone 17 models

Happy coding! 🎉

0 views
Back to Blog

Related posts

Read more »