將 Swift Package 編譯為 XCFramework

Photo by Sebastian Pena Lambarri on Unsplash
Photo by Sebastian Pena Lambarri on Unsplash
相較於 Xcode project 的 Build Settings,Package.swift 使用起來簡單很多。然而,當我們將 Xcode framework project 轉換成 Swift package 時,卻無法順利地產生 XCFramework。

相較於 Xcode project 的 Build Settings,Package.swift 使用起來簡單很多。然而,當我們將 Xcode framework project 轉換成 Swift package 時,卻無法順利地產生 XCFramework。本文章將介紹,如何將 Swift Package 編譯為 XCFramework。

完整程式碼可以在 下載。

問題:Xcode 不支援將 Swift Package 編譯為 XCFramework

我們在以下的文章中,講解到如何將 framework project 編譯為 XCFramework。

在範例程式碼中,有一個 Greeting Swift package。我們用以上文章中介紹的方法來編譯 Greeting,其 script 如下。

#!/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"

最後,我們編譯出來的 Greeting.xcframework 如下圖。我們發現它遺漏了 resources、umbrella header、swiftmodule 等。所以,它不是一個完整的 XCFramework。如果我們將它引入到 App project 中,編譯時會發生錯誤。

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

真正完整的 XCFramework 應該要如下。

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

將 Swift Package 編譯為 XCFramework

在以上的講解中,我們了解到 xcodebuild 不支援將 Swift package 編譯為 XCFramework。但是,如果分析一下 xcodebuild 在編譯過程中產生的 derived data,我們會發現,所有 XCFramework 需要的檔案,xcodebuild 其實都有產生出來。因此,我們可以利用 derived data 來產生 XCFramework。

以下的 script 是將 Building XCFrameworks from a hierarchy of Swift Packages 中提供的 build_xcframework_from_swift_package.sh 修改而來的。此 script 有以下的要點:

  • 要編譯成 XCFramework 的 Swift package,其 type 必須為 .dynamic
  • 我們只需要 xcodebuild,而不需要 xcodebuild archive
  • Scheme 為 Swift package 的名稱。
  • 複製 umbrella header 檔案。
  • 複製程式碼中所有的 header 檔案。
  • 複製 swiftmodule 和 modulemap 檔案。
  • 複製 bundle resource 檔案。
#!/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"

Swift Package 的 Module 名稱

xcodebuild 在編譯 framework project 和 Swift package 成 XCFramework 後,其 XCFramework 的 module 名稱會不相同。

當編譯 framework project 時,其 XCFramework 的 module 名稱會是 scheme 的名稱。

當編譯 Swift package 時,其 XCFramework 的 module 名稱會是 [package]_[product]。如下的 Package.swift,其 module 名稱會是 Greeting_GreetingSDK。在上面的 script 中,其 scheme 會是 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"),
    ]
)

因此,當想要存取 Swift package 中的 resources 時,要使用 [package]_[product].bundle,如下。

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
        }
    }
}

在 Swift Package 中存取 Storyboard 和 Xib

當我們的 App project 存取範例程式中的 Greeting.storyboard 時,可能會有以下的錯誤發生。

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

要解決此錯誤,我們必須要在 storyboard 中,取消勾選 Inherit Module From Target,並且在 Module 中選擇 Greeting。

這是因為 GreetingViewController 在編譯後,其名稱為 Greeting.GreetingViewController。但是,其 module 名稱 Greeting_Greeting。如果勾選 Inherit Module From Target 時,在該 storyboard 中,它會認為 GreetingViewController 是 Greeting_Greeting.GreetingViewController,因而發生錯誤。

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

結語

相較於 framework project,Swift package 是一個更好且更簡單的專案管理方式。利用本文章中的 script,我們可以將原本的 framework project 轉換成 Swift package。

參考

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

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

Dispatch Queue 教學

GCD 提供有效率的並行處理,讓我們不需要直接管理多執行緒。它的 Dispatch Queues 可以循序地(serially)或是並行地(concurrently)執行任務。我們只需要將要並行的程式當作任務提交到 dispatch queues 就可以了。
Read More
Photo by Fabian Gieske on Unsplash
Read More

SwiftUI @State & @Binding 教學

SwiftUI 推出了兩個 Property Wrapper – @State and @Binding。利用它們可以達到變數的 Two-way Binding 功能。也就是當變數的值改變時,它會重新被顯示。本章藉由製作一個 Custom View 來展示如何使用 @State 和 @Binding。
Read More
Photo by Svitlana on Unsplash
Read More

iOS:禁止螢幕截圖

基於一些理由,我們可能會想要禁止使用者對我們的 app 做螢幕截圖。然而,iOS 並沒有提供這樣的功能。所幸,我們可以利用 UITextField 來達到此效果。
Read More