8000 Support Watch Widget Extensions by mpiannucci · Pull Request #121 · EvanBacon/expo-apple-targets · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Support Watch Widget Extensions #121

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions packages/apple-targets/src/config-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const withTargetsDir: ConfigPlugin<
absolute: true,
});

targets.forEach((configPath) => {
const evaluatedTargets = targets.map((configPath) => {
const targetConfig = require(configPath);
let evaluatedTargetConfigObject = targetConfig;
// If it's a function, evaluate it
Expand All @@ -57,11 +57,28 @@ export const withTargetsDir: ConfigPlugin<
);
}

config = withWidget(config, {
appleTeamId,
return {
...evaluatedTargetConfigObject,
directory: path.relative(projectRoot, path.dirname(configPath)),
configPath,
};
});

// Sort so that any watch-widget targets come before any watch targets, LIFO
evaluatedTargets.sort((a, b) => {
if (a.type === "watch-widget") {
return -1;
}
if (b.type === "watch-widget") {
return 1;
}
return 0;
});

evaluatedTargets.forEach((target) => {
config = withWidget(config, {
appleTeamId,
...target,
});
});

Expand Down
14 changes: 14 additions & 0 deletions packages/apple-targets/src/target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type ExtensionType =
| "imessage"
| "clip"
| "watch"
| "watch-widget"
| "location-push"
| "credentials-provider"
| "account-auth"
Expand Down Expand Up @@ -56,6 +57,7 @@ export const SHOULD_USE_APP_GROUPS_BY_DEFAULT: Record<ExtensionType, boolean> =
"bg-download": true,
clip: true,
widget: true,
"watch-widget": true,
"account-auth": false,
"credentials-provider": false,
"device-activity-monitor": false,
Expand Down Expand Up @@ -305,6 +307,7 @@ export function productTypeForType(type: ExtensionType) {
export function needsEmbeddedSwift(type: ExtensionType) {
return [
"watch",
"watch-widget",
"spotlight",
"share",
"intent",
Expand All @@ -326,6 +329,8 @@ export function getFrameworksForType(type: ExtensionType) {
"ActivityKit",
"AppIntents",
];
} else if (type === "watch-widget") {
return ["AppIntents", "WidgetKit", "SwiftUI"];
} else if (type === "intent") {
return ["Intents"];
} else if (type === "intent-ui") {
Expand Down Expand Up @@ -360,6 +365,15 @@ export function isNativeTargetOfType(
target.getDefaultConfiguration().props.buildSettings
);
}
if (
(type === "watch-widget" || type === "widget") &&
target.props.productType === "com.apple.product-type.app-extension"
) {
return (
"WATCHOS_DEPLOYMENT_TARGET" in
target.getDefaultConfiguration().props.buildSettings
) && type === "watch-widget"
}
if (
type === "clip" &&
target.props.productType ===
Expand Down
10 changes: 9 additions & 1 deletion packages/apple-targets/src/withWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,11 +334,19 @@ const withWidget: ConfigPlugin<Props> = (config, props) => {
}

let bundleId = mainAppBundleId;

// Watch widgets are embedded in the watch app, so the root bundle identifier needs to
// match the watch app's bundle identifier. If the bundle identifier is not set, then well
// default to using the default main app's bundle identifier + watch suffix
if (props.type === "watch-widget") {
bundleId += ".watch";
}

bundleId += ".";

// Generate the bundle identifier. This logic needs to remain generally stable since it's used for a permanent value.
// Key here is simplicity and predictability since it's already appended to the main app's bundle identifier.
return mainAppBundleId + "." + getSanitizedBundleIdentifier(props.type);
return bundleId + getSanitizedBundleIdentifier(props.type);
})();

const deviceFamilies: DeviceFamily[] = config.ios?.isTabletOnly
Expand Down
139 changes: 136 additions & 3 deletions packages/apple-targets/src/withXcodeChanges.ts
5D40
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
PBXBuildFile,
PBXCopyFilesBuildPhase,
PBXFileReference,
PBXFileSystemSynchronizedBuildFileExceptionSet,
PBXFileSystemSynchronizedRootGroup,
Expand Down Expand Up @@ -537,6 +538,87 @@ function createWatchAppConfigurationList(

return configurationList;
}
function createWatchWidgetConfigurationList(
project: XcodeProject,
{
name,
cwd,
bundleId,
deploymentTarget,
currentProjectVersion,
hasAccentColor,
}: XcodeSettings
) {
const mainAppTarget = getMainAppTarget(project).getDefaultConfiguration();
// NOTE: No base Info.plist needed.

const common: BuildSettings = {

CLANG_ANALYZER_NONNULL: "YES",
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION: "YES_AGGRESSIVE",
CLANG_CXX_LANGUAGE_STANDARD: "gnu++20",
CLANG_ENABLE_OBJC_WEAK: "YES",
CLANG_WARN_DOCUMENTATION_COMMENTS: "YES",
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER: "YES",
CLANG_WARN_UNGUARDED_AVAILABILITY: "YES_AGGRESSIVE",
CODE_SIGN_STYLE: "Automatic",
CURRENT_PROJECT_VERSION: currentProjectVersion,
GCC_C_LANGUAGE_STANDARD: "gnu11",
INFOPLIST_FILE: cwd + "/Info.plist",
GENERATE_INFOPLIST_FILE: "YES",
INFOPLIST_KEY_CFBundleDisplayName: name,
// @ts-expect-error Not part of xcode project types yet
INTENTS_CODEGEN_LANGUAGE: "Swift",
LD_RUNPATH_SEARCH_PATHS: "$(inherited) @executable_path/Frameworks",
MARKETING_VERSION: "1.0",
MTL_FAST_MATH: "YES",
PRODUCT_BUNDLE_IDENTIFIER: bundleId,
PRODUCT_NAME: "$(TARGET_NAME)",
SDKROOT: "watchos",
SKIP_INSTALL: "YES",
SWIFT_EMIT_LOC_STRINGS: "YES",
SWIFT_OPTIMIZATION_LEVEL: "-Onone",
SWIFT_VERSION: "5.0",
TARGETED_DEVICE_FAMILY: "4",
WATCHOS_DEPLOYMENT_TARGET: deploymentTarget ?? "9.4",
};

if (hasAccentColor) {
common.ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = "$accent";
}

const debugBuildConfig = XCBuildConfiguration.create(project, {
name: "Debug",
buildSettings: {
...common,
// Diff
MTL_ENABLE_DEBUG_INFO: "INCLUDE_SOURCE",
SWIFT_ACTIVE_COMPILATION_CONDITIONS: "DEBUG $(inherited)",
// SWIFT_ACTIVE_COMPILATION_CONDITIONS: "DEBUG",
DEBUG_INFORMATION_FORMAT: "dwarf", // NOTE
},
});

const releaseBuildConfig = XCBuildConfiguration.create(project, {
name: "Release",
buildSettings: {
...common,
// Diff
SWIFT_OPTIMIZATION_LEVEL: "-Owholemodule",
COPY_PHASE_STRIP: "NO",
DEBUG_INFORMATION_FORMAT: "dwarf-with-dsym",
},
});

const configurationList = XCConfigurationList.create(project, {
buildConfigurations: [debugBuildConfig, releaseBuildConfig],
defaultConfigurationIsVisible: 0,
defaultConfigurationName: "Release",
});

return configurationList;
}

function createSafariConfigurationList(
project: XcodeProject,
{
Expand Down Expand Up @@ -885,6 +967,8 @@ function createConfigurationListForType(
return createAppClipConfigurationList(project, props);
} else if (props.type === "watch") {
return createWatchAppConfigurationList(project, props);
} else if (props.type === "watch-widget") {
return createWatchWidgetConfigurationList(project, props);
} else if (props.type === "app-intent") {
return createAppIntentConfigurationList(project, props);
} else {
Expand Down Expand Up @@ -1137,10 +1221,40 @@ async function applyXcodeChanges(
productType: productType,
});

if (props.type !== "watch-widget") {
const copyPhase = mainAppTarget.getCopyBuildPhaseForTarget(targetToUpdate);

if (!copyPhase.getBuildFile(appExtensionBuildFile.props.fileRef)) {
copyPhase.props.files.push(appExtensionBuildFile);
if (!copyPhase.getBuildFile(appExtensionBuildFile.props.fileRef)) {
copyPhase.props.files.push(appExtensionBuildFile);
}
}

// For watch widget extensions, also add them to the watch app target's copy phase
if (props.type === "watch-widget") {
const watchAppTarget = project.rootObject.props.targets.find((target) => {
return (
PBXNativeTarget.is(target) &&
target.props.productType === "com.apple.product-type.application" &&
"WATCHOS_DEPLOYMENT_TARGET" in target.getDefaultConfiguration().props.buildSettings
);
}) as PBXNativeTarget | undefined;

if (watchAppTarget) {
watchAppTarget.createBuildPhase(
PBXCopyFilesBuildPhase,
{
dstPath: "",
dstSubfolderSpec: 6,
name: "Embed App Extensions",
files: [
PBXBuildFile.create(project, {
fileRef: appExtensionBuildFile.props.fileRef,
}),
],
runOnlyForDeploymentPostprocessing: 0,
}
);
}
}
}

Expand All @@ -1156,7 +1270,26 @@ async function applyXcodeChanges(

configureJsExport(targetToUpdate);

mainAppTarget.addDependency(targetToUpdate);
// Add watch widget extensions as dependencies to the watch app target instead of the main app target
if (props.type === "watch-widget") {
// Find the watch app target
const watchAppTarget = project.rootObject.props.targets.find((target) => {
return (
PBXNativeTarget.is(target) &&
target.props.productType === "com.apple.product-type.application" &&
"WATCHOS_DEPLOYMENT_TARGET" in target.getDefaultConfiguration().props.buildSettings
);
}) as PBXNativeTarget | undefined;

if (watchAppTarget) {
watchAppTarget.addDependency(targetToUpdate);
} else {
// Fallback to main app target if watch app target is not found
mainAppTarget.addDependency(targetToUpdate);
}
} else {
mainAppTarget.addDependency(targetToUpdate);
}

const assetsDir = path.join(magicCwd, "assets");

Expand Down
0