Shipping an iOS share extension without pain
Share extensions on iOS have a reputation. That reputation is partially deserved. Here's what actually tripped us up, what didn't, and how we shipped a stable one.
When someone takes a screenshot of a whiteboard photo already in their Photos library and wants to run it through BoardSnap, they shouldn't have to open the app, navigate to a project, and import manually. That's four taps for something that should be two.
The iOS share extension was the answer. In Photos, tap the share icon, find BoardSnap, choose a project. Done.
Building it was educational in the ways iOS development is always educational: the abstractions are clean, the edge cases are cruel.
### The architecture problem first
Share extensions run in a separate process from the host app. This sounds like a minor implementation detail. It is not.
Separate process means: no access to the app's main memory, no access to the app's network session, and — critically — no direct access to the app's Core Data store. The extension and the app can share data only through an App Group, which is a shared container both are entitled to use.
If you build a share extension assuming it can talk directly to the app's existing data layer, you'll rebuild significant architecture. I saw this coming from reading documentation and avoided it by designing the App Group boundary first, but I watched a StackOverflow thread where someone rebuilt their extension three times discovering this incrementally.
Design rule: treat the share extension as a completely separate app that can write to a shared inbox. The main app reads from that inbox on launch.
### What the share extension actually does
Our share extension does the minimum viable job:
- Accepts an image from the share sheet (via
NSExtensionItemand thepublic.imageUTI). - Presents a minimal UI: a project picker and a confirm button.
- Writes the image and selected project ID to the App Group container.
- Opens the main app via URL scheme to process the queued item.
It doesn't process the image. It doesn't run analysis. It doesn't touch the API. Those things happen in the main app.
This minimal-responsibility architecture makes the extension fast (it's just a picker and a write operation) and makes failures tractable (if something goes wrong with the API, it's the main app's problem, not the extension's).
### The actual painful parts
Entitlements. The App Group entitlement has to be configured in two places: the main target and the extension target. Xcode's UI for this is fine. The infuriating part is that the App Group ID and the entitlement file have to match exactly across both targets, and a mismatch fails silently at runtime rather than at build time. I spent two hours debugging a write that appeared to succeed but was writing to a different container than the main app was reading from.
Fix: add a unit test that writes a sentinel value from the extension bundle ID context and reads it from the main app bundle ID context. Run it in the CI pipeline. This would have caught my two-hour problem in ten seconds.
Memory limits. Share extensions have strict memory limits — around 120MB on most devices. A high-resolution whiteboard photo can be 8–15MB after VisionKit processing. If the user shares multiple images at once, you can hit the limit. We handle this by compressing images before writing to the App Group container, accepting a small quality tradeoff for reliability.
The share sheet icon. Small thing, but worth knowing: the share sheet icon for an extension is distinct from the app icon and needs to be explicitly provided as a NSExtensionPointIdentifier icon. If you omit it, you get the generic gray icon, which makes your extension look unfinished. Took me an embarrassingly long time to track down where to set this.
Testing extensions. You can't easily launch a share extension in the simulator without going through the full share sheet flow. The debugging loop is slower than normal iOS development. Add print statements, use OSLog, and be prepared for more build-run-check cycles than you'd like.
### What I'd do differently in v2
The current extension makes the user pick a project every time they share. Most users have a default project they use 90% of the time.
V2 would: remember the last selected project per extension session, offer a "use as default" toggle, and pre-select it on the next share. The modal would be dismissable in one tap for the common case — just confirm. The project picker would be there but secondary.
The share extension is currently the highest-friction path into BoardSnap for existing users. Making it a one-tap flow for the common case is on the short list.
Frequently asked
Can the BoardSnap share extension process images from apps other than Photos?
Yes — any app that shares images via the iOS share sheet can send them to BoardSnap. This includes Files, Mail, Messages, and most third-party apps. The extension accepts the standard public.image UTI.
Snap your first board today.
See the workflow this post talks about — free on the App Store.