A user shares a photo from their library to an iOS app. The Share Extension appears in the menu, but opening it produces a “Cannot read” error. Checking that the extension builds is not enough to diagnose this. The source app supplies an NSItemProvider, which may advertise several types. A file representation may also provide a temporary path that is valid only during the loading callback. Test “appears in the menu,” “can read the input,” and “can hand it to the containing app” separately to find where the process breaks down.
Define the input contract first
Choose the sources and data the product actually needs to support. “All images” is too vague to be a useful test case. Record at least the source app, the shared content, its registeredTypeIdentifiers, the expected loading method, and what the containing app must ultimately receive. The same image may arrive as a file, as data, or as both, depending on its source. The extension must not rely on the first type in the array.
| Input | What the extension should check | Acceptance result |
|---|---|---|
| One image | Whether an image-compatible representation is available | Reads the content and creates a handoff record |
| Multiple images | The types advertised by each attachment | The attachment count matches the combined success and failure counts |
| Plain text | Whether a text representation is available | Accepts it if required by the product, or explicitly rejects it |
| Unsupported content | No matching type is available | Shows an understandable error and creates no empty record |
The Share Extension’s activation rule determines when it appears in the menu. Runtime type checks determine whether it can handle the current attachments. Test both; appearing in the menu does not prove that an input can be read.
Inspect declarations before choosing a representation
During development, write attachment type identifiers to a local diagnostic record, but do not upload the user’s content with them. Check whether a type conforms to the target type instead of hard-coding the full identifier returned by one source app. If the product accepts only images, route text input through an explicit rejection path. Do not try to decode unknown content as an image.
Copy files inside the callback
The riskiest way to handle a file representation is to save the URL returned by loadFileRepresentation and ask the containing app to open it after the extension finishes. That URL points to a temporary file supplied during loading; it is not a durable handoff location.
The acceptance criterion is not “the callback can print a path.” It is “after the callback returns, the containing app can still read a copy it is authorized to access.”
The Swift snippet below shows the critical boundary for an image file. If the extension and containing app need to share files, configure the same App Group for both and pass its identifier through deployment configuration. Do not put an example identifier directly into the project. The copy gets a new filename so that one share does not overwrite another.
import Foundation
import UniformTypeIdentifiers
enum ShareCopyError: Error {
case unsupportedType
case containerUnavailable
case missingFile
}
func copySharedImage(
from provider: NSItemProvider,
groupID: String,
completion: @escaping (Result<URL, Error>) -> Void
) {
guard let typeID = provider.registeredTypeIdentifiers.first(where: {
UTType($0)?.conforms(to: .image) == true
}) else {
completion(.failure(ShareCopyError.unsupportedType))
return
}
provider.loadFileRepresentation(forTypeIdentifier: typeID) { source, error in
if let error {
completion(.failure(error))
return
}
guard let source else {
completion(.failure(ShareCopyError.missingFile))
return
}
guard let container = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: groupID) else {
completion(.failure(ShareCopyError.containerUnavailable))
return
}
do {
let directory = container.appendingPathComponent(
"Incoming", isDirectory: true
)
try FileManager.default.createDirectory(
at: directory, withIntermediateDirectories: true
)
let ext = UTType(typeID)?.preferredFilenameExtension ?? "bin"
let target = directory.appendingPathComponent(
UUID().uuidString + "." + ext
)
try FileManager.default.copyItem(at: source, to: target)
completion(.success(target))
} catch {
completion(.failure(error))
}
}
}
This code handles only one image attachment. A production implementation also needs to aggregate results for multiple attachments, limit acceptable file sizes, and decide whether to retain copied files or roll everything back after a partial success. Hand the containing app an identifier for a file in a controlled directory. Once it has read the file, clean up according to the product’s rules rather than letting copies accumulate indefinitely.
Include the loading boundary in automated tests
Test the type-selection and copy functions before testing the complete sharing interface. Cover at least image types, unsupported types, loading errors, an unavailable shared container, partial failure with multiple attachments, and inputs with the same name that must not overwrite each other. The test double should control callback timing: make a file available inside the callback, remove the source file after the callback returns, and then assert that the copy remains readable. This tests the file’s lifetime, not merely whether a URL is nonempty.
On a CDNMac cloud Mac, inspect the project’s exposed schemes and available simulators before running the relevant tests. The commands below require you to set three environment variables to values that actually exist in the current project. The result bundle goes in a newly created directory to avoid collisions on reruns.
: "${PROJECT:?set PROJECT to an existing .xcodeproj path}"
: "${SCHEME:?set SCHEME to a shared test scheme}"
: "${SIMULATOR_ID:?set SIMULATOR_ID to an available device ID}"
xcodebuild -list -project "$PROJECT"
xcrun simctl list devices available
RESULT_ROOT="$(mktemp -d)"
xcodebuild -project "$PROJECT" \
-scheme "$SCHEME" \
-destination "platform=iOS Simulator,id=$SIMULATOR_ID" \
-resultBundlePath "$RESULT_ROOT/share-input.xcresult" \
test
CI should retain the test result bundle and use the test process’s exit status to determine whether the job succeeds. Do not just search the logs for “passed”: an extension that fails to launch or tests omitted from the scheme must also be detected. When setting up the pipeline, deliberately fail an assertion once to confirm that it blocks the job.
Close the testing gaps with real shares
Unit tests can verify your loading logic, but they cannot guarantee which representations a source app will provide. On a test device, share an item from each source you plan to support. First confirm that the extension appears, then inspect the types actually advertised, the loading result, and the copy in the containing app. For large or multiple images, pay particular attention to memory use, progress feedback, and cancellation. One small image passing is not enough to complete acceptance testing.
Troubleshoot in order: if the extension does not appear, check the activation rule; if it appears but no type matches, record the identifiers supplied by the source; if loading fails, retain the error category. If the extension reads the input but the containing app cannot, check App Group permissions, confirm that copying finished inside the callback, and verify that the app received the copy’s path rather than the source path. Keep only the necessary type, stage, and error information in test records; do not write shared content to build logs.
Pre-release checklist
Before release, verify four things: the extension’s declarations match the types it actually supports; every file is copied inside its loading callback; the containing app reads only copies in an authorized directory; and rejection, cancellation, and partial failure all produce visible outcomes. If you change the App Group configuration, rerun the handoff on a freshly installed test device. An existing container on a development machine is not sufficient verification.
Keep the type matrix, automated test results, and device-sharing records together with the same change. Then, when adding an input format, there is a clear starting point: update the contract and add a failing test before implementing loading, rather than waiting for a user to report “there’s no file after sharing” and guessing what the source supplied.
Frequently asked questions
Can a Share Extension hand the received file URL directly to its containing app?
No. Copy the file to a location the app can read while the loadFileRepresentation callback is active, then hand off an identifier for the copy.
What still needs checking on a device after simulator tests pass?
Share from the source app and check extension visibility, the actual supplied type, large-file handling, and whether the containing app can read the copied file.
Run your next iOS build on a dedicated cloud Mac
Choose a fixed physical environment with M4, 16GB RAM, and a 256GB SSD, and pick the right fit for your task from six nodes and four rental terms.