Back in iOS 17, Apple quietly shipped something remarkable: a fully on-device nudity detector, free, no ML expertise required, open to any app that adds a single entitlement. It is called SensitiveContentAnalysis. Three years later, with iOS 26 shipping and iOS 27 in beta, it remains one of the most overlooked frameworks in the SDK.
After spending a few weeks building an open-source SDK around it, I think I understand why. The framework is genuinely good, but it comes with constraints Apple does not put on the label. This post covers what it really does, where it disappoints, and where it is the right tool for the job.
What it is
SCSensitivityAnalyzer takes an image (file URL or CGImage) or a video file and answers one question: does this contain nudity? Analysis runs entirely on device. The framework uploads nothing to anyone; whatever your report flow does with the media afterward is a separate, app-level choice.
import SensitiveContentAnalysis
let analyzer = SCSensitivityAnalyzer()
let analysis = try await analyzer.analyzeImage(at: imageURL)
if analysis.isSensitive {
// blur it, block it, warn the user
}
Your app needs one entitlement: com.apple.developer.sensitivecontentanalysis.client set to analysis. No special approval process, just add the capability.
This is the same machinery behind Communication Safety in Messages and the system-wide Sensitive Content Warning. The practical fit is app-owned media: incoming chat images, classroom uploads, profile pictures, video attachments. It is not a screen filter and cannot touch content in other apps.
The catch in the fine print
Before analyzing anything you must check analyzer.analysisPolicy. If it returns .disabled, the framework will not detect anything. And here is the part that surprises every developer who tries it:
For adults, detection is off unless the user opted in.
analysisPolicy is .disabled unless your app has the entitlement and at least one user-side protection is active:
- The user enabled Sensitive Content Warning (Settings > Privacy & Security), which is off by default for adults
- Communication Safety is active through Screen Time (on by default for children under 18 with Child Accounts in Family Sharing on current Apple software)
There is also no way to deep-link users to that Settings pane. UIApplication.openSettingsURLString opens your app's own settings page, and the undocumented prefs: URL schemes are App Review bait.
So if you ship a general-audience chat app and integrate this framework, the honest expectation is: for most of your adult users, analysis will be unavailable until they flip a switch they have never heard of. Your code must treat "unavailable" as a normal state with designed behavior, not an error.
Where it genuinely shines
This sounds damning, but there are three contexts where SensitiveContentAnalysis is not just viable but the right choice.
End-to-end encrypted chat. Server-side moderation cannot inspect E2E-encrypted media. If you want automatic nudity detection in an E2E messenger without weakening encryption, it has to happen on device, and SensitiveContentAnalysis is Apple's built-in way to do that. This is exactly how Apple handles it in Messages.
Family, teen, and classroom apps. Communication Safety is on by default for children under 18 with Child Accounts in Family Sharing on current Apple software. In these apps the analyzer is actually likely to be available, and your audience is exactly who the feature exists for.
App Review's UGC requirements. Guideline 1.2 expects user-generated-content apps to ship filtering, reporting, and blocking. On-device detection with a blur and report flow covers the filtering side with zero servers; you still need user blocking and someone who acts on reports.
Testing without explicit content
Apple thought about the awkward part: how do you test a nudity detector without putting nudity in your repo? There is an official flow using a configuration profile and a QR code that makes the analyzer return a positive result for a harmless test image. No explicit fixtures, CI stays clean.
One gotcha from my own device testing: downloading the profile is not enough. It sits inactive until you go to Settings > General > VPN & Device Management and tap Install. I spent a confused stretch watching the test image sail through unblurred before realizing the profile was never actually installed. If detection looks dead on a real device, check there first.
What's new since iOS 17
iOS 26 added SCVideoStreamAnalyzer for live streams and video calls. iOS 27, in beta as I write this, adds SCSensitivityAnalysis.detectedTypes with categories like sexuallyExplicit and goreOrViolence, so policy can finally differ by content type. Both are availability-gated, and detectedTypes only compiles against the Xcode 27 SDK, so adopting them takes some #if compiler care.
The missing layer
For iOS 17 through iOS 26, image analysis gives you isSensitive: a boolean. Everything an actual product needs sits on top of that boolean:
- a policy layer (what should happen for sensitive, unknown, unavailable, failed?)
- caching so you do not re-analyze the same file on every scroll
- correct handling when the user toggles the setting mid-session
- blur, reveal, block, and report UI that fails closed while scanning
- a way to test all of those states in previews and unit tests
I built that layer as an open-source Swift package: SafeMediaKit (MIT, iOS 17+, macOS 14+, Mac Catalyst 17+, zero dependencies, no telemetry).
import SafeMediaKit
import SwiftUI
let engine = SafeMediaEngine(
analyzer: AppleSensitiveContentAnalyzer(),
cache: InMemorySafeMediaVerdictCache()
)
SafeMediaImage(
url: imageURL,
engine: engine,
context: .incomingMessage,
policy: .teenMessaging
)
That gives you scanning with a placeholder (the raw image is never handed to the view until a decision exists), blur with reveal and report, distinct copy for the unavailable state, and four preset policies from adultMinimal to classroomStrict that fail closed where it matters.
If the bundled look does not fit your app, replace it entirely while keeping the plumbing:
SafeMediaImage(url: imageURL, engine: engine, context: .incomingMessage, policy: .teenMessaging) { state in
MyBrandedOverlay(
title: state.title,
canReveal: state.canReveal,
onReveal: state.reveal
)
}
One deliberate constraint: state.reveal() is a no-op when policy forbids revealing, and the redaction layer stays under whatever you draw. Branding cannot bypass a no-reveal policy.
For testing, SafeMediaKitTesting ships a mock analyzer and fixtures, so your previews can show safe, sensitive, and unavailable states without any explicit content and without the entitlement.
Should you use it?
Not if you need guaranteed moderation for a general adult audience. Most adults never flip the switch, and no SDK changes that. But in encrypted chat and in apps for families, teens, and classrooms, you get free, private, on-device detection for exactly the users who need it. If you are building there, the framework deserves far more attention than it gets. SafeMediaKit is the layer I wish I had at the start: the two weeks of policy, caching, and fail-closed UI work that the boolean does not include.
Links: SafeMediaKit on GitHub Β· Swift Package Index Β· Apple's SensitiveContentAnalysis docs

Top comments (0)