Building Swift Package as XCFramework

Photo by Sebastian Pena Lambarri on Unsplash
Photo by Sebastian Pena Lambarri on Unsplash
Compared with Xcode project’s Build Settings, Package.swift is much simpler to use. However, when we convert a Xcode framework project into a Swift package, the XCFramework cannot be built successfully.

Compared with Xcode project’s Build Settings, Package.swift is much simpler to use. However, when we convert a Xcode framework project into a Swift package, the XCFramework cannot be built successfully. This article will introduce how to build Swift Package as XCFramework.

The complete code for this chapter can be found in .

Problem: Xcode does not support building Swift Package as XCFramework

In the following article, we explained how to build a framework project as a XCFramework.

In the sample code, there is a Greeting Swift package. We use the way introduced in the above article to build Greeting, and the script is as follows.

#!/bin/bash

archiveDir="$(pwd)/archives"
distDir="$(pwd)/dist"
scheme="Greeting"
debugSymbols=1

simulatorArchivePath="$archiveDir/$scheme-simulator"
(cd "$scheme" && xcodebuild archive 
  -scheme "$scheme" 
  -configuration "Release" 
  -destination "generic/platform=iOS Simulator" 
  -archivePath "$simulatorArchivePath" 
  VALID_ARCHS="i386 x86_64 arm64" 
  SKIP_INSTALL=NO 
  BUILD_LIBRARY_FOR_DISTRIBUTION=YES) || exit 1

deviceArchivePath="$archiveDir/$scheme-device"
(cd "$scheme" && xcodebuild archive 
  -scheme "$scheme" 
  -configuration "Release" 
  -destination "generic/platform=iOS" 
  -archivePath "$deviceArchivePath" 
  VALID_ARCHS="i386 x86_64 arm64" 
  SKIP_INSTALL=NO 
  BUILD_LIBRARY_FOR_DISTRIBUTION=YES) || exit 2

xcframework="$distDir/$scheme.xcframework"
simulatorDebugSymbols=""
deviceDebugSymbols=""
if [ $debugSymbols -eq 1 ]; then
  simulatorDebugSymbols="-debug-symbols $simulatorArchivePath.xcarchive/dSYMs/$scheme.framework.dSYM"
  deviceDebugSymbols="-debug-symbols $deviceArchivePath.xcarchive/dSYMs/$scheme.framework.dSYM"
fi

xcodebuild -create-xcframework 
  -framework "$simulatorArchivePath.xcarchive/Products/usr/local/lib/$scheme.framework" 
  $simulatorDebugSymbols 
  -framework "$deviceArchivePath.xcarchive/Products/usr/local/lib/$scheme.framework" 
  $deviceDebugSymbols 
  -output "$distDir/$scheme.xcframework"

Finally, the Greeting.xcframework we built is as shown below. We found that it was missing resources, umbrella header, swiftmodule, etc. Therefore, it is not a complete XCFramework. If we include it into an App project, errors will occur during compilation.

Build Swift package as XCFramework with xcodebuild archive.
Build Swift package as XCFramework with xcodebuild archive.

The complete XCFramework should be as follows.

Build Swift package as XCFramework with xcodebuild.
Build Swift package as XCFramework with xcodebuild.

Building Swift Package as XCFramework

In the above explanation, we learned that xcodebuild does not support building Swift package as XCFramework. However, if we look into the derived data generated by xcodebuild during the compilation, we will find that xcodebuild actually generates all the files needed by XCFramework. Therefore, we can use derived data to generate XCFramework.

The following script is modified from build_xcframework_from_swift_package.sh in Building XCFrameworks from a hierarchy of Swift Packages. This script has the following main points:

  • To be built as a Swift package of XCFramework, its type must be .dynamic.
  • We only need xcodebuild, not xcodebuild archive.
  • The scheme is the name of Swift package.
  • Copy the umbrella header file.
  • Copy all header files in the code.
  • Copy the swiftmodule and modulemap files.
  • Copy the bundle resource files.
#!/bin/bash

SIMULATOR_SDK="iphonesimulator"
DEVICE_SDK="iphoneos"

PACKAGE="Greeting"
CONFIGURATION="Release"
DEBUG_SYMBOLS="true"

BUILD_DIR="$(pwd)/build"
DIST_DIR="$(pwd)/dist"

