ey>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<string>SUBQUERY(
extensionItems,
$extensionItem,
SUBQUERY(
$extensionItem.attachments,
$attachment,
ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image"
|| ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.plain-text"
).@count > 0
).@count > 0</string>
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
<integer>10</integer>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
</dict>
Enable App Groups in both the main app and extension targets. This is mandatory for shared state.
### Step 2: Shared Module Architecture
Never duplicate domain logic. Use a Swift Package or framework to expose shared models, networking layers, and serialization utilities. The extension should only contain UI and context-handling code.
```swift
// Shared/Models/SharePayload.swift
import Foundation
public struct SharePayload: Codable, Equatable {
public let title: String
public let url: URL?
public let imageData: Data?
public let sourceApp: String
public init(title: String, url: URL? = nil, imageData: Data? = nil, sourceApp: String = "extension") {
self.title = title
self.url = url
self.imageData = imageData
self.sourceApp = sourceApp
}
}
// Shared/Storage/SharedContainerManager.swift
import Foundation
public final class SharedContainerManager {
private let defaults: UserDefaults
private let fileManager = FileManager.default
public init?(groupIdentifier: String) {
guard let suite = UserDefaults(suiteName: groupIdentifier) else { return nil }
self.defaults = suite
}
public func savePayload(_ payload: SharePayload) throws {
let data = try JSONEncoder().encode(payload)
let url = sharedDirectoryURL().appendingPathComponent("latest_share.json")
try data.write(to: url, options: .atomic)
defaults.set(Date().timeIntervalSince1970, forKey: "last_payload_update")
}
public func loadLatestPayload() throws -> SharePayload? {
let url = sharedDirectoryURL().appendingPathComponent("latest_share.json")
guard fileManager.fileExists(atPath: url.path) else { return nil }
let data = try Data(contentsOf: url)
return try JSONDecoder().decode(SharePayload.self, from: data)
}
private func sharedDirectoryURL() -> URL {
return fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.com.yourcompany.app")!
}
}
Step 3: Extension View Controller and Context Handoff
Extensions receive data through NSExtensionContext. Parse payloads asynchronously, avoid blocking the main thread, and always complete the request.
import UIKit
import MobileCoreServices
class ShareViewController: UIViewController {
private var extensionContext: NSExtensionContext?
private let containerManager: SharedContainerManager?
override func viewDidLoad() {
super.viewDidLoad()
containerManager = SharedContainerManager(groupIdentifier: "group.com.yourcompany.app")
extensionContext = self.extensionContext
extractItemProvider()
}
private func extractItemProvider() {
guard let items = extensionContext?.inputItems as? [NSExtensionItem] else {
completeRequest()
return
}
let group = DispatchGroup()
var payloads: [SharePayload] = []
for item in items {
guard let attachments = item.attachments else { continue }
for provider in attachments {
group.enter()
if provider.hasItemConformingToTypeIdentifier(kUTTypeImage as String) {
provider.loadItem(forTypeIdentifier: kUTTypeImage as String, options: nil) { [weak self] image, error in
defer { group.leave() }
guard let data = (image as? UIImage)?.jpegData(compressionQuality: 0.8) else { return }
payloads.append(SharePayload(title: "Shared Image", imageData: data))
self?.saveAndComplete(payloads: payloads)
}
} else if provider.hasItemConformingToTypeIdentifier(kUTTypeURL as String) {
provider.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil) { [weak self] url, error in
defer { group.leave() }
if let url = url as? URL {
payloads.append(SharePayload(title: url.absoluteString, url: url))
}
self?.saveAndComplete(payloads: payloads)
}
}
}
}
}
private func saveAndComplete(payloads: [SharePayload]) {
guard let first = payloads.first, let manager = containerManager else {
completeRequest()
return
}
do {
try manager.savePayload(first)
} catch {
print("Failed to save to shared container: \(error)")
}
completeRequest()
}
private func completeRequest() {
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
}
}
Step 4: Architecture Decisions and Rationale
- Value-First Shared Models: Use
Codable structs instead of classes. Extensions and main apps run in separate processes; reference semantics cause synchronization bugs.
- Explicit Context Completion: Always call
completeRequest(returningItems:completionHandler:). iOS will terminate the extension process if the request remains open beyond the system timeout.
- Deferred Processing: Heavy work (network uploads, image processing) should be handed off to a background task or the main app via
BGAppRefreshTask or URLSession background configuration. Extensions should not perform long-running operations.
- Memory Budget Enforcement: Profile with Instruments. Set
os_signpost markers to track allocation spikes. Compress images before writing to shared containers. Avoid loading full UI frameworks unnecessarily.
Pitfall Guide
1. Assuming Full App Lifecycle
Extensions do not receive applicationDidFinishLaunching or standard scene lifecycle callbacks. They are instantiated on demand and destroyed when the context completes. Relying on AppDelegate or SceneDelegate state will cause nil crashes.
2. Shared Container Sync Storms
Writing to UserDefaults or shared files on every UI interaction causes file descriptor exhaustion and database corruption. Coalesce writes using DispatchWorkItem with debounce, or batch updates before calling completeRequest.
3. Ignoring OOM Thresholds
Most extensions are killed at 300MB. Image-heavy extensions frequently exceed this when loading full-resolution assets. Downsample images to 1080p maximum, use CGImageSourceCreateThumbnailAtIndex, and avoid caching in-memory.
4. Blocking NSExtensionContext Handoff
Synchronous network calls or heavy JSON parsing on the main thread delays context handoff. iOS enforces a strict launch budget. Offload work to background queues and return control immediately.
Overly broad rules trigger the extension for unsupported content types, causing runtime crashes. Overly narrow rules hide the extension from valid use cases. Use SUBQUERY predicates with explicit UTI conformance checks and test against real NSItemProvider payloads.
6. Testing Only in Simulator
The simulator relaxes memory limits, mocks NSItemProvider behavior, and extends process lifetimes. Always test on physical devices with real share sheets, keyboard input, and widget rendering. Use xcrun simctl to inject test payloads when simulator testing is unavoidable.
7. Violating Sandbox Boundaries
Extensions cannot access the main app’s Documents or Library directories directly. They can only read/write to App Group containers. Attempting to bypass this triggers sandbox violations and App Store rejections. Use NSFileCoordinator for safe cross-process file access.
Best Practices from Production
- Implement
isExtensionInvalidated to cancel pending tasks when iOS suspends the extension.
- Use
os_signpost and ActivityKit to measure cold launch and memory allocation in production.
- Validate
NSExtensionActivationSupportsAttachmentsWithMaxCount against actual payload sizes.
- Never store authentication tokens in shared containers; use Keychain with appropriate access groups.
- Design extensions to be idempotent. Users may trigger them multiple times or switch apps mid-operation.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Content distribution (images, links, text) | Share Extension | System-native integration, high user adoption, minimal UI overhead | Low development, high engagement ROI |
| At-a-glance information (weather, tasks, metrics) | Today Widget (WidgetKit) | Live activity support, system surface placement, background refresh | Medium development, high DAU impact |
| Text input customization (emojis, shortcuts, formatting) | Custom Keyboard Extension | Direct input control, high retention if utility-driven, strict App Review | High development, moderate retention |
| Cloud storage integration (file browsing, upload) | File Provider Extension | System file picker integration, background sync, complex state management | High development, enterprise/creator value |
| Siri and Shortcuts automation | Intents Extension | Voice/UI automation, cross-app triggers, requires precise parameter mapping | Medium development, high automation ROI |
Configuration Template
<!-- Entitlements.plist (Shared) -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.yourcompany.app</string>
</array>
</dict>
</plist>
<!-- Package.swift (Shared Module) -->
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "AppShared",
platforms: [.iOS(.v16)],
products: [
.library(name: "AppShared", targets: ["AppShared"])
],
targets: [
.target(
name: "AppShared",
path: "Sources",
resources: [.process("Resources")]
)
]
)
Quick Start Guide
- Create Extension Target: In Xcode, select File > New > Target. Choose your extension type (Share, Today, etc.). Enable "Include UI Extension" if applicable.
- Configure Entitlements: Add the same App Group identifier to both the main app and extension target in Signing & Capabilities. Create
Entitlements.plist if needed.
- Wire Shared Container: Add the Swift Package to both targets. Initialize
SharedContainerManager with the group identifier. Implement savePayload and loadLatestPayload.
- Implement Context Handoff: Replace default
NSExtensionContext parsing with asynchronous NSItemProvider loading. Call completeRequest immediately after saving to the shared container.
- Test on Device: Run the extension via Xcode's scheme selector. Trigger it from Safari, Photos, or Notes. Verify shared container writes, memory footprint, and process termination behavior.
iOS app extensions succeed when treated as constrained system integrations rather than secondary applications. Decouple state, respect memory budgets, handle context lifecycles explicitly, and validate against real payloads. The architectural overhead pays for itself in crash reduction, review approval velocity, and sustained user engagement.