Cocoa Touch Framework

最近在给朋友做一个项目,要求将涉及到的算法内容整理成一个单独的 framework,这样可以隐藏算法细节,方便交付。这个需求可以很容易地通过Cocoa Touch Framework实现。不过在交付的时候存在一个头疼的问题:默认情况下,Xcode 在编译 Cocoa Touch Framework 时只会编译出支持模拟器或者真机的 Framework,而无法编译出同时支持模拟器和真机的 Framework,即 Universal(Fat) Framework。这一需求还需要进一步地利用一些系统脚本来实现。

这里假设你已经有了一个能够正常工作,编译的包含 Cocoa Touch Framework 的工程。我这里实现时使用的是 Xcode10.2。

事实上我在调研中发现了很多不同的实现编译 Universal Framework 的教程,但是他们并不总是有用,我这里只遴选了我自己测试通过没有问题的思路。这一思路通过 Archive 过程来打包输出 framework

首先从 Xcode 左上角选择 Cocoa Touch Framework 的默认 scheme,然后点击 Edit Scheme

Edit Scheme

在 Archive 的 post-action 中添加一个运行脚本(New Run Script Action)

New Run Script Action

脚本内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
exec > /tmp/${PROJECT_NAME}_archive.log 2>&1

UNIVERSAL_OUTPUTFOLDER=${BUILD_DIR}/${CONFIGURATION}-universal

if [ "true" == ${ALREADYINVOKED:-false} ]
then
echo "RECURSION: Detected, stopping"
else
export ALREADYINVOKED="true"

# make sure the output directory exists
mkdir -p "${UNIVERSAL_OUTPUTFOLDER}"

echo "Building for iPhoneSimulator"
xcodebuild -workspace "${WORKSPACE_PATH}" -scheme "${TARGET_NAME}" -configuration ${CONFIGURATION} -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 6' ONLY_ACTIVE_ARCH=NO ARCHS='i386 x86_64' BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" ENABLE_BITCODE=YES OTHER_CFLAGS="-fembed-bitcode" BITCODE_GENERATION_MODE=bitcode clean build

# Step 1. Copy the framework structure (from iphoneos build) to the universal folder
echo "Copying to output folder"
# 这行是在我参考的脚本的基础上添加进去的。脚本在运行过程中有一个问题:在试图将
# archive过程中生成的device framework拷贝进来时,总是拷贝的framework文件夹
# 的内容,而非整个文件夹,所以我们这里手动创建这个文件夹
mkdir -p "${UNIVERSAL_OUTPUTFOLDER}/${FULL_PRODUCT_NAME}"
cp -R "${ARCHIVE_PRODUCTS_PATH}${INSTALL_PATH}/${FULL_PRODUCT_NAME}" "${UNIVERSAL_OUTPUTFOLDER}/${FULL_PRODUCT_NAME}"

# Step 2. Copy Swift modules from iphonesimulator build (if it exists) to the copied framework directory
SIMULATOR_SWIFT_MODULES_DIR="${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework/Modules/${TARGET_NAME}.swiftmodule/."
echo "SIMULATOR_SWIFT_MODULES_DIR: ${SIMULATOR_SWIFT_MODULES_DIR}"
if [ -d "${SIMULATOR_SWIFT_MODULES_DIR}" ]; then
cp -R "${SIMULATOR_SWIFT_MODULES_DIR}" "${UNIVERSAL_OUTPUTFOLDER}/${TARGET_NAME}.framework/Modules/${TARGET_NAME}.swiftmodule"
fi

# Step 3. Create universal binary file using lipo and place the combined executable in the copied framework directory
echo "Combining executables"
lipo -create -output "${UNIVERSAL_OUTPUTFOLDER}/${FULL_PRODUCT_NAME}/${EXECUTABLE_PATH}" "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${EXECUTABLE_PATH}" "${ARCHIVE_PRODUCTS_PATH}${INSTALL_PATH}/${EXECUTABLE_PATH}"

# Step 4. Create universal binaries for embedded frameworks
#for SUB_FRAMEWORK in $( ls "${UNIVERSAL_OUTPUTFOLDER}/${TARGET_NAME}.framework/Frameworks" ); do
#BINARY_NAME="${SUB_FRAMEWORK%.*}"
#lipo -create -output "${UNIVERSAL_OUTPUTFOLDER}/${TARGET_NAME}.framework/Frameworks/${SUB_FRAMEWORK}/${BINARY_NAME}" "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${SUB_FRAMEWORK}/${BINARY_NAME}" "${ARCHIVE_PRODUCTS_PATH}${INSTALL_PATH}/${TARGET_NAME}.framework/Frameworks/${SUB_FRAMEWORK}/${BINARY_NAME}"
#done

# Step 5. Convenience step to copy the framework to the project's directory
echo "Copying to project dir"
yes | cp -Rf "${UNIVERSAL_OUTPUTFOLDER}/${FULL_PRODUCT_NAME}" "${PROJECT_DIR}"

open "${PROJECT_DIR}"

fi

上述脚本的内容主要来自于export-fat-swift-dynamic-framework,我在这里根据实际情况进行了更改

此时执行 archive 操作(Product->Archive)完成后会自动弹出 Finder 窗口显示新生成的 framework 的位置(应当就是位于项目根目录下)。