Framework制作 Cocoapods私有库

Framework制作教程
  1. 详细步骤图
  2. 创建源码项目
  3. 创建对外发布的Framework项目
一、详细步骤图
FM制作流程.png
二、创建源码项目

以MAWebKit为例, 源码项目统一都是放到GitLab源码Group下。

1. 创建Framework项目

Xcode -> File -> New -> Project 选择Cocoa Touch Framework模板,新建项目MAWebKit,勾选Include Unit Tests 为写单元测试和Sonar扫描做准备。
点击下一步选择合适的目录保存。


image.png
2. 新生成的项目结构如图
image.png
3. 多个组件配置

如果一个组件库包含多个组件,类似Cocoapods的subspec。那需要新建其他组件的Framework。 例如:MAWebKitCore、MAWebKitNFC、MAWebKitGPS、MAWebKitFace

  • Project中选中项目文件,展示出Target列表,最下方选择 + 新建Target。


    image.png
  • 选择Cocoa Touch Framework,填写必要项,该处可以不勾选UnitTests,点击完成创建。


    image.png
  • 按照上述两步骤依次创建MAWebKitNFC、MAWebKitGPS、MAWebKitFace。创建完成后整体项 目结构如图。


    image.png
4. 配置项目
  • 配置所有Target的Deployment Target 为9.0
  • 配置所有Target的版本号Version和Build
  • 反选所有Target的Automatically Manage Signing(自动管理签名)
  • 配置所有Target的Build Settings,Defines Module 改为 No (不生成module文件)

按照目前这种结构产生的文件目录,在项目目录下比较乱,可以把所有info.plist文件所在文件夹整 理到同一个文件夹Supports下,步骤如下:

  • 项目中新建Group,命名为Supports
  • 将所有info.plist所在文件夹的Group直接在Xcode中拖到Supports的Group下。
  • 配置Build Settings中的info.plist File的值为: Supports/MAWebKit/info.plist (MAWebKit是 Target名字),由于info.plist文件的物理位置发生改变,原来的配置无法找到,所以需要重新进行配置


    image.png

    image.png

    image.png

    image.png
5. 配置Framework之间的依赖关系

Framework之间的依赖关系分为两种:
项目内的Framework依赖 和 跨项目的Framework依赖 。

(1) 项目内的Framework依赖: 直接配置
例如:MAWebKitNFC依赖于MAWebKitCore、 MAWebKitGPS依赖于MAWebKitCore

  • 选中MAWebKitNFC的Target,点击General标签,
  • Frameworks and Libraries 中选择加号,选择 MAWebKitCore ,同时选
    择 Do Not Embed 。
  • 注意点:如果MAWebKitCore中有Category,则需要在MAWebKitNFC的Build Settings中配置 other linker flag 增加 -ObjC 选项。
image.png

(2) 跨项目的Framework依赖:cocoapods管理
例如:MAWebKitGPS依赖⻨芽的 MAGPSKit ,MAWebKitPhoto依赖第三方
库 TZImagePickerController ,这样我们就通过cocoapods来进行管理

  • 打开终端Terminal,cd到项目所在目录
  • 运行命令: pod init , 这样会针对所有的Target生成一个Pofile文件
image.png
  • 修改平台的版本号。
  • 给target添加私有库或第三方库依赖的配置。
  • 删除不需要配置依赖的target项。
  • 开启 use_frameworks! 配置,为了打包的时候不将依赖的第三方库打到Framework中。
  • 在终端中运行命令: pod install ,生成workspace文件、导入依赖的第三方库。
6. 源码目录结构规划

