iOS extensions with multiple targets
IosPlistCode SigningIos App-ExtensionBundle IdentifierIos Problem Overview
In iOS 8, when we create an app extension, we have to decide which target it is attached to. The extension will have the same bundle ID's prefix as the target.
- Is there any way to change the target afterward?
- If my project contains 2 (or more) targets (for example one for debug/simulator, one for production/device), what's the best way to work with extensions? Do I need to create another extension and duplicate the code (very bothersome to keep the same code for both targets)?
Ios Solutions
Solution 1 - Ios
To share one widget with others targets, you only need to add widget.appex target
to Embedded Binaries
for every parent target in General
configuration tab
Then you'll get Embed App Extensions
area at Build Phases
automatically
Solution 2 - Ios
This is my setup: I have 3 targets (production, staging, local) and an extension target that I don't want to duplicate 3 times.
Just to clarify Neo Chen's answer, edit each of your parent targets' schemes:
Build > Pre-actions > New Run Script Action > Provide build settings from (parent scheme).
Paste this for each extension:
#!/bin/bash
buildID=${PRODUCT_BUNDLE_IDENTIFIER}
extId="notification-service"
/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $buildID.$extId" "${SRCROOT}/${extId}/Info.plist"
Seems to work on first build.
Solution 3 - Ios
It seems like you should be able to just duplicate the Extension target with its own Info.plist, but not anything else.
However, when you create an Extension, Xcode adds "Embed App Extensions" to the Build Phases of the app's target, as seen below, and there's no UI to do that yet.
Still, you can create the extension for the second target, then delete all the files except the .plist, and fix what needs to be fixed. Here's a step-by-step:
-
Create "Extension 1" for "Target 1"
-
Create "Extension 2" for "Target 2"
-
Delete all files created for "Extension 2", except its Info.plist
-
Make the "Build Phases" for "Extension 2" target the same as the build phases for "Extension 1". Usually that's adding the necessary .m files to the "Compile Sources" phase, and resources to the "Copy Bundle Resources" phase
Solution 4 - Ios
I have create a Run Script to support this requirement
#!/bin/sh
buildNumber=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "$INFOPLIST_FILE")
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $buildNumber" "${SRCROOT}/ImagePush/Info.plist"
buildVersion=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" "$INFOPLIST_FILE")
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $buildVersion" "${SRCROOT}/ImagePush/Info.plist"
buildID=${PRODUCT_BUNDLE_IDENTIFIER}
/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $buildID.ImagePush" "${SRCROOT}/ImagePush/Info.plist"
ImagePush is my Extension
add to the target which one you need and add make sure this script run before your extension setting in Build Phases, then you just need to do the build action twice (PS: first time it will fail, will try to improve) and it will support multiple target
Solution 5 - Ios
In my project I need to build a bit different versions of apps (differ in details, e.g. each application is branded with a different logo).
Let's say there is about 10 "app" targets, I can't imagine adding Notification Content and Notification Service extensions per each main target (in this case I would maintaining 30 targets in total - madness).
I run a script (https://gist.github.com/damian-rzeszot/0b23ad87e5ab5d52aa15c095cbf43c59) after "Embed App Extensions" phase, that overrides bundle id in app extension plists and entitlements, app version, changes the provisioning profile and re-signs the bundle.
Solution 6 - Ios
Xcconfigs are a great way to modify plist entries and in code variable based on an Xcode scheme change.
If you are wanting to change the Bundle identifier
for example of a given extension based on scheme:
-
Create an Xcconfig file for your extension. My
iMessageExtension-Debug.xcconfig
has this entry:PRODUCT_BUNDLE_IDENTIFIER = $(APP_BUNDLE_IDENTIFIER).iMessageExtension
-
From the Xcode file inspector click on your project > in the details pane click on your project > Info tab > Add configuration
-
I created a debug config.
-
In your newly create config drill to your extension > Select the config file
-
Create a new Scheme: in the run tab of that new scheme you can select your newly created config. This same process can be done for release extra.
-
The Xcconfig option we created can be used directly inside of the iMessageExtension > Info.plist:
Bundle identifier : $(PRODUCT_BUNDLE_IDENTIFIER)
If you need to reference a variable in code based on an xcconfig variable:
For example I wanted to change my application initial screen based on the Xcode scheme that was selected:
Xcconfig:
INITIAL_SCREEN = tabBarHome
Plist:
initialScreen : $(INITIAL_SCREEN)
Swift Code:
var initialScreen = object(forInfoDictionaryKey: "initialScreen") as? String
Solution 7 - Ios
EDIT
2 solutions:
- Use
XCConfig
. See answer mentioned here: https://stackoverflow.com/a/63583849/5175709 and here. Also search forXCConfig
on this SO question. You might find some comments. - Change everything through scripts. My answer is doing this.
To be honest I think the XCConfig
solution is much much more elegant. You just swap things in and out rather than trying to override everything in a very specific order...
Every other answer had a piece of what was necessary. With some important modifications to this post I was able to get things right.
You need to do three things:
- Change bundleId of appex (app extension)
- re-sign appex after its bundle is changed. While the app runs on simulator correctly it won't work on real device without this step.
- set appropriate provisioning profiles. (excluded from this answer as I don't know how to do that yet)
Note: You can't sign the appex until it's embedded. So the 're-sign appex' step needs to happen after the 'embed App extension' step. Similarly the appex can't be embedded if the bundleId isn't prefixed with the parent app's bundleId.
The final order should look like this:
change bundleId of appex
plutil
's sytanx is like this:
-replace keypath -type value
So just do:
plutil -replace \
CFBundleIdentifier -string \
$PRODUCT_BUNDLE_IDENTIFIER.contentExt \
"$BUILT_PRODUCTS_DIR/contentExt.appex/Info.plist"
In case you wanted to learn more about plutil
(see here and here for more). PlistBuddy
is kinda old.
Note: ContentExtension is the target name I have. Make sure you use yours correctly
re-sign appex
/usr/bin/codesign \
--force \
--sign $EXPANDED_CODE_SIGN_IDENTITY \
--entitlements $CONFIGURATION_TEMP_DIR/ContentExtension.build/ContentExtension.appex.xcent \
--timestamp=none \
"$BUILT_PRODUCTS_DIR/$FULL_PRODUCT_NAME/$BUNDLE_PLUGINS_FOLDER_PATH/ContentExtension.appex"
Note: ContentExtension is the target name I have. Make sure you use yours correctly
The end result is like this:
Don't forget to repeat the steps for every target. The best way to make sure that you have it right is to set the appex's bundleId to something totally wrong and then test all your targets on a real device. If you test it on the sim then you won't be able to validate if the code-signing works correctly or not.
FWIW it's usually a good idea to dump all your shells in one directory like and then reference them from there. But for the sake of simplicity of this post I'm not doing that.
Also make sure you see the original gist it's much smarter if you use that to change all your appexes. You just have to pass the name of the appex and then it would figure out the rest...
Solution 8 - Ios
You need to create multiple extensions for each ID, but you can create dynamic framework and just link it with each extension. Then you will not need to duplicate your code.