Skip to content
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
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,33 @@ To support iOS 15.4, ensure your `Podfile` specifies the platform version:
platform :ios, '15.4'
```

The iOS player is delivered as a Swift Package, so frameworks must be linked
dynamically. Enable dynamic frameworks in your `Podfile`:

```ruby
use_frameworks! :linkage => :dynamic
```

`DotLottiePlayer` ships as a Swift Package binary target. Xcode embeds it
automatically when you build from the IDE, but command-line builds
(`react-native run-ios`, `xcodebuild`) do not — the app then fails to install
with *"DotLottiePlayer.framework is missing its bundle executable."* To fix this,
require our embed helper and call it from `post_install` in your `Podfile`:

```ruby
require File.join(File.dirname(`node --print "require.resolve('@lottiefiles/dotlottie-react-native/package.json')"`), "scripts/dotlottie_embed")

# ... inside your target's post_install block:
post_install do |installer|
dotlottie_embed_frameworks!(installer)
# ... your other post_install steps
end
```

> Expo projects don't need this step — the `withDotLottie` config plugin wires it
> into the generated `Podfile` automatically during prebuild. See [Expo
> Configuration](#expo-configuration).

After installing the package, navigate to the `ios` directory and install the pods:

```sh
Expand Down Expand Up @@ -61,7 +88,7 @@ module.exports = (async () => {

### Expo Configuration

Expo projects must include the native binaries before this library can render animations. We ship a config plugin scaffold (`withDotLottie`) so Expo developers can prepare builds with minimal setup.
Expo projects must include the native binaries before this library can render animations. We ship a config plugin (`withDotLottie`) so Expo developers can prepare builds with minimal setup. It sets the iOS deployment target, forces dynamic frameworks, and wires the `DotLottiePlayer` framework embed step into the generated `Podfile` automatically — so the manual `Podfile` step from [Pod Installation (iOS)](#pod-installation-ios) is **not** required for Expo apps.

1. **Add the plugin** – the package already declares it under the `expo.plugins` field, so it is applied automatically. To customize behaviour, you can reference it explicitly in `app.json`:

Expand Down
9 changes: 9 additions & 0 deletions example/ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ require Pod::Executable.execute_command('node', ['-p',
{paths: [process.argv[1]]},
)', __dir__]).strip

# dotLottie ships DotLottiePlayer as a Swift Package binary target. This helper
# embeds the SPM-built DotLottiePlayer.framework into the app bundle so
# command-line builds find the framework's executable. This example lives in the
# library's monorepo, so it requires the helper by relative path. Published
# consumers should instead use:
# require File.join(File.dirname(`node --print "require.resolve('@lottiefiles/dotlottie-react-native/package.json')"`), "scripts/dotlottie_embed")
require File.expand_path("../../scripts/dotlottie_embed", __dir__)

platform :ios, '15.4'
project 'example.xcodeproj'
prepare_react_native_project!
Expand All @@ -27,6 +35,7 @@ target 'example' do
)

post_install do |installer|
dotlottie_embed_frameworks!(installer)
# https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202
react_native_post_install(
installer,
Expand Down
8 changes: 4 additions & 4 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
"android:fabric": "ORG_GRADLE_PROJECT_newArchEnabled=true react-native run-android",
"android:paper:build": "sh -c 'cd android && ORG_GRADLE_PROJECT_newArchEnabled=false ./gradlew assembleDebug'",
"android:fabric:build": "sh -c 'cd android && ORG_GRADLE_PROJECT_newArchEnabled=true ./gradlew assembleDebug'",
"ios:paper": "RCT_NEW_ARCH_ENABLED=0 sh -c 'cd ios && pod install && cd .. && react-native run-ios'",
"ios:fabric": "RCT_NEW_ARCH_ENABLED=1 sh -c 'cd ios && pod install && cd .. && react-native run-ios'",
"ios:paper:build": "RCT_NEW_ARCH_ENABLED=0 sh -c 'cd ios && xcodebuild -workspace example.xcworkspace -scheme example -configuration Debug -sdk iphonesimulator -destination \"generic/platform=iOS Simulator\" CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO build CODEGEN_GENERATE_ONLY=YES'",
"ios:fabric:build": "RCT_NEW_ARCH_ENABLED=1 sh -c 'cd ios && xcodebuild -workspace example.xcworkspace -scheme example -configuration Debug -sdk iphonesimulator -destination \"generic/platform=iOS Simulator\" CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO build CODEGEN_GENERATE_ONLY=YES'",
"ios:paper": "RCT_NEW_ARCH_ENABLED=0 USE_FRAMEWORKS=dynamic sh -c 'cd ios && pod install && cd .. && react-native run-ios'",
"ios:fabric": "RCT_NEW_ARCH_ENABLED=1 USE_FRAMEWORKS=dynamic sh -c 'cd ios && pod install && cd .. && react-native run-ios'",
"ios:paper:build": "RCT_NEW_ARCH_ENABLED=0 USE_FRAMEWORKS=dynamic sh -c 'cd ios && xcodebuild -workspace example.xcworkspace -scheme example -configuration Debug -sdk iphonesimulator -destination \"generic/platform=iOS Simulator\" CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO build CODEGEN_GENERATE_ONLY=YES'",
"ios:fabric:build": "RCT_NEW_ARCH_ENABLED=1 USE_FRAMEWORKS=dynamic sh -c 'cd ios && xcodebuild -workspace example.xcworkspace -scheme example -configuration Debug -sdk iphonesimulator -destination \"generic/platform=iOS Simulator\" CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO build CODEGEN_GENERATE_ONLY=YES'",
"web": "webpack serve --mode development",
"lint": "eslint .",
"start": "react-native start",
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@
}
},
"./plugin": "./plugins/withDotLottie.js",
"./plugins/withDotLottie": "./plugins/withDotLottie.js"
"./plugins/withDotLottie": "./plugins/withDotLottie.js",
"./package.json": "./package.json"
},
"files": [
"src",
"lib",
"android",
"ios",
"plugins",
"scripts",
"cpp",
"*.podspec",
"!ios/build",
Expand Down
66 changes: 66 additions & 0 deletions plugins/withDotLottie.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
const fs = require('fs');
const path = require('path');
const {
createRunOncePlugin,
withDangerousMod,
withPodfileProperties,
} = require('@expo/config-plugins');
const pkg = require('../package.json');
Expand Down Expand Up @@ -39,6 +42,67 @@ const isLowerVersion = (current, minimum) => {
return false;
};

const EMBED_CALL = ' dotlottie_embed_frameworks!(installer)';

// Path to the Ruby helper (without the `.rb` extension Ruby's `require` adds).
const EMBED_HELPER = path.join(__dirname, '..', 'scripts', 'dotlottie_embed');

// Builds the Podfile `require` line. We point at the helper by a path relative
// to the Podfile (ios dir) rather than `node --print require.resolve(...)`,
// because that resolves correctly whether the package is installed under
// node_modules (real consumers) or linked as a workspace (this monorepo).
const buildEmbedRequire = (podfileDir) => {
let rel = path.relative(podfileDir, EMBED_HELPER);
if (!rel.startsWith('.')) {
rel = `./${rel}`;
}
return `require File.expand_path("${rel}", __dir__)`;
};

/**
* Injects the dotLottie embed helper into the prebuild-generated ios/Podfile.
*
* Expo regenerates the Podfile on every prebuild, so this runs as a dangerous
* mod each time. It is idempotent: it adds a `require` for the package's Ruby
* helper after the leading requires, and calls `dotlottie_embed_frameworks!`
* inside the existing `post_install do |installer|` block. The helper copies the
* SPM-built DotLottiePlayer.framework into the app bundle so CLI builds find its
* executable.
*/
const withDotLottiePodfileEmbed = (config) =>
withDangerousMod(config, [
'ios',
(modConfig) => {
const podfilePath = path.join(
modConfig.modRequest.platformProjectRoot,
'Podfile'
);
let contents = fs.readFileSync(podfilePath, 'utf8');

if (!contents.includes('scripts/dotlottie_embed')) {
const requireLine = buildEmbedRequire(path.dirname(podfilePath));
const requireAnchor = contents.lastIndexOf('\nrequire ');
if (requireAnchor !== -1) {
const lineEnd = contents.indexOf('\n', requireAnchor + 1);
const insertAt = lineEnd === -1 ? contents.length : lineEnd;
contents = `${contents.slice(0, insertAt)}\n${requireLine}${contents.slice(insertAt)}`;
} else {
contents = `${requireLine}\n${contents}`;
}
}

if (!contents.includes('dotlottie_embed_frameworks!')) {
contents = contents.replace(
/(post_install do \|installer\|\n)/,
`$1${EMBED_CALL}\n`
);
}

fs.writeFileSync(podfilePath, contents);
return modConfig;
},
]);

const withDotLottie = (config) => {
const iosConfig = config.ios ?? {};

Expand All @@ -51,6 +115,8 @@ const withDotLottie = (config) => {

config.ios = iosConfig;

config = withDotLottiePodfileEmbed(config);

return withPodfileProperties(config, (podfileConfig) => {
const currentTarget = podfileConfig.modResults['ios.deploymentTarget'];

Expand Down
72 changes: 72 additions & 0 deletions scripts/dotlottie_embed.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# dotLottie iOS embed helper.
#
# The iOS player ships `DotLottiePlayer` as a Swift Package `binaryTarget`
# (an xcframework) pulled in through `spm_dependency` in
# dotlottie-react-native.podspec. CocoaPods attaches that SPM product to the
# Pods `dotlottie-react-native` target, not to the app target. The Xcode IDE
# auto-embeds binary targets, but command-line builds (`expo run:ios`,
# `react-native run-ios`, `xcodebuild`) do not. The framework folder gets copied
# into the app bundle without its Mach-O binary, so installation fails with
# "DotLottiePlayer.framework is missing its bundle executable".
#
# This helper adds a build phase to the app target that copies the fully built
# framework from ${BUILT_PRODUCTS_DIR} into the app bundle and signs it.
#
# Usage (Podfile):
# require File.join(File.dirname(`node --print "require.resolve('@lottiefiles/dotlottie-react-native/package.json')"`), "scripts/dotlottie_embed")
# ...
# post_install do |installer|
# dotlottie_embed_frameworks!(installer)
# end

DOTLOTTIE_EMBED_PHASE_NAME = '[dotLottie] Embed DotLottiePlayer'.freeze

DOTLOTTIE_EMBED_SCRIPT = <<~SH.freeze
set -e
FRAMEWORK="DotLottiePlayer.framework"
SRC="${BUILT_PRODUCTS_DIR}/${FRAMEWORK}"
if [ ! -d "${SRC}" ]; then
echo "warning: ${FRAMEWORK} not found at ${SRC}; skipping dotLottie embed"
exit 0
fi
DEST="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
mkdir -p "${DEST}"
rsync -av --delete "${SRC}/" "${DEST}/${FRAMEWORK}/"
if [ "${CODE_SIGNING_ALLOWED}" = "YES" ] && [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" ] && [ "${EXPANDED_CODE_SIGN_IDENTITY}" != "-" ]; then
codesign --force --sign "${EXPANDED_CODE_SIGN_IDENTITY}" --preserve-metadata=identifier,entitlements,flags "${DEST}/${FRAMEWORK}"
fi
SH

# Adds the embed build phase to every application target managed by CocoaPods.
def dotlottie_embed_frameworks!(installer)
installer.aggregate_targets.each do |aggregate_target|
user_project = aggregate_target.user_project
next if user_project.nil?

changed = false
aggregate_target.user_target_uuids.each do |uuid|
native_target = user_project.objects_by_uuid[uuid]
next if native_target.nil?

changed = true if dotlottie_add_embed_phase(native_target)
end

user_project.save if changed
end
end

# Returns true if a phase was added, false if it already existed.
def dotlottie_add_embed_phase(native_target)
existing = native_target.shell_script_build_phases.find do |phase|
phase.name == DOTLOTTIE_EMBED_PHASE_NAME
end
return false unless existing.nil?

phase = native_target.new_shell_script_build_phase(DOTLOTTIE_EMBED_PHASE_NAME)
phase.shell_path = '/bin/sh'
phase.shell_script = DOTLOTTIE_EMBED_SCRIPT
phase.input_paths = ['$(BUILT_PRODUCTS_DIR)/DotLottiePlayer.framework/DotLottiePlayer']
phase.output_paths = ['$(TARGET_BUILD_DIR)/$(FRAMEWORKS_FOLDER_PATH)/DotLottiePlayer.framework/DotLottiePlayer']
Pod::UI.puts "[dotLottie] Added '#{DOTLOTTIE_EMBED_PHASE_NAME}' phase to #{native_target.name}" if defined?(Pod::UI)
true
end
Loading