为了方便统一管理主framework和各个subspec的framework源码,将所有源码按照以下步骤,进行集中 管理。

  • 创建源码总目录文件夹 MAWebKit ,用于存储资源文件和源码文件 为了方便源码调试,才设计这样的目录结构(参考了源码cocoapods私有库目录结构)

  • 创建资源文件存储文件夹( Assets ),并创建bundle资源文件( MAWebKit.bundle ),如:MAWebKit/Assets/MAWebKit.bundle


    image.png
  • 创建源码存储文件夹( Classes ),并在其下依次创建各个target的源码文件夹,如:
    MAWebKit/Classes/MAWebKit , MAWebKit/Classes/MAWebKitCore , MAWebKit/Classes/MAWebKitGPS

  • 在各个target文件夹下依次创建公有文件夹( Public )和私有文件夹( Private ),如:MAWebKit/Classes/MAWebKit/Public , MAWebKit/Classes/MAWebKit/Private


    image.png
  • 将Supports下的各个target的头文件,依次移动到各自的Public目录下

  • 打开framework工程,依次删除Supports目录下相关头文件的引用

  • 右键点击 Add Files To ”MAWebKit“ ,添加Assets到工程中

  • 右键点击 Add Files To ”MAWebKit“ ,添加Classes到工程中

  • 依次编辑各个target的Public中的头文件为对应的framework,且选择可⻅范围为public

  • 依次编辑各个target的Public中的总头文件(如 MAWebKitCore.h ), 添加版本号备注信息, 方便集成方快速知道当前所用组件的版本号


    image.png

    image.png

    image.png

    image.png
7. 编写Framework源码

选中Public或者Private目录,右键,新建类文件,例如UIView+MAPrivate.h、 UIView+MAPrivate.m。选择对应的Framework(MAWebKitCore)的target。

image.png
  • 配置对外暴露的.h文件
    选中.h文件,直接在最右侧的属性栏中选择可⻅范围(Private/Project/Public)。
    在Build Phases的Headers中直接拖拽进行配置。
    将所有设置为Public可⻅范围的.h文件引入到和Framework同名的.h文件中,统一使用尖括号 <>的方式引入。
    以MAWebKitCore的Framework为例:
#import <MAWebKitCore/MAWebViewController.h>
// ... ...
  • 源码编写规则
    源码中引入其他类的规范
    引入统一target内的类文件直接 #import "类名.h" 即可。
    引入依赖的target或者第三方库的,使用 #import <第三方库(Framework)名/全量的头文件名.h>
    源码的编写要符合sonar扫描的标准规则
    所有能处理的警告信息都要处理掉
    对外暴露的使用方法尽可能的简单
    尽量没有初始化操作
8. 打包Framework操作

编写打包Framework脚本(最后有参考),命名为make-framework(可以其他名称),可以直接将 make-framework下载下来打包使用。

  • 为了方便以后的打包,建议将 make-framework配置到系统的环境变量PATH中去,这样可以直接执行 make-framework xxxx 命令来进行打包操作。

  • make-framework脚本使用方法


    image.png
  • 由于打包脚本要求必须有workspace文件,所以如果项目中没有workspace文件需要创建一个。
    Xcode -> File -> New -> Workspace 新建workspace,保存目录xcodeproj放在同一目录下 即可。
    打开新建的workspace,右键添加文件到workspace,选择对应的xcodeproj即可。

  • 如果一个项目中有很多个Framework(类似我们举例的MAWebKit),如果每个Framework都手动 打包,这样会很麻烦,也很容易出错,这样我们可以写一个批量打包脚本。

# 批量打包脚本示例 
maia-framework -w MAWebKit -s MAWebKit -p 
maia-framework -w MAWebKit -s MAWebKitCore -p 
maia-framework -w MAWebKit -s MAWebKitPhoto -p 
maia-framework -w MAWebKit -s MAWebKitAudio -p 
maia-framework -w MAWebKit -s MAWebKitNFC -p 
maia-framework -w MAWebKit -s MAWebKitCodeScan -p 
maia-framework -w MAWebKit -s MAWebKitGPS -p 
maia-framework -w MAWebKit -s MAWebKitFace -o
9. 创建podspec文件

有时候为了方便定位问题,需要通过源码的方式进行调试,这就需要创建源码podspec。 我们可以通过命令行来创建cocoapods的配置podspec文件。

  • 打开终端Terminal,进入项目目录
  • 运行命令: pod spec create 库名(MAWebKit) ,这样会生成一个MAWebKit.podspec文件。
  • 配置源码podspec文件,根据自己组件的具体情况,指定默认subspec,比如:组件MAWebKit,默认依赖核心组件MAWebKitCore,那么就指定默认subspec为Core 条码扫描MACodeScan,默认不依赖任何subspec,因为它的两个subspec是互斥的
