一般步骤
-
在现有项目中,新建 target,选择: Cocoa Touch Framework。mach-o type 设置为 Static Library(否则苹果商店审核不过)。
-
编辑 Podfile 文件为:
source 'https://github.com/CocoaPods/Specs.git' platform :ios, "9.0" targets = ['ParkButler-Owner','MCCSframework'] targets.each do |t| target t do pod 'JSONModel' pod 'AFNetworking' ... end end
这里的 targets 数组中,将所有 target 的名字加入,包括原项目的 target (也就是这里的ParkButler-Owner)和新建的 framework(也就是这里的MCCSframework)。然后用一个循环,向两个 target 中都安装同样的 pod。如果你不想在 framework 中安装所有 pod,则需要单独为 framework 导入框架:
target 'MCCSframework' do pod 'IGListKit', '~> 2.0.0' pod 'UICollectionViewLeftAlignedLayout' end
-
然后关闭 Xcode,删除原项目所用的 .xcworkspace,用 pod intall 命令重新生成新的 .xcworkspace 文件,然后打开这个 .xcworkspace 文件。
注:这一步很重要,否则 pod 不会安装到新的 framework 的 target 中。要验证 framework 中已经正确安装 pod,可以查看 framework 的 target 的 Build Phases 中是否已包含 [CP] Check Pods Manifest.lock 和 [CP] Copy Pods Resources 项。
-
在项目导航器中,将 .h/.m 文件从原项目拖到 framework 文件夹中,注意需要将 .h/.m 文件的 Target Membership 都修改为 framework 的 target。这样原项目对 .h/.m 的依赖就转移到了 framework 上。
如果要公开这个类给外部使用,可以在 Target Membership 中将 .h 文件设置为 Public,默认是 Project。建议,将所有 .h 文件设置为 Public。
同时为了使框架便于使用,可以在 <框架名>.h 头文件中将所有 .h 文件 import,形式如:#import "MCCSframework/IGListBaseVC.h" #import "MCCSframework/UIView+loadFromNib.h" #import "MCCSframework/NibView.h"
-
在 app 项目中全局搜索
import "刚才移动的头文件.h"
(快捷键 command+option+shift+F),替换为import <框架名称/刚才移动的头文件.h>
。注意,如果是 framework 自身的类 import 这个头文件就无需替换。认真来说,这一步不是必须的,也可以在集成 pod 时再做。但是也可以提前到这里做,要稍微省事一些。
-
build framework,看是否报错。同时,build 原项目,看是否报错。这是很有可能的,当你移动源文件后,framework 没有报错,但原项目却编译不了了。所以最好在每次移动文件后,都运行一下 app,看是否有什么问题。
编译 Universal Framework
-
选择框架的 scheme,选择模拟器,command+B,打出模拟器的 release 包,注意,打出的包位于 Release-iphonesimulator 目录。
记住, Scheme 的 Build Configuration 要改成 release。
-
选择真机,command+B,打出真机的 release 包。注意打出的包位于 Release-iphoneos 目录。
-
在 Products 目录下创建一个 Release-universal 目录。
-
拷贝 lipo-create.sh 到 Products 目录下,编辑脚本内容为:
lipo -create "./Release-iphonesimulator/MCCSframework.framework/MCCSframework" "./Release-iphoneos/MCCSframework.framework/MCCSframework" -output "./Release-universal/MCCSframework"
将脚本中的 MCCSframework 替换成你自己的框架名。
-
执行 lipo-create.sh,将两个包合并成 universal 的包(放在 Release-universal 目录下)。注意,如果文件无法执行,可能需要用 chmod 777 lipo-create.sh 命令。
使用 Aggregate 创建 Universal 包
如果不想自己在终端执行脚本,可以创建 Aggregate Target:
- 添加 Target,选择 Cross-platform -> other 下的 Aggreagate。
- 打开 target 的 Build Phases,New Run Script Phase,粘贴以下脚本:
#!/bin/sh
#要build的target名(若一个工程有多个target,最好手动指定需要打包的目标,如TARGET_NAME="framework名")
TARGET_NAME="MCCSframework"
if [[ $1 ]]
then
TARGET_NAME=$1
fi
UNIVERSAL_OUTPUT_FOLDER="${SRCROOT}/Products/"
#创建输出目录,并删除之前的framework文件
mkdir -p "${UNIVERSAL_OUTPUT_FOLDER}"
rm -rf "${UNIVERSAL_OUTPUT_FOLDER}/${TARGET_NAME}.framework"
#分别编译模拟器和真机的Framework
xcodebuild -target "${TARGET_NAME}" ONLY_ACTIVE_ARCH=NO -configuration ${CONFIGURATION} -sdk iphoneos BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" clean build
xcodebuild -target "${TARGET_NAME}" ONLY_ACTIVE_ARCH=NO -configuration ${CONFIGURATION} -sdk iphonesimulator BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" clean build
#拷贝framework到univer目录
cp -R "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework" "${UNIVERSAL_OUTPUT_FOLDER}"
#合并framework,输出最终的framework到build目录
lipo -create -output "${UNIVERSAL_OUTPUT_FOLDER}/${TARGET_NAME}.framework/${TARGET_NAME}" "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework/${TARGET_NAME}" "${BUILD_DIR}/${CONFIGURATION}-iphoneos/${TARGET_NAME}.framework/${TARGET_NAME}"
#删除编译之后生成的无关的配置文件
dir_path="${UNIVERSAL_OUTPUT_FOLDER}/${TARGET_NAME}.framework/"
for file in ls $dir_path
do
if [[ ${file} =~ ".xcconfig" ]]
then
rm -f "${dir_path}/${file}"
fi
done
#判断build文件夹是否存在,存在则删除
if [ -d "${SRCROOT}/build" ]
then
rm -rf "${SRCROOT}/build"
fi
rm -rf "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator" "${BUILD_DIR}/${CONFIGURATION}-iphoneos"
#打开合并后的文件夹
open "${UNIVERSAL_OUTPUT_FOLDER}"
然后 Build。这个是最新脚本,亲测在 Xcode 10.3 下可用。
Bundle 不区分 iphonesimulator 和 iphoneos,不需要创建二合一版本。直接拷贝 Products 下的 .bundle 文件即可。
创建 pod 工程
-
在 git 上新建代码仓库后,用 git clone 命令 check out 到本地。
-
将打包出来的 .framework(任意一个 .framework,模拟器或者真机的包都可)拷贝到项目目录,然后将 .framework 中的静态库文件(即 .a 文件,虽然这个文件并没有后缀名)替换成 universal 版本。
-
如果框架中的资源以 bundle 形式封装,记得也拷贝 .bundle 文件到 pod 项目中来。
-
add、commit、push。
-
在 pod 项目目录下运行:
pod spec create
-
打开 xxxframework.podspec,修改内容。注意:
- s.version 必须和 tag 保持一致。
- 这个库除了 .framework 文件之外,没有任何源文件,不用指定 s. source_files。但是需要指定 s.verdored_frameworks,这个属性用于指定 pod 库中包含的所有非系统的 framework,我们用 xcode 编译出来的那个 framework 当然也是其中之一。
- 如果使用了第三方 pod,需要添加 s.dependency(可以有多条)。
-
打标签:
git tag ‘<你的 tag 版本>’ -m ‘<标签信息>’
git push --tags
删除一个 tag 使用
git tag -d '<tag 名>'
命令。
上传至 pod 索引库
-
切换到 pod 项目所在目录(即
.podspec
文件所在目录),比如/Users/qq/YHYRepos/MCCSframework
-
本地验证 pod lib lint --no-clean,远程验证 pod spec lint,如果出现 passed validation. 表示校验通过。
-
pod repo push <
pod 索引库
名称> <podspec文件名>,比如:pod repo push YHYSpecs MCCSframework.podspec
这里以上传到远程私有库为例。如果你想上传至 cocospods 官方索引库,请用 pod trunk push 命令替代。
在项目中使用 framework
-
打开 app 项目,删除 framework 的 target 和源文件夹。
-
将 Scheme 中 framework 所对应的 target 删除。
-
编辑 Podfile ,将 framework 所对应的的 target 删除。同时将 Pods 目录下多余的两个 .xcconfig 文件删除。
-
pod repo 命令查看<远程私有
索引库
url>,比如https://gitee.com/kmyhy/YHYSpecs.git
,复制此 url。 -
修改 Podfile 文件,添加私有库:
source '<远程私有索引库 Url>' source 'https://github.com/CocoaPods/Specs.git' 其中,第二个 source 是官方索引库的 url,这是必须的,否则所有官方 pod 都安装不了。
-
修改 PodFile,在其中 pod ‘远程私有库名称’,比如:
pod 'MCCSframework'
-
pod install
-
command + b,编译 app。会报一堆错误,但大部分是头文件找不到错误,需要修改引用 .h 文件的方式。因为有一部分头文件从 APP 中挪到 framework 中了。比如原来引用
#import "Utils.h"
的地方,全部都需要修改为#import <MCCSframework/NibView.h>
。可以用 command+shift+option+F 进行全局查找替换。
-
build & run,分别在真机和模拟器上测试,查看 app 功能是否正常。
这里还是以远程私有库为例。
加载 nib
如果 framework 中包含了 nib,那么在加载这些 nib 时,需要注意几件事情:
-
app 打包后,如果需要加载 framework 中的资源,必须将 framework 也打包到 app 中(也可以选择打包成单独的 Bundle)。因此,需要在 app 的 Build Phases -> Copy Bundle Resouces 中,将 .framework 文件加入,否则 nib 不可用。
-
由于 nib 是在 xxx.framework 目录下,而不是在 xxx.app 目录下,所以在加载 nib 时需要指定 bundle,同时在 bundle 中指定 .framework 的路径:
NSBundle *bundle = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"MCCSframework" ofType:@"framework"]]; UINib* nib = [UINib nibWithNibName:@"SimpleDatePicker" bundle:bundle];
这样,才能正确加载 nib。
-
这样在分发框架时,注意将 .a 文件和 .nib 文件一起拷贝。
使用 bundle
虽然也可以直接在 framework 中包含 nib 和 png 等 UI 资源,但更好的办法是将它们单独打包到一个 bundle 进行分发。这样如果调用者仅仅是换图片或换 nib 文件只需要修改这个 bundle 即可。而且也不必因为框架中使用了 framework 中的 nib 文件或图片,将整个 .framework 文件打包进 app 中(即添加进 Copy Bundle Resources)。
-
创建一个 Target,选择 -> macOS -> Framework & Library -> Bundle,命名为 xxxBundle
这种方式的 Bundle 在打包时可能会签名失败,此时需要手动签名,将签名证书从 Mac Developer 修改为 iOS Developer。
-
在 target 的 Build Settings 中,将 BaseSDK、Support Platforms 改为 iOS,iOS Deployment Target 修改为 10。“COMBINE_HIDPI_IMAGES” 设置为 “NO”。
-
将 .xib 文件拖到这个 xxxBundle 目录里面,command+B,生成一个 xxxBundle.bundle 文件(这个文件中将是编译好的 .nib 文件),将这个文件拖到 app 的 Copy Bundle Resources 中。
-
在代码中,这样加载 bundle 中的 .nib 文件:
NSBundle *bundle = getBundl(@"MCCS");
其中 getBundle 是一个实用函数:
// 获取指定 bundle NSBundle* getBundle(NSString* bundleName){ return [NSBundle bundleWithPath: [[NSBundle mainBundle] pathForResource:bundleName ofType: @"bundle"]]; }
-
在代码中,这样加载 bundle 中的图片:
// 指定图片文件名、bundle 文件名 self.imageView.image = getBundleImage(@"cart@3x", @"MCCS");
注意,其中图片文件名中的 @3x 不能省略。
其中 getBundleImage 是一个实用函数:
// 获取指定 bundle 下的图片 UIImage* getBundleImage(NSString *name,NSString* bundleName){ return [UIImage imageWithContentsOfFile:[getBundle(bundleName) pathForResource:name ofType:@"png"]]; }
注意,如果在框架中使用了 bundle,则上传 Pod 库时,记得在 podspec 文件中添加 s.resouces 属性。
常见错误
-
include of non-modular header inside XXX.h
通常在编译 framework 时并不会报此错误,而是在使用该框架的 application 中会报此错误。此错误表明,在 framework 的 XXX.h 头文件中暴露了一个非此模块的头文件,这个头文件是 umbrella 的,你不应该在自己框架的头文件中暴露它,而是要把它隐藏起来。
怎么隐藏呢,其实就是把那个头文件的 import 语句移到 .m 文件中即可。如果在 XXX.h 中需要使用此头文件的定义,可以用 @class 语句(向前声明),比如:// 在 .h 中 @class IGListSectionController; @interface IGListVC : IGListSectionController // 在 .m 中 #import <IGListKit.h>
还有一种情况,就是你正在继承或者扩展第三方框架中类(Category),此时你只能在扩展的 .h 文件中 import 这个类,因为扩展一个类时必须明确类的定义,此时向前声明是无效的。
解决办法就是将 Allow Non-module Include In Framework Modules 设置为 Yes(不管 framework 还是 app 都需要设置)。 -
不能加载框架中的图片资源
将图片资源拷贝到框架文件夹的 .xcassets 中。
-
could not load nib in bundle
要加载位于框架中的 nib 文件,需要在初始化 NSBundle 时指定框架的 Bundle Id:
NSString* const frameworkBundleID = @"com.xxx.MCCSframework"; NSBundle* bundle = [NSBundle bundleWithIdentifier:frameworkBundleID]; NSString* nibName = NSStringFromClass([self class]); UINib* nib = [UINib nibWithNibName:nibName bundle:bundle];
-
unrecognized selector sent to class
如果框架中包含了 Category,那么当 app 中调用到 Category 中的方法时就会出现此错误。这是因为链接器在处理包含Category(类别)方法"静态库" 时,没有将Category的方法链接到 APP 中。具体可参考这里。
解决办法是,将 framework 的 Perform Single-Object Prelink 设置为 Yes。将 framework 中所有对象文件合并成单一文件,这样所有的代码都会被链接到 app 中。
-
Undefined symbols for architecture x86_64 ‘xxx’
将 xxx 类删除重建,将原来的代码拷回重新编译。
-
pod lib lint 时出错:Could not find a
ios
simulator更新 rubygem,更新 cocoapods 后再执行:
pod lib lint --verbose --use-libraries --allow-warnings
-
更新 rubygem 出错:You don’t have write permissions for the /Library/Ruby/Gems/2.3.0 directory.
rubygem 更新至 2.7.7 时出错(Mac mojava)。解决办法:
sudo gem update -n /usr/local/bin —system
-
更新 cocoapods 出错: ERROR: While executing gem … (Gem::FilePermissionError) You don’t have write permissions for the /usr/bin directory.
cocoapods 更新至 1.7.5 时出错。解决办法:
sudo gem install cocoapods -n /usr/local/bin
-
更新 cocoapod 出错:Unable to download data from https://gems.ruby-china.org/
替换https://gems.ruby-china.org/为https://gems.ruby-china.com/:
gem sources --add https://gems.ruby-china.com/ --remove https://gems.ruby-china.org/
确保只有 gems.ruby-china.com