# iOS Share Extension Configuration Audit **Generated:** 2026-06-25 15:33:39 PST ## Scope Static inspection of the `TimeSafariShareExtension` target configuration to determine the extension entry point, principal view controller, storyboard vs. code-based setup, and whether `ShareViewController.viewDidLoad()` is guaranteed to execute. No code was modified. ### Files Inspected | File | Role | |------|------| | `ios/App/App.xcodeproj/project.pbxproj` | Target, build settings, file membership | | `ios/App/TimeSafariShareExtension/Info.plist` | NSExtension configuration | | `ios/App/TimeSafariShareExtension/ShareViewController.swift` | Principal class implementation | | `ios/App/TimeSafariShareExtension/TimeSafariShareExtension.entitlements` | App Group access | --- ## Direct Answers ### 1. What class is configured as the extension entry point? `ShareViewController`, resolved via `Info.plist` key `NSExtensionPrincipalClass`: ```xml NSExtensionPrincipalClass $(PRODUCT_MODULE_NAME).ShareViewController ``` `$(PRODUCT_MODULE_NAME)` resolves to `TimeSafariShareExtension` (derived from `PRODUCT_NAME = $(TARGET_NAME)`), so the runtime entry point is `TimeSafariShareExtension.ShareViewController`. ### 2. Is ShareViewController actually the configured principal view controller? **Yes.** `ShareViewController.swift` declares: ```swift class ShareViewController: UIViewController { ``` within the `TimeSafariShareExtension` target. The class name, module, and `UIViewController` base class match the `NSExtensionPrincipalClass` reference. There is no competing storyboard-designated initial controller, so `ShareViewController` is unambiguously the principal view controller. ### 3. Is the extension storyboard-based or code-based? **Code-based.** - `Info.plist` contains `NSExtensionPrincipalClass` and does **not** contain `NSExtensionMainStoryboard`. - The extension folder contains no `.storyboard` file (only `Info.plist`, `ShareViewController.swift`, and the entitlements file). - The only storyboards in the project (`Main.storyboard`, `LaunchScreen.storyboard`) belong exclusively to the **App** target's resources, not the extension. This deviates from the default Xcode Share Extension template (which ships a `MainInterface.storyboard` + `NSExtensionMainStoryboard`). The deviation is intentional and internally consistent. ### 4. Does the configuration guarantee that ShareViewController.viewDidLoad() executes when the extension launches? **Yes, under normal launch.** Because: - The principal class is a `UIViewController` subclass, the extension host instantiates it and installs its view into the extension's window. This triggers the standard view lifecycle: `loadView()` → `viewDidLoad()`. - `ShareViewController` overrides `viewDidLoad()` and calls `super.viewDidLoad()`, then immediately runs `processAndOpenApp()`. The startup marker (`shareExtensionLastStart`) and `[ShareTarget] viewDidLoad started` log execute before any other logic. - The Swift source is compiled into the extension target via the Xcode 16 **file-system synchronized group** (`PBXFileSystemSynchronizedRootGroup` for `TimeSafariShareExtension`), so the class is guaranteed to be present in the built `.appex`. **Caveats (not failures, but worth noting):** - The guarantee holds only if the OS successfully resolves and instantiates the principal class. If `$(PRODUCT_MODULE_NAME)` ever diverges from the actual Swift module name (e.g., a custom `PRODUCT_MODULE_NAME`), runtime class lookup would fail and the extension would not launch. Currently they match. - `viewDidLoad()` executing does not, by itself, guarantee the *share* succeeds — the asynchronous `loadItem` work in `processSharedImage` happens after `viewDidLoad` returns. ### 5. Are there any mismatches between Info.plist, storyboard, and ShareViewController? **No blocking mismatches.** Details: | Check | Result | |-------|--------| | `NSExtensionPrincipalClass` ↔ Swift class name | Match (`ShareViewController`) | | Principal class module ↔ target module | Match (`TimeSafariShareExtension`) | | `NSExtensionMainStoryboard` ↔ storyboard file | Consistent — neither exists (code-based) | | Activation rule ↔ implementation | Consistent — `NSExtensionActivationSupportsImageWithMaxCount = 1` matches first-image-only handling | | `NSExtensionPointIdentifier` | `com.apple.share-services` (correct for a Share extension) | | Source file membership | `ShareViewController.swift` compiled via synchronized group | See "Observations / Non-Blocking Notes" for environment-specific items. --- ## Detailed Configuration ### NSExtension (Info.plist) ```xml NSExtension NSExtensionAttributes NSExtensionActivationRule NSExtensionActivationSupportsImageWithMaxCount 1 NSExtensionPointIdentifier com.apple.share-services NSExtensionPrincipalClass $(PRODUCT_MODULE_NAME).ShareViewController ``` | Key | Value | Meaning | |-----|-------|---------| | `NSExtensionPointIdentifier` | `com.apple.share-services` | Registers as a Share sheet extension | | `NSExtensionPrincipalClass` | `$(PRODUCT_MODULE_NAME).ShareViewController` | Code-based entry point | | `NSExtensionActivationRule` | `…ImageWithMaxCount = 1` | Activates for shares containing at least one image; processes one | | `NSExtensionMainStoryboard` | *absent* | Confirms code-based (no storyboard UI) | ### TimeSafariShareExtension Target (project.pbxproj) | Attribute | Value | |-----------|-------| | `isa` | `PBXNativeTarget` | | `productType` | `com.apple.product-type.app-extension` | | `productReference` | `TimeSafariShareExtension.appex` | | `CreatedOnToolsVersion` | `26.1.1` | | File membership | `fileSystemSynchronizedGroups` → `TimeSafariShareExtension` (auto-membership) | | Sources build phase | Empty explicit list (handled by synchronized group) | | `Info.plist` membership | Excepted from synchronized group (`PBXFileSystemSynchronizedBuildFileExceptionSet`) | | Embedded into | App target's "Embed Foundation Extensions" copy phase | | Target dependency | App target depends on `TimeSafariShareExtension` | ### Extension Build Settings (Debug / Release) | Setting | Debug | Release | |---------|-------|---------| | `INFOPLIST_FILE` | `TimeSafariShareExtension/Info.plist` | same | | `GENERATE_INFOPLIST_FILE` | `YES` | `YES` | | `PRODUCT_NAME` | `$(TARGET_NAME)` → `TimeSafariShareExtension` | same | | `PRODUCT_BUNDLE_IDENTIFIER` | `app.trentlarson.timesafari.TimeSafariShareExtension` | `app.timesafari.TimeSafariShareExtension` | | `CODE_SIGN_ENTITLEMENTS` | `TimeSafariShareExtension/TimeSafariShareExtension.entitlements` | same | | `IPHONEOS_DEPLOYMENT_TARGET` | `14.0` | `14.0` | | `DEVELOPMENT_TEAM` | `7XVXYPEQYJ` | `GM3FS5JQPH` | | `SWIFT_VERSION` | `5.0` | `5.0` | | `SKIP_INSTALL` | `YES` | `YES` | `PRODUCT_MODULE_NAME` is not overridden, so it defaults to `PRODUCT_NAME` = `TimeSafariShareExtension`, making the principal class resolve to `TimeSafariShareExtension.ShareViewController`. ### ShareViewController Linkage ```swift import UIKit import UniformTypeIdentifiers class ShareViewController: UIViewController { ... override func viewDidLoad() { // writes shareExtensionLastStart, logs, then super + processAndOpenApp() } } ``` - Subclass of `UIViewController` → eligible as a code-based principal class. - Lives in the `TimeSafariShareExtension` target via the synchronized group. - No `@objc(...)` annotation is required because the principal class is referenced with the fully-qualified Swift name (`module.Class`). ### Scene / Lifecycle Configuration - **No** `UIApplicationSceneManifest` / `UISceneConfigurations` in the extension `Info.plist`. - **No** `SceneDelegate` in the extension target. - The extension relies entirely on the principal `UIViewController` lifecycle (`viewDidLoad` → `processAndOpenApp` → `processSharedImage` → `completeRequest`). - The main app (`AppDelegate`) is a `UIApplicationDelegate` and is unrelated to the extension's lifecycle except via the shared App Group. ### App Group Linkage `TimeSafariShareExtension.entitlements` grants `group.app.timesafari.share`, matching the App target's entitlement. This is what allows `viewDidLoad()`'s `shareExtensionLastStart` write to be visible to the main app's `getShareExtensionDiagnostics()`. --- ## Observations / Non-Blocking Notes 1. **Config-dependent bundle IDs & teams.** Debug uses `app.trentlarson.timesafari*` with team `7XVXYPEQYJ`; Release uses `app.timesafari*` with team `GM3FS5JQPH`. Within each configuration the extension bundle ID is correctly nested under the app bundle ID. Ensure provisioning profiles for both teams include the App Group capability. 2. **`GENERATE_INFOPLIST_FILE = YES` alongside an explicit `INFOPLIST_FILE`.** Xcode merges auto-generated keys into the supplied `Info.plist`. This is supported and the explicit `NSExtension` block is preserved; no conflict observed. 3. **Deployment target gap.** Extension targets iOS 14.0 while the App target targets iOS 15.5. Valid (an extension may target lower), and not a launch concern. 4. **Principal-class resolution dependency.** The launch guarantee depends on `$(PRODUCT_MODULE_NAME)` matching the compiled module. If `PRODUCT_MODULE_NAME` is later customized or the target renamed without updating expectations, the OS would fail to instantiate `ShareViewController` and `viewDidLoad()` would never run. Currently consistent. 5. **Code-based template divergence.** Since there is no `MainInterface.storyboard`, any future tooling or documentation that assumes the stock storyboard-based Share Extension template will not apply here. --- ## Conclusion The `TimeSafariShareExtension` is a **code-based** Share extension whose entry point is `ShareViewController` (a `UIViewController` subclass) via `NSExtensionPrincipalClass`. The Info.plist, (absent) storyboard, and Swift implementation are mutually consistent. Under normal extension launch, `ShareViewController.viewDidLoad()` is guaranteed to run, executing the startup marker and the share-processing pipeline. No blocking misconfiguration was found; only environment-specific items (signing identities, principal-class resolution dependency) warrant ongoing attention.