三、创建对外发布的Framework项目
  1. 创建脚本maia-project(最后有参考),只要运行命令,按 照步骤一步一步的操作下去,会自动创建Assets、Frameworks、库名.podspec文件,同时会进行git的 关联操作。
  • Assets:放置资源文件的位置
  • Frameworks:放置所有framework的位置
  • 库名.podspec:cocoapods的配置文件
脚本参考

mark-framework

#!/bin/sh

function usage() {
  echo "************************************************"
    echo "-s scheme的名称,必填"
  echo "-w workspace名称,可选项,默认从项目中检测xcworkspace文件"
    echo "-f 打包最终生成的framework的名字,可选参数,如果没有默认使用scheme的内容"
  echo "-o 打包成功后是否自动打开framework所在的文件夹,默认不打开"
  echo "-p 是否需要运行pod install,默认否"
  echo "-d 是否以Debug的配置进行打包,默认使用Release"
    echo "例: maia-framework -w MAWebKit -s MAWebKit -f MAWebKit -o -p -d"
    echo "************************************************"
  exit 1;
}

######################
# Options
######################

CONFIGURATION=Release
WORKSPACE_NAME=
SCHEME_NAME=
FRAMEWORK_NAME=
REVEAL_ARCHIVE_IN_FINDER=false
POD_INSTALL=false

######################
# Read Params
######################
while getopts "w:s:f:opd" arg
do
  case $arg in
    w )
      WORKSPACE_NAME=$OPTARG
      ;;
    s )
      SCHEME_NAME=$OPTARG
      ;;
    f )
      FRAMEWORK_NAME=$OPTARG
      ;;
    o )
      REVEAL_ARCHIVE_IN_FINDER=true
      ;;
    p )
      POD_INSTALL=true
      ;;
    d )
      CONFIGURATION=Debug
      ;;
    ? )
      usage
      ;;
  esac
done

if [[ ${SCHEME_NAME} == "" ]]; then
  echo "scheme必填, 使用-s来指定scheme。"
  usage
fi

if [[ ${FRAMEWORK_NAME} == "" ]]; then
  FRAMEWORK_NAME=${SCHEME_NAME}
fi

function printParams() {
  echo "****************************"
  echo "workspace: ${WORKSPACE_NAME}"
  echo "scheme: ${SCHEME_NAME}"
  echo "FRAMEWORK: ${FRAMEWORK_NAME}"
  echo "Pod install: ${POD_INSTALL}"
  echo "Reveal In Finder: ${REVEAL_ARCHIVE_IN_FINDER}"
  echo "****************************"
}
printParams

ma_default_workspace=""
function check_default_workspace() {
  echo "\n------ xcode workspace checking ..."
  workspace_files=$(ls | grep .xcworkspace$ | wc -l)

  if [ ${workspace_files} == 1 ] ; then

    ma_default_workspace="`ls | grep .xcworkspace$ | sed -e 's/.xcworkspace//g'`"
    echo "检测到workspace【 ${ma_default_workspace} 】"
  else
    echo "没有找到xcode的workspace或者项目下有多个workspace文件,请自行检查。"
    exit 1
  fi
}

check_default_workspace
if [[ ${WORKSPACE_NAME} == "" ]]; then
  WORKSPACE_NAME="${ma_default_workspace}"
fi

if [[ ${POD_INSTALL} = true ]]; then
  if [[ -f "Podfile" ]]; then
    echo "--- Removing Pods cache"
    rm -rf Podfile.lock
    rm -rf Pods

    pod install
  else
    echo "没有找到Podfile文件"
    exit 1
  fi
fi

######################
# Directories
######################
FRAMEWORK_TARGET_PATH="${HOME}/Documents/MaiaFrameworks/${FRAMEWORK_NAME}"
SIMULATOR_BUILD_DIR="${FRAMEWORK_TARGET_PATH}/${CONFIGURATION}-iphonesimulator"
DEVICE_BUILD_DIR="${FRAMEWORK_TARGET_PATH}/${CONFIGURATION}-iphoneos"
SIMULATOR_LIBRARY_PATH="${SIMULATOR_BUILD_DIR}/${FRAMEWORK_NAME}.framework"
DEVICE_LIBRARY_PATH="${DEVICE_BUILD_DIR}/${FRAMEWORK_NAME}.framework"
UNIVERSAL_LIBRARY_DIR="${FRAMEWORK_TARGET_PATH}/${CONFIGURATION}-iphoneuniversal"
UNIVERSAL_LIBRARY_PATH="${UNIVERSAL_LIBRARY_DIR}/${FRAMEWORK_NAME}.framework"