build_framework() {
  scheme=$1
  sdk=$2
  if [ "$2" = "$SIMULATOR_SDK" ]; then
    dest="generic/platform=iOS Simulator"
  elif [ "$2" = "$DEVICE_SDK" ]; then
    dest="generic/platform=iOS"
  else
    echo "Unknown SDK $2"
    exit 11
  fi

  echo "Build framework"
  echo "Scheme: $scheme"
  echo "Configuration: $CONFIGURATION"
  echo "SDK: $sdk"
  echo "Destination: $dest"
  echo

  (cd "$PACKAGE" &&
    xcodebuild 
      -scheme "$scheme" 
      -configuration "$CONFIGURATION" 
      -destination "$dest" 
      -sdk "$sdk" 
      -derivedDataPath "$BUILD_DIR" 
      SKIP_INSTALL=NO 
      BUILD_LIBRARY_FOR_DISTRIBUTION=YES 
      OTHER_SWIFT_FLAGS="-no-verify-emitted-module-interface") || exit 12

  product_path="$BUILD_DIR/Build/Products/$CONFIGURATION-$sdk"
  framework_path="$BUILD_DIR/Build/Products/$CONFIGURATION-$sdk/PackageFrameworks/$scheme.framework"

  # Copy Headers
  headers_path="$framework_path/Headers"
  mkdir "$headers_path"
  cp -pv 
    "$BUILD_DIR/Build/Intermediates.noindex/$PACKAGE.build/$CONFIGURATION-$sdk/$scheme.build/Objects-normal/arm64/$scheme-Swift.h" 
    "$headers_path/" || exit 13

  # Copy other headers from Sources/
  headers=$(find "$PACKAGE/$scheme" -name "*.h")
  for h in $headers; do
    cp -pv "$h" "$headers_path" || exit 14
  done

  # Copy Modules
  modules_path="$framework_path/Modules"
  mkdir "$modules_path"
  cp -pv 
    "$BUILD_DIR/Build/Intermediates.noindex/$PACKAGE.build/$CONFIGURATION-$sdk/$scheme.build/$scheme.modulemap" 
    "$modules_path" || exit 15
  mkdir "$modules_path/$scheme.swiftmodule"
  cp -pv "$product_path/$scheme.swiftmodule"/*.* "$modules_path/$scheme.swiftmodule/" || exit 16

  # Copy Bundle
  bundle_dir="$product_path/${PACKAGE}_$scheme.bundle"
  if [ -d "$bundle_dir" ]; then
    cp -prv "$bundle_dir"/* "$framework_path/" || exit 17
  fi
}

create_xcframework() {
  scheme=$1

  echo "Create $scheme.xcframework"

  args=""
  shift 1
  for p in "$@"; do
    args+=" -framework $BUILD_DIR/Build/Products/$CONFIGURATION-$p/PackageFrameworks/$scheme.framework"
    if [ "$DEBUG_SYMBOLS" = "true" ]; then
      args+=" -debug-symbols $BUILD_DIR/Build/Products/$CONFIGURATION-$p/$scheme.framework.dSYM"
    fi
  done

  xcodebuild -create-xcframework $args -output "$DIST_DIR/$scheme.xcframework" || exit 21
}

reset_package_type() {
  (cd "$PACKAGE" && sed -i '' 's/( type: .dynamic,)//g' Package.swift) || exit
}

set_package_type_as_dynamic() {
  (cd "$PACKAGE" && sed -i '' "s/(.library(name: *"$1",)/1 type: .dynamic,/g" Package.swift) || exit
}

echo "**********************************"
echo "******* Build XCFrameworks *******"
echo "**********************************"
echo

rm -rf "$BUILD_DIR"
rm -rf "$DIST_DIR"

reset_package_type

set_package_type_as_dynamic "Greeting"
build_framework "Greeting" "$SIMULATOR_SDK"
build_framework "Greeting" "$DEVICE_SDK"
create_xcframework "Greeting" "$SIMULATOR_SDK" "$DEVICE_SDK"

Module Name of Swift Package

After xcodebuild builds the framework project and Swift package as XCFramework, the module names of the XCFramework will be different.

When building a framework project, the module name of its XCFramework will be the name of the scheme.

When building a Swift package, its XCFramework module name will be [package]_[product]. For the following Package.swift, its module name will be Greeting_GreetingSDK. In the above script, the scheme will be GreetingSDK.

// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "Greeting",
    platforms: [.iOS(.v13)],
    products: [
        .library(name: "GreetingSDK", type: .dynamic, targets: ["Greeting"]),
    ],
    targets: [
        .target(name: "Greeting"),
    ]
)

Therefore, when you want to access resources in a Swift package, use [package]_[product].bundle, as follows.

private var bundle: Bundle {
    get {
        let bundle = Bundle(for: Self.self)
        if let bundleURL = bundle.url(forResource: "Greeting_GreetingSDK",
                                      withExtension: "bundle") {
            return Bundle(url: bundleURL) ?? bundle
        } else {
            return bundle
        }
    }
}

Accessing Storyboard and Xib in Swift Package

When our App project accesses Greeting.storyboard in the sample code, the following errors may occur.

[Storyboard] Unknown class Greeting_Greeting.GreetingViewController in interface Builder file.

To solve this error, we must uncheck Inherit Module From Target in the storyboard and select Greeting in the Module.

This is because the name of GreetingViewController after compilation is Greeting.GreetingViewController. However, its module name is Greeting_Greeting. If Inherit Module From Target is checked in the storyboard, it will think that GreetingViewController is Greeting_Greeting.GreetingViewController, so an error occurs.

Uncheck [Inherit Module From Target].
Uncheck [Inherit Module From Target].

Conclusion

Compared to framework projects, Swift packages are a better and simpler way to manage projects. Using the script in this article, we can convert our original framework project into a Swift package.

Reference

Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like
Photo by Alex Alvarez on Unsplash
Read More

Dispatch Queue Tutorial

Grand Central Dispatch (GCD) provides efficient concurrent processing so that we don’t need to directly manage multiple threads. Its dispatch queues can execute tasks serially or concurrently.
Read More