From 8f06dd2782c023510bc29d95985ba4be880e2240 Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Fri, 28 Aug 2026 11:50:17 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat(=F0=9F=8D=8F):=20Swift=20Package=20Man?= =?UTF-8?q?ager=20support=20for=20iOS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit React Native 0.87 ships preview SwiftPM support, and CocoaPods trunk goes read-only in December 2026. A library that ships its own Package.swift is referenced verbatim by autolinking, so this is additive: SwiftPM ignores the podspec and CocoaPods ignores Package.swift. Autolinking exposes the library through a symlink under the app's build directory and resolves the manifest's relative paths against it, so the two React Native package paths are fixed for every standard app. Skia's binaries stay out of the package. The manifest prefers the react-native-skia-apple-ios copy npm already installs for the CocoaPods build and falls back to the published Swift package, so a checkout that has run `yarn install` builds without network access. Two defines the podspec never needed: CocoaPods forces RCT_NEW_ARCH_ENABLED and RCT_REMOVE_LEGACY_ARCH project-wide, and the SwiftPM path defines neither, yet Skia's Apple sources still gate on both. Co-Authored-By: Claude Opus 5 (1M context) --- packages/skia/Package.swift | 117 ++++++++++++++++++++++++ packages/skia/include/ReactNativeSkia.h | 6 ++ packages/skia/package.json | 3 + packages/skia/react-native.config.js | 5 + 4 files changed, 131 insertions(+) create mode 100644 packages/skia/Package.swift create mode 100644 packages/skia/include/ReactNativeSkia.h create mode 100644 packages/skia/react-native.config.js diff --git a/packages/skia/Package.swift b/packages/skia/Package.swift new file mode 100644 index 0000000000..b6c6b9a9cc --- /dev/null +++ b/packages/skia/Package.swift @@ -0,0 +1,117 @@ +// swift-tools-version: 6.0 +// +// SwiftPM manifest for @shopify/react-native-skia. iOS, Ganesh. +// Additive: the CocoaPods podspec remains the supported default. +// +// React Native 0.87+ references a library that ships its own Package.swift +// through a symlink at /ios/build/generated/autolinking/libs/, and +// resolves the relative paths below against that symlink. They are the same +// for every standard app, because the symlink location is. + +import Foundation +import PackageDescription + +// Skia's xcframeworks are ~200MB and are not carried in this package. They come +// from the react-native-skia-apple-ios npm package, which @shopify/react-native-skia +// depends on, so a checkout that has installed its dependencies builds offline. +// Xcode passes the symlink as the package directory, hence resolvingSymlinks. +let packageRoot = URL(fileURLWithPath: Context.packageDirectory) + .resolvingSymlinksInPath().path + +let binariesPath = [ + "../../react-native-skia-apple-ios", // consumer: sibling in node_modules + "../../node_modules/react-native-skia-apple-ios", // this monorepo +] +.map { "\(packageRoot)/\($0)" } +.first { FileManager.default.fileExists(atPath: "\($0)/Package.swift") } + +guard let binariesPath else { + // Reached when the package was skipped at install time (`--omit=optional`, + // or a non-Apple `os` filter), which SwiftPM would otherwise report as an + // unresolvable dependency path. + fatalError( + """ + react-native-skia-apple-ios was not found next to @shopify/react-native-skia. \ + It ships the prebuilt Skia binaries this package links against. Reinstall \ + dependencies without --omit=optional, on macOS. + """) +} + +let package = Package( + name: "ReactNativeSkia", + platforms: [.iOS(.v15)], + products: [ + // Autolinking looks this name up verbatim; react-native.config.js pins it. + .library(name: "ReactNativeSkia", targets: ["ReactNativeSkia"]) + ], + dependencies: [ + .package(name: "React-GeneratedCode", path: "../../../ios"), + .package(name: "ReactNative", path: "../../../../xcframeworks"), + .package(path: binariesPath), + ], + targets: [ + .target( + name: "ReactNativeSkia", + dependencies: [ + .product(name: "ReactHeaders", package: "ReactNative"), + .product(name: "ReactNativeHeaders", package: "ReactNative"), + .product(name: "ReactNativeDependenciesHeaders", package: "ReactNative"), + .product(name: "ReactAppHeaders", package: "React-GeneratedCode"), + .product(name: "react-native-skia-apple-ios", package: "react-native-skia-apple-ios"), + ], + // apple/ and cpp/ have no common ancestor below the package root, and + // .headerSearchPath cannot escape the target path. + path: ".", + exclude: [ + "cpp/rnskia/RNDawnWindowContext.cpp", // Graphite only + "cpp/rnskia/RNDawnInterop.cpp", // Graphite only + ], + // Explicit, so SwiftPM never walks node_modules, lib, android, or the + // headers-only cpp/skia — which carries x86 AVX skcms sources that + // cannot build for arm64. + sources: [ + "apple", + "cpp/api", + "cpp/jsi", + "cpp/rnskia", + "cpp/utils", + // libskottie.a needs skjson and Apple builds no libskjson archive. + // CocoaPods compiles this via its cpp/**/*.cpp glob. + "cpp/skia/modules/jsonreader/SkJSONReader.cpp", + ], + cxxSettings: [ + // Replaces the podspec's recursive cpp/** glob, which expands to 90. + .headerSearchPath("cpp"), // "api/…", "jsi/…", "utils/…" + .headerSearchPath("cpp/skia"), // "include/core/…", "modules/…", "src/…" + .headerSearchPath("cpp/rnskia"), // apple/ uses bare "RNSkView.h" + .headerSearchPath("cpp/utils"), // apple/ uses bare "RNSkLog.h" + + // CocoaPods forces both project-wide; the SwiftPM path defines neither. + // Skia's Apple sources still gate on them: without them RNSkiaModule's + // legacy branch fails to compile and -getTurboModule: is dropped, so + // the JSI bindings never install. + .define("RCT_NEW_ARCH_ENABLED", to: "1"), + .define("RCT_REMOVE_LEGACY_ARCH", to: "1"), + + .define("SK_METAL", to: "1"), + .define("SK_GANESH", to: "1"), + .define("SK_IMAGE_READ_PIXELS_DISABLE_LEGACY_API", to: "1"), + .define("SK_DISABLE_LEGACY_SHAPER_FACTORY", to: "1"), + + // React's prebuilt C++ ABI is NDEBUG-gated, and Skia derives + // SK_RELEASE from it. Omitting either breaks the Release link. + .define("DEBUG", .when(configuration: .debug)), + .define("NDEBUG", .when(configuration: .release)), + ], + linkerSettings: [ + .linkedFramework("MetalKit"), + .linkedFramework("AVFoundation"), + .linkedFramework("AVKit"), + .linkedFramework("CoreMedia"), + ] + ) + ], + // React Native's headers need C++20, and so does Skia m152: SkMathPriv.h + // calls std::countl_zero, std::countr_zero and std::popcount. + cxxLanguageStandard: .cxx20 +) diff --git a/packages/skia/include/ReactNativeSkia.h b/packages/skia/include/ReactNativeSkia.h new file mode 100644 index 0000000000..7a6c49d136 --- /dev/null +++ b/packages/skia/include/ReactNativeSkia.h @@ -0,0 +1,6 @@ +#pragma once + +// Umbrella header for the SwiftPM target's publicHeadersPath. Nothing outside +// this package includes Skia or RNSkia headers — React Native resolves the +// Fabric component and TurboModule by class name — so the public surface is +// deliberately empty. diff --git a/packages/skia/package.json b/packages/skia/package.json index 999c56c467..605aa28a00 100644 --- a/packages/skia/package.json +++ b/packages/skia/package.json @@ -32,6 +32,9 @@ "cpp/**/*.{h,cpp}", "apple/**", "react-native-skia.podspec", + "Package.swift", + "include/**", + "react-native.config.js", "dist/**", "libs/.graphite" ], diff --git a/packages/skia/react-native.config.js b/packages/skia/react-native.config.js new file mode 100644 index 0000000000..9892e6fecf --- /dev/null +++ b/packages/skia/react-native.config.js @@ -0,0 +1,5 @@ +// Pins the SwiftPM target name. Without it the name is derived — today from the +// npm name (ReactNativeSkia), but an in-flight React Native change derives it +// from the podspec instead (react-native-skia). The name is also the header +// import prefix, so it must not move. +module.exports = {spm: {name: 'ReactNativeSkia'}}; From a197edded296f4ab5a78b38c61c335b283231968 Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Tue, 8 Sep 2026 13:02:11 +0200 Subject: [PATCH 2/2] =?UTF-8?q?docs(=F0=9F=8D=8F):=20document=20the=20Swif?= =?UTF-8?q?tPM=20build=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Covers the React Native 0.87 floor, how autolinking resolves the manifest's relative paths, the two architecture defines CocoaPods supplies but SwiftPM does not, where the binaries come from, and the stale Package.resolved pin that silently keeps the previous binary source when switching between them. Co-Authored-By: Claude Opus 5 (1M context) --- packages/skia/CONTRIBUTING.md | 37 +++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/packages/skia/CONTRIBUTING.md b/packages/skia/CONTRIBUTING.md index b7efb5ee90..dbb176150c 100644 --- a/packages/skia/CONTRIBUTING.md +++ b/packages/skia/CONTRIBUTING.md @@ -129,6 +129,43 @@ The npm packages this library consumes (`react-native-skia-android`, `react-nati Back in this repo, bump the prebuilt binary versions in `packages/skia/package.json` (`react-native-skia-android` and `react-native-skia-apple-*`) to the versions you just published, run `yarn`, and re-run `pod install` in the example app so it consumes the released binaries. Drop the throwaway `node_modules` and `libs/ios/.version` edits from step 3. +### Swift Package Manager (preview) + +CocoaPods stays the default. `Package.swift` is additive: SwiftPM ignores the +podspec, and CocoaPods ignores `Package.swift`. + +SwiftPM support requires **React Native 0.87 or newer** — earlier releases ship +no `scripts/spm`. `apps/example` is on an older version, so it cannot exercise +this path. + +Autolinking references the library through a symlink at +`/ios/build/generated/autolinking/libs/ReactNativeSkia`, and SwiftPM +resolves the manifest's relative paths against that symlink rather than against +`packages/skia`. The two React Native package paths are therefore identical for +every standard app. The target name is pinned in `react-native.config.js`; +without it a future React Native release would derive it from the podspec +instead and change the header import prefix. + +Skia's Apple sources still gate on `RCT_NEW_ARCH_ENABLED` and +`RCT_REMOVE_LEGACY_ARCH`. CocoaPods forces both project-wide; the SwiftPM path +defines neither, so `Package.swift` defines them itself. + +#### Binaries + +The manifest links the `react-native-skia-apple-ios` npm package, the same one +the CocoaPods build uses, so no network is needed once dependencies are +installed. It is resolved by path, from either a sibling in `node_modules` or +this monorepo's root, and the manifest fails with an explanatory message when +neither exists — which is what an `--omit=optional` install looks like. + +Fetching the binaries from a released Swift package instead is future work; it +becomes useful only once the binary npm packages are no longer dependencies. See +[wcandillon/react-native-skia-binaries](https://github.com/wcandillon/react-native-skia-binaries). + +After changing which binaries a checkout uses, delete +`ios/.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved` +— a stale pin silently keeps the previous source. + ### Publishing - Run the commands in the [Building](#building) section