function printDirs() {
  echo "----------------------------"
  echo "BUILD_DIR : ${FRAMEWORK_TARGET_PATH}"
  echo "SIMULATOR_LIBRARY_PATH : ${SIMULATOR_LIBRARY_PATH}"
  echo "DEVICE_LIBRARY_PATH : ${DEVICE_LIBRARY_PATH}"
  echo "UNIVERSAL_LIBRARY_DIR : ${UNIVERSAL_LIBRARY_DIR}"
  echo "FRAMEWORK : ${UNIVERSAL_LIBRARY_PATH}"
  echo "----------------------------"
}
printDirs

######################
# Create directory for universal
######################
rm -rf "${FRAMEWORK_TARGET_PATH}"

mkdir -p "${SIMULATOR_BUILD_DIR}"
mkdir -p "${DEVICE_BUILD_DIR}"
mkdir -p "${UNIVERSAL_LIBRARY_DIR}"

mkdir "${UNIVERSAL_LIBRARY_PATH}"

######################
# Build Frameworks
######################
BUILD_SETTINGS="ONLY_ACTIVE_ARCH=NO CLANG_ENABLE_CODE_COVERAGE=NO MACH_O_TYPE=staticlib DEFINES_MODULE=NO"


######################
# Build Simulator Frameworks
######################

SIM_BUILD_SHELL_PREFIX="xcodebuild -workspace ${WORKSPACE_NAME}.xcworkspace -scheme ${SCHEME_NAME} -sdk iphonesimulator -configuration ${CONFIGURATION}"
SIM_BUILD_SHELL="${SIM_BUILD_SHELL_PREFIX} ${BUILD_SETTINGS} EXCLUDED_ARCHS=arm64 clean build"

echo "------ Build Simulator Framework ------"
echo "${SIM_BUILD_SHELL}"
${SIM_BUILD_SHELL} 2>&1

SIM_FRAMEWORK_TARGET_DIR=`${SIM_BUILD_SHELL_PREFIX} -showBuildSettings | grep "\<BUILT_PRODUCTS_DIR" | sed -e 's/BUILT_PRODUCTS_DIR = //g'`
echo "****** Simulator framework path:  ${SIM_FRAMEWORK_TARGET_DIR}"

SIM_COPY_SHELL="cp -r ${SIM_FRAMEWORK_TARGET_DIR}/${FRAMEWORK_NAME}.framework ${SIMULATOR_BUILD_DIR}"
echo "====== SIM COPY SHELL: ${SIM_COPY_SHELL}"
${SIM_COPY_SHELL}

######################
# Build Device Frameworks
######################
DEVICE_BUILD_SHELL_PREFIX="xcodebuild -workspace ${WORKSPACE_NAME}.xcworkspace -scheme ${SCHEME_NAME} -sdk iphoneos -configuration ${CONFIGURATION}"
DEVICE_BUILD_SHELL="${DEVICE_BUILD_SHELL_PREFIX} ${BUILD_SETTINGS} clean build"

echo "------ Build Device Framework ------"
echo "${DEVICE_BUILD_SHELL}"
${DEVICE_BUILD_SHELL} 2>&1

DEVICE_FRAMEWORK_TARGET_DIR=`${DEVICE_BUILD_SHELL_PREFIX} -showBuildSettings | grep "\<BUILT_PRODUCTS_DIR" | sed -e 's/BUILT_PRODUCTS_DIR = //g'`
echo "****** Device framework path:  ${DEVICE_FRAMEWORK_TARGET_DIR}"
DEVICE_COPY_SHELL="cp -r ${DEVICE_FRAMEWORK_TARGET_DIR}/${FRAMEWORK_NAME}.framework ${DEVICE_BUILD_DIR}"

echo "====== DEVICE COPY SHELL: ${DEVICE_COPY_SHELL}"
${DEVICE_COPY_SHELL}

######################
# Copy files Framework
######################
cp -r "${DEVICE_LIBRARY_PATH}/." "${UNIVERSAL_LIBRARY_PATH}"


