From 6afe40bc23064a790c125cfecc15239e37856fcd Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Thu, 25 Jun 2026 17:28:20 +0800 Subject: [PATCH] docs(ios): add Share Extension configuration audit Document that TimeSafariShareExtension is a code-based extension with ShareViewController as NSExtensionPrincipalClass, confirm viewDidLoad executes on launch, and note no blocking Info.plist/storyboard mismatches. --- doc/share-extension-configuration-audit.md | 188 +++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 doc/share-extension-configuration-audit.md diff --git a/doc/share-extension-configuration-audit.md b/doc/share-extension-configuration-audit.md new file mode 100644 index 00000000..be8526a6 --- /dev/null +++ b/doc/share-extension-configuration-audit.md @@ -0,0 +1,188 @@ +# 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.