By 37Design |

iOS Share Extension Kept Crashing After 2 Seconds: The OOM Kill Fix Nobody Talks About

While building SnapPress (an app that lets you upload photos from your iPhone directly to WordPress), I added a Share Extension so users could share photos from the iOS Photos app without opening SnapPress first. I also added an Action Extension for the share sheet.

Both extensions worked. Mostly. The Share Extension would sometimes open, show a loading spinner for about 2 seconds, and then vanish without a trace. No crash log in Xcode Device Logs. No stack trace. No error message. Just gone.

This post is about how I diagnosed the problem, what was actually happening, and the fix that solved it completely.

Share Extension vs Action Extension: What is the Difference?

Before getting into the crash, a quick note on terminology, because these two extension types are easy to confuse.

An Action Extension (com.apple.ui-services) appears in the share sheet under the app icons row. It is designed to transform or act on content in-place (think "Open in App" style buttons). The icon is square and the extension typically hands off to the host app.

A Share Extension (com.apple.share-services) appears in the share sheet's activities list below the app icons. It is designed to send content somewhere: a social network, a CMS, a note-taking app. It runs as a fully independent mini-app with its own UI, its own process, and its own memory limits.

For SnapPress, I implemented both: the Share Extension handles the actual upload UI, and the Action Extension is a quick-launch entry point that opens the main app.

The Crash: "Invalidation Requested"

The symptom was consistent: open the share sheet, tap SnapPress, watch the extension load for 2 to 3 seconds, then disappear. No error shown to the user. No crash log in Xcode's Device Logs window.

The first clue came from Console.app on Mac. With the iPhone connected and Console filtering by the extension's process name, I found this entry in sharingd:

MetricEvent 'com.apple.sharing.sharesheetCompleted' : {
  "success" : false,
  "totalShareTimeMs" : 2700,
  "activityType" : "app.snappress.SnapPress.ShareExtension",
}

And from Photos:

View service session ended with error:
_UIViewServiceHostSessionErrorDomain Code=4
UserInfo={Message=Invalidation requested}

Invalidation requested means the extension process terminated unexpectedly. totalShareTimeMs: 2700 means it lived for exactly 2.7 seconds. This is the fingerprint of an OOM kill. iOS silently terminates processes that exceed the memory limit, with no crash log, no signal, and no user-visible error.

I also saw this in symptomsd:

COSMCtrl applyPolicyDelta unexpected absence of policy
on appRecord app.snappress.SnapPress.ShareExtension

This appears after an extension is killed before iOS can finish setting up its resource policy, another OOM kill indicator.

Root Cause: UIImage(data:) Decodes to Full Resolution in Memory

The Share Extension loads images from NSItemProvider, processes them, and uploads to WordPress. The processing step called this method:

static func process(_ imageData: Data, quality: ImageQuality) -> Data? {
    guard let image = UIImage(data: imageData) else { return nil }
    let resized = resize(image, maxDimension: quality.maxDimension)
    return resized.jpegData(compressionQuality: quality.compressionQuality)
}

The problem is UIImage(data: imageData). This decodes the compressed image file into a raw pixel buffer at full resolution before doing anything else. A 12-megapixel iPhone photo (4032 × 3024 pixels) expands to:

4032 × 3024 × 4 bytes (RGBA) = ~47 MB

Add the resized copy, the JPEG output buffer, the SwiftData container, and the thumbnail previews, a single image upload can spike to 60 to 80 MB. iOS gives Share Extensions roughly 120 to 150 MB. With a few large HEIC photos, that limit is easy to exceed.

The crash was intermittent because it depended on how much memory was already under pressure from other running apps and system processes.

The Fix: Decode Directly to Target Size with CGImageSource

The solution is to never decode to full resolution at all. CGImageSource can downsample during decode, producing the target-sized image without ever creating the full-resolution buffer in memory.

static func process(_ imageData: Data, quality: ImageQuality) -> Data? {
    let maxDim = Int(quality.maxDimension)
    let options: [CFString: Any] = [
        kCGImageSourceCreateThumbnailFromImageAlways: true,
        kCGImageSourceThumbnailMaxPixelSize: maxDim,
        kCGImageSourceCreateThumbnailWithTransform: true,
        kCGImageSourceShouldCacheImmediately: false,
    ]
    guard
        let source = CGImageSourceCreateWithData(imageData as CFData, nil),
        let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary)
    else { return nil }

    return UIImage(cgImage: cgImage).jpegData(compressionQuality: quality.compressionQuality)
}

With kCGImageSourceThumbnailMaxPixelSize set to 2048, the same 12 MP photo now decodes directly to a 2048px image without creating the full-resolution buffer. Memory usage for a single image drops from ~60 MB to ~5 to 8 MB.

Memory Comparison

Approach Peak memory (12 MP HEIC)
UIImage(data:) → resize → JPEG ~60 MB per image
CGImageSourceCreateThumbnailAtIndex → JPEG ~6 MB per image

That is a 10x reduction. The Share Extension has not crashed since.

Other Lessons from Building the Extensions

@MainActor on NSItemProvider calls

The functions that call NSItemProvider.loadFileRepresentation and loadDataRepresentation should be marked @MainActor. Calling NSItemProvider methods from a background actor context can produce unpredictable behavior in Share Extensions. The callback itself runs on a background thread, which is fine, but the call site should be on the main actor.

Keep the ModelContainer alive

If you use SwiftData in a Share Extension, store the ModelContainer in a @State property on the root view. If it is only referenced in a local variable inside a task, it can be deallocated while your context is still using it, causing silent data failures.

@State private var modelContainer: ModelContainer?

Debugging Share Extension crashes

Xcode Device Logs will not show OOM kills. Use Console.app on Mac with your device connected and filter for these process names:

  • sharingd: share sheet lifecycle events
  • symptomsd: resource policy and process termination signals
  • your.bundle.id.ShareExtension: your own extension logs

The combination of Invalidation requested in sharingd and unexpected absence of policy in symptomsd is a reliable OOM kill signature.

Summary

A Share Extension that vanishes after 2 to 3 seconds with no crash log is almost certainly being OOM-killed. The most common cause is UIImage(data:) decoding a compressed photo to its full uncompressed size in memory. Replace it with CGImageSourceCreateThumbnailAtIndex and set kCGImageSourceThumbnailMaxPixelSize to your target dimension. You will decode straight to the output size and skip the full-resolution buffer entirely.

SnapPress is available on the App Store. The source of this fix is in the ImageProcessor.swift shared between the main app and the Share Extension.