######################
# Make an universal binary
######################
lipo "${SIMULATOR_LIBRARY_PATH}/${FRAMEWORK_NAME}" "${DEVICE_LIBRARY_PATH}/${FRAMEWORK_NAME}" -create -output "${UNIVERSAL_LIBRARY_PATH}/${FRAMEWORK_NAME}" | echo

# For Swift framework, Swiftmodule needs to be copied in the universal framework
if [ -d "${SIMULATOR_LIBRARY_PATH}/Modules/${FRAMEWORK_NAME}.swiftmodule/" ]; then
cp -f ${SIMULATOR_LIBRARY_PATH}/Modules/${FRAMEWORK_NAME}.swiftmodule/* "${UNIVERSAL_LIBRARY_PATH}/Modules/${FRAMEWORK_NAME}.swiftmodule/" | echo
fi

if [ -d "${DEVICE_LIBRARY_PATH}/Modules/${FRAMEWORK_NAME}.swiftmodule/" ]; then
cp -f ${DEVICE_LIBRARY_PATH}/Modules/${FRAMEWORK_NAME}.swiftmodule/* "${UNIVERSAL_LIBRARY_PATH}/Modules/${FRAMEWORK_NAME}.swiftmodule/" | echo
fi

cp -r "${UNIVERSAL_LIBRARY_PATH}" "${FRAMEWORK_TARGET_PATH}"

######################
# Move Final Bundles
######################
FRAMEWORK_FINAL_PATH="${FRAMEWORK_TARGET_PATH}/${FRAMEWORK_NAME}.framework"
FRAMEWORK_BUNDLES="${FRAMEWORK_FINAL_PATH}/*.bundle"

mv ${FRAMEWORK_BUNDLES} "${FRAMEWORK_TARGET_PATH}"

######################
# Remove Bundles
######################
SIMULATOR_LIBRARY_BUNDLES="${SIMULATOR_LIBRARY_PATH}/*.bundle"
DEVICE_LIBRARY_BUNDLES="${DEVICE_LIBRARY_PATH}/*.bundle"
UNIVERSAL_LIBRARY_BUNDLES="${UNIVERSAL_LIBRARY_PATH}/*.bundle"

echo "---------- **** Remove Bundles **** ----------"
echo "Simulator Bundles: ${SIMULATOR_LIBRARY_BUNDLES}"
echo "Device Bundles: ${DEVICE_LIBRARY_BUNDLES}"
echo "Universal Bundles: ${UNIVERSAL_LIBRARY_BUNDLES}"

rm -rf ${SIMULATOR_LIBRARY_BUNDLES}
rm -rf ${DEVICE_LIBRARY_BUNDLES}
rm -rf ${UNIVERSAL_LIBRARY_BUNDLES}

FWK_PRIVATE_HEADERS_PATH="${FRAMEWORK_FINAL_PATH}/PrivateHeaders"
rm -rf ${FWK_PRIVATE_HEADERS_PATH}

echo -e "Framework is in \033[31m ${FRAMEWORK_TARGET_PATH} \033[0m"

if [ ${REVEAL_ARCHIVE_IN_FINDER} = true ]; then
open "${FRAMEWORK_TARGET_PATH}/"
fi

make-project

#!/bin/bash

function usage() {
    echo
    echo "Usage:"
    echo
    echo -e "     \033[31m $ maia-project PROJECT-NAME \033[0m"
    echo
    echo "      maia-project, 输出标准的Maia-iOS私有库项目工程,PROJECT-NAME为必须参数"
    echo
}

if [[ $# -gt 0 ]]; then
    projectName="$1"
    if [[ -d $projectName ]]; then
        echo -e "项目 [ \033[31m ${projectName} \033[0m ] 已存在。"
    else
        echo -e "待创建的项目为:\033[31m ${projectName} \033[0m"
        echo "创建项目 ${projectName} ..."
        mkdir $projectName

        cd $projectName

        echo "创建Frameworks文件夹 ..."
        mkdir Frameworks

        echo "创建Assets文件夹 ..."
        mkdir Assets
        touch Assets/.gitkeep

        read -p "请输入组件库的版本号,直接回车默认为1.0.0 : " app_version
        if [[ $app_version == "" ]]; then
            app_version="1.0.0"
        fi

        read -p "请输入项目简介,直接回车默认为项目简介 : " project_info
        if [[ $project_info == "" ]]; then
            project_info="项目简介"
        fi

        read -p "请输入项目描述,直接回车默认为项目描述 : " project_desc
        if [[ $project_desc == "" ]]; then
            project_desc="项目描述"
        fi

        read -p "请输入最低支持的iOS系统版本,直接回车默认为8.0 : " miniVersion
        if [[ $miniVersion == "" ]]; then
            miniVersion="8.0"
        fi

        read -p "请输入作者姓名,直接回车默认为黄于 : " author
        if [[ $author == "" ]]; then
            author="黄于"
        fi

        read -p "请输入作者邮箱,直接回车默认为huangyu20@longfor.com : " author_email
        if [[ $author_email == "" ]]; then
            author_email="huangyi20@longfor.com"
        fi

        echo ""
        echo "============================================================"
        echo ""
        echo "版本号 : ${app_version}"
        echo "应用描述 : ${project_info}"
        echo "应用简介 : ${project_desc}"
        echo "最低支持的iOS系统版本 : ${miniVersion}"
        echo "作者 : ${author}"
        echo "作者邮箱 : ${author_email}"
        echo ""
        echo "============================================================"
        echo ""

        echo "生成Podspec文件 ..."
        podFile=$projectName.podspec

        echo "#" > $podFile
        echo "# Be sure to run \`pod lib lint ${podFile}\` to ensure this is a" >> $podFile
        echo "# valid spec before submitting." >> $podFile
        echo "#" >> $podFile
        echo "# Any lines starting with a # are optional, but their use is encouraged" >> $podFile
        echo "# To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html" >> $podFile
        echo "#" >> $podFile
        echo "" >> $podFile
        echo "Pod::Spec.new do |s|" >> $podFile
        echo "" >> $podFile
        echo "  s.name         = '${projectName}'" >> $podFile
        echo "  s.version      = '${app_version}'" >> $podFile
        echo "  s.summary      = '${project_info}'" >> $podFile
        echo "  s.description  = <<-DESC" >> $podFile
        echo "                   ${project_desc}" >> $podFile
        echo "                   DESC" >> $podFile
        echo "" >> $podFile
        echo "  s.homepage = 'http://git.xxxxx.net/ios/${projectName}'" >> $podFile
        echo "  s.license  = { :type => 'Copyright', :text => 'Copyright 2018 - 2020 longfor.com. All rights reserved.\n' }" >> $podFile
        echo "  s.authors  = { '${author}' => '${author_email}' }" >> $podFile
        echo "" >> $podFile
        echo "  s.ios.deployment_target = '${miniVersion}'" >> $podFile
        echo ""  >> $podFile
        echo "  s.source = { :git => 'http://git.xxxxx.net/ios/${projectName}.git', :tag => s.version.to_s }" >> $podFile
        echo ""  >> $podFile
        echo "  # s.resource  = 'Assets/icon.png'" >> $podFile
        echo "  # s.resources = 'Assets/Resources/*.png'" >> $podFile
        echo "" >> $podFile
        echo "  s.vendored_frameworks = 'Frameworks/*.framework'" >> $podFile
        echo "  # s.frameworks = 'CoreLocation', 'Foundation', 'UIKit', 'SystemConfiguration', 'AdSupport', 'Security', 'CoreTelephony'" >> $podFile
        echo "" >> $podFile
        echo "  # s.library   = 'sqlite3.0'" >> $podFile
        echo "  # s.libraries = 'sqlite3.0','c++'" >> $podFile
        echo "" >> $podFile
        echo "  s.requires_arc = true" >> $podFile
        echo "" >> $podFile
        echo "  # s.xcconfig = { 'HEADER_SEARCH_PATHS' => '\$(SDKROOT)/usr/include/libxml2' }" >> $podFile
        echo "  # s.dependency 'JSONKit', '~> 1.4'" >> $podFile
        echo "" >> $podFile
        echo "end" >> $podFile

        echo "绑定Git ..."
        git init
        # 关联远程仓库
        git remote add origin http://git.xxxxx.net/ios/$projectName.git
    fi
else
    usage
fi

推荐阅读更多精彩内容