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.
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.plistcontainsNSExtensionPrincipalClassand does not containNSExtensionMainStoryboard.- The extension folder contains no
.storyboardfile (onlyInfo.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
UIViewControllersubclass, the extension host instantiates it and installs its view into the extension's window. This triggers the standard view lifecycle:loadView()→viewDidLoad(). ShareViewControlleroverridesviewDidLoad()and callssuper.viewDidLoad(), then immediately runsprocessAndOpenApp(). The startup marker (shareExtensionLastStart) and[ShareTarget] viewDidLoad startedlog execute before any other logic.- The Swift source is compiled into the extension target via the Xcode 16 file-system synchronized group (
PBXFileSystemSynchronizedRootGroupforTimeSafariShareExtension), 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 customPRODUCT_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 asynchronousloadItemwork inprocessSharedImagehappens afterviewDidLoadreturns.
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 | 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
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
TimeSafariShareExtensiontarget 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/UISceneConfigurationsin the extensionInfo.plist. - No
SceneDelegatein the extension target. - The extension relies entirely on the principal
UIViewControllerlifecycle (viewDidLoad→processAndOpenApp→processSharedImage→completeRequest). - The main app (
AppDelegate) is aUIApplicationDelegateand 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
-
Config-dependent bundle IDs & teams. Debug uses
app.trentlarson.timesafari*with team7XVXYPEQYJ; Release usesapp.timesafari*with teamGM3FS5JQPH. 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. -
GENERATE_INFOPLIST_FILE = YESalongside an explicitINFOPLIST_FILE. Xcode merges auto-generated keys into the suppliedInfo.plist. This is supported and the explicitNSExtensionblock is preserved; no conflict observed. -
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.
-
Principal-class resolution dependency. The launch guarantee depends on
$(PRODUCT_MODULE_NAME)matching the compiled module. IfPRODUCT_MODULE_NAMEis later customized or the target renamed without updating expectations, the OS would fail to instantiateShareViewControllerandviewDidLoad()would never run. Currently consistent. -
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.