Apple Intelligence is one of the most exciting things to happen to iOS development in years. The Foundation Models framework gives you direct access to an on-device LLM with zero API costs, zero network calls, and full privacy. But here's the hard truth: a huge 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 a large portion of your user base. This article is about how to handle that properly — detecting unavailability, communicating it clearly to users, and falling back gracefully so your app stays useful for everyone.
Who Can't Use Apple Intelligence?
Let's be precise about this, because the numbers matter.
iOS 26.3 (released February 11, 2026) runs on iPhone 11 and later — that's any device with an A13 chip or newer. But Apple Intelligence requires an A17 Pro chip or newer, which means only:
- 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
So an iPhone 15 (standard) running iOS 26.3 supports all the Liquid Glass UI changes, but gets zero Foundation Models access. Same for anything older. On top of that, even eligible devices need:
- Apple Intelligence enabled in Settings (it's opt-in)
- 7 GB of free storage on the device
- Device and Siri language set to a supported language
- The model fully downloaded (it downloads in the background after enabling)
Bottom line: even among users on supported hardware, not everyone will have Apple Intelligence ready to go. You cannot assume availability.
The Three Unavailability Cases
The Foundation Models framework gives you exactly three reasons why the model might not be available, surfaced through SystemLanguageModel.default.availability:
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 needs a different response from your app. They are not all the same problem.
Building a Proper Fallback Strategy
Think of these three cases as three separate UX problems.
Case 1: Device Not Eligible
This is permanent. The user's hardware will never support Apple Intelligence. Don't show them a spinner. Don't show a "check back later" message. Show them a functional experience that doesn't rely on the model at all.
case .unavailable(.deviceNotEligible):
// Serve a non-AI version of the feature
showBasicTextSummarizer()
What "basic version" means depends on your feature. For a smart journaling app, you might skip auto-tagging and let the user tag manually. For a writing assistant, you might offer simpler preset templates instead of generated suggestions. The key is: the feature should still work, just without the AI enhancement.
Case 2: Apple Intelligence Not Enabled
This one is different — the hardware supports it, but the user hasn't opted in. You can prompt the user to enable it. But be thoughtful about how you do this. Don't block the UI. Don't show it repeatedly. Show it once, explain the benefit clearly, and deep link directly to the setting.
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
Apple Intelligence is enabled at Settings → Apple Intelligence & Siri. You can open Settings directly with UIApplication.openSettingsURLString, but you can't deep link to that exact screen — the user has to navigate there themselves.
Case 3: Model Not Ready
This is temporary. The model is downloaded in the background after a user enables Apple Intelligence, and it can take a while. The right response here is to wait and retry — not to permanently fall back.
case .unavailable(.modelNotReady):
showLoadingState(message: "AI features are warming up. This only takes a moment.")
scheduleAvailabilityCheck()
For the retry logic, you can periodically re-check SystemLanguageModel.default.availability. A simple approach is to use a Timer or Task with a delay:
func scheduleAvailabilityCheck() {
Task {
try? await Task.sleep(for: .seconds(10))
await checkAndUpdateAvailability()
}
}
Don't poll too aggressively — once every 10–30 seconds is fine while the user is actively in that screen.
Putting It Together: A Clean Architecture
Here's a practical pattern that keeps your feature code clean and handles all three cases from a single place.
import FoundationModels
import SwiftUI
enum AIAvailabilityState {
case available
case unsupportedDevice
case notEnabled
case modelLoading
}
@Observable
class AIFeatureManager {
private(set) var state: AIAvailabilityState = .modelLoading
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:
state = .unsupportedDevice
}
}
private func scheduleRetry() {
Task {
try? await Task.sleep(for: .seconds(15))
refreshAvailability()
}
}
}
And in your view:
struct SmartFeatureView: View {
@State 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...")
}
}
}
This keeps your views thin. Each state gets its own view. And when the model becomes available, refreshAvailability() updates the state and SwiftUI re-renders automatically.
What Your Fallback UI Should Actually Do
A fallback isn't just "hide the AI button." A good fallback means the feature still delivers value without the model. Here are patterns for common use cases:
Smart text summarization → Fall back to a character-count preview or a "show more/less" toggle. Not as smart, but still useful.
Auto-tagging / content classification → Fall back to a curated list of tags the user picks from manually. Or skip tagging entirely and search by keyword.
AI-generated suggestions → Fall back to a set of hand-written preset options. Less personalised, but still functional.
Contextual chat assistant → Fall back to an FAQ-style interface or a link to your help docs.
The goal is: a user on an iPhone 14 should open your app and find a working, useful feature — not a broken screen or a wall of text explaining why their device isn't good enough.
A Note on the @unknown default
Always include @unknown default in your switch statement. Apple's API is still relatively new and they may add new unavailability reasons in future OS versions. If you omit it, a new case could cause a compile-time warning and — more importantly — unexpected runtime behaviour. Treat any unknown case the same as deviceNotEligible: assume the model isn't coming, and serve the basic experience.
Testing Without an Eligible Device
Testing all three unavailability states in Simulator can be tricky. Here's what works in practice:
-
.deviceNotEligible: Use an iPhone simulator that's older than iPhone 15 Pro (e.g. iPhone 14). -
.appleIntelligenceNotEnabled: On a supported simulator, go to Settings → Apple Intelligence & Siri and toggle it off. -
.modelNotReady: Harder to simulate reliably. You can mock this in yourAIFeatureManagerfor testing by injecting a fake availability value.
For unit testing, make SystemLanguageModel.default.availability mockable by abstracting it behind a protocol:
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 MockChecker in your tests, LiveChecker in production. This lets you write clean unit tests for every availability state without needing a physical device.
The Bigger Picture
The developers who ship great apps right now are the ones who treat Foundation Models as a progressive enhancement — something that makes the experience better for those who have it, without breaking anything for those who don't.
Build the baseline first. Then layer the intelligence on top.
Requirements: iOS 26+ · Xcode 26+
Apple Intelligence devices: iPhone 15 Pro/Max, all iPhone 16 and 17 models
Top comments (1)
Apple Intelligence devices: iPhone 15 Pro/Max, all iPhone 16 and 17 models