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

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

Source: Dev.to

Who Can't Use Apple Intelligence? ## The Three Unavailability Cases ## Building a Proper Fallback Strategy ## Case 1: Device Not Eligible ## Case 2: Apple Intelligence Not Enabled ## Case 3: Model Not Ready ## Putting It Together: A Clean Architecture ## What Your Fallback UI Should Actually Do ## A Note on the @unknown default ## Testing Without an Eligible Device ## The Bigger Picture 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. 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: 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: Bottom line: even among users on supported hardware, not everyone will have Apple Intelligence ready to go. You cannot assume availability. The Foundation Models framework gives you exactly three reasons why the model might not be available, surfaced through SystemLanguageModel.default.availability: Each case needs a different response from your app. They are not all the same problem. Think of these three cases as three separate UX problems. 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. 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. 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. 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. 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. 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: Don't poll too aggressively — once every 10–30 seconds is fine while the user is actively in that screen. Here's a practical pattern that keeps your feature code clean and handles all three cases from a single place. 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. 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. 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 all three unavailability states in Simulator can be tricky. Here's what works in practice: For unit testing, make SystemLanguageModel.default.availability mockable by abstracting it behind a protocol: 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 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 Templates let you quickly answer FAQs or store snippets for re-use. Apple Intelligence devices: iPhone 15 Pro/Max, all iPhone 16 and 17 models Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK: 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 } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: 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 } CODE_BLOCK: 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 } CODE_BLOCK: case .unavailable(.deviceNotEligible): // Serve a non-AI version of the feature showBasicTextSummarizer() Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: case .unavailable(.deviceNotEligible): // Serve a non-AI version of the feature showBasicTextSummarizer() CODE_BLOCK: case .unavailable(.deviceNotEligible): // Serve a non-AI version of the feature showBasicTextSummarizer() CODE_BLOCK: 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 Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: 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 CODE_BLOCK: 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 CODE_BLOCK: case .unavailable(.modelNotReady): showLoadingState(message: "AI features are warming up. This only takes a moment.") scheduleAvailabilityCheck() Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: case .unavailable(.modelNotReady): showLoadingState(message: "AI features are warming up. This only takes a moment.") scheduleAvailabilityCheck() CODE_BLOCK: case .unavailable(.modelNotReady): showLoadingState(message: "AI features are warming up. This only takes a moment.") scheduleAvailabilityCheck() CODE_BLOCK: func scheduleAvailabilityCheck() { Task { try? await Task.sleep(for: .seconds(10)) await checkAndUpdateAvailability() } } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: func scheduleAvailabilityCheck() { Task { try? await Task.sleep(for: .seconds(10)) await checkAndUpdateAvailability() } } CODE_BLOCK: func scheduleAvailabilityCheck() { Task { try? await Task.sleep(for: .seconds(10)) await checkAndUpdateAvailability() } } CODE_BLOCK: 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() } } } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: 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() } } } CODE_BLOCK: 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() } } } CODE_BLOCK: 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...") } } } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: 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...") } } } CODE_BLOCK: 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...") } } } CODE_BLOCK: protocol LanguageModelAvailabilityChecker { var availability: SystemLanguageModel.Availability { get } } struct LiveChecker: LanguageModelAvailabilityChecker { var availability: SystemLanguageModel.Availability { SystemLanguageModel.default.availability } } struct MockChecker: LanguageModelAvailabilityChecker { var availability: SystemLanguageModel.Availability } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: protocol LanguageModelAvailabilityChecker { var availability: SystemLanguageModel.Availability { get } } struct LiveChecker: LanguageModelAvailabilityChecker { var availability: SystemLanguageModel.Availability { SystemLanguageModel.default.availability } } struct MockChecker: LanguageModelAvailabilityChecker { var availability: SystemLanguageModel.Availability } CODE_BLOCK: protocol LanguageModelAvailabilityChecker { var availability: SystemLanguageModel.Availability { get } } struct LiveChecker: LanguageModelAvailabilityChecker { var availability: SystemLanguageModel.Availability { SystemLanguageModel.default.availability } } struct MockChecker: LanguageModelAvailabilityChecker { var availability: SystemLanguageModel.Availability } - iPhone 15 Pro / iPhone 15 Pro Max - iPhone 16 / 16 Plus / 16 Pro / 16 Pro Max - iPhone 17 / 17 Pro / 17 Pro Max / iPhone Air - 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) - .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 your AIFeatureManager for testing by injecting a fake availability value. - Location London - Work Work on mobile application as Tech expert - Joined Jun 11, 2025