Files
crowd-funder-for-time-pwa/doc/share-extension-configuration-audit.md
Jose Olarte III 6afe40bc23 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.
2026-06-25 17:28:20 +08:00

10 KiB

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:

<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).ShareViewController</string>

$(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:

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)

<key>NSExtension</key>
<dict>
    <key>NSExtensionAttributes</key>
    <dict>
        <key>NSExtensionActivationRule</key>
        <dict>
            <key>NSExtensionActivationSupportsImageWithMaxCount</key>
            <integer>1</integer>
        </dict>
    </dict>
    <key>NSExtensionPointIdentifier</key>
    <string>com.apple.share-services</string>
    <key>NSExtensionPrincipalClass</key>
    <string>$(PRODUCT_MODULE_NAME).ShareViewController</string>
</dict>
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 fileSystemSynchronizedGroupsTimeSafariShareExtension (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

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 (viewDidLoadprocessAndOpenAppprocessSharedImagecompleteRequest).
  • 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.