让自己的代码支持Cocoapods导入

本文讲述如何让自己的代码、.a、.framework支持cocoapods导入,变成类似于AFNetworking、SDWebImage那样的第三方库。代码可以托管在SVN和Git服务器上,会在不同之处做出说明。是公有库还是私有库取决于你的代码托管平台设置的公开性。

初级阶段

所谓“初级阶段”,是说使用cocoapods提供的模板创建项目,然后将自己的代码添加进去,做一些修改之后发布出去。这种方式相对简单,结构固定,但是灵活性不高。

下面进入正题~

1. 创建空项目

首先在SVN或者Git服务器上创建一个空项目,然后check out或者clone到本地。

2. 创建模板工程

打开终端,cd到工程所在目录,输入命令:pod lib create 工程名。Cocoapods会在终端询问几个问题,下面结合图片解释:

create lib.jpg

5个问题回答完之后,将会自动打开项目。你需要把自己的代码放在Pods/Development Pods/目录下。

3. 修改配置

在Xcode的Demo项目中的Podspec Metadata文件夹下,或者Finder中该项目的根目录下找到一个后缀为podspec的跟项目名同名的文件,这个是让自己的代码支持Cocoapods导入的核心配置文件,下面对文件中各配置项的含义进行逐条说明:

Pod::Spec.new do |s|
  # 库名
  s.name             = 'TestPoood'
  # 版本号
  s.version          = '0.1.0'
  # 对库的类型、作用做一个简短的描述
  s.summary          = 'A short description of TestPoood.'

  # 如果需要对库进行较多的说明,可在此处进行描述
  s.description      = <<-DESC
TODO: Add long description of the pod here.
                       DESC

  # 库工程目录地址,这里会根据你在SVN或Git服务器上创建项目的地址自动生成
  s.homepage         = 'https://github.com/RavenKite/TestPoood'
  # 遵守的哪些许可协议,默认就行了
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  # 作者信息
  s.author           = { '作者名' => '邮箱' }

  # 库的源路径。SVN与Git稍有不同,下面分为两行举例。后面的`:tag => s.version.to_s`是库的版本号,
  # `pod install`时会自动根据后面的tag版本号去工程目录中寻找与版本号同名的源。
  s.source           = { :svn => 'http://192.168.101.1/svn/iOS/TestPoood/', :tag => s.version.to_s  }
  s.source           = { :git => 'https://github.com/RavenKite/TestPoood.git', :tag => s.version.to_s }

  # 最低兼容的iOS版本号
  s.ios.deployment_target = '8.0'
  # 库中源码的路径,默认包含Classes下所有文件夹及所有的.h和.m文件
  # 注意:如果包含xib或storyboard,放在此目录下是无效的,必须将其移动到Assets目录下作为资源文件存在
  s.source_files = 'Classes/**/*.{h,m}'

  # 资源文件目录,可以在此目录下存放图片、xib等资源,可以使用通配符或者{png,jpg,xib}这样的方式来指定文件类型
  s.resource_bundles = {
    'TestPoood' => ['TestPoood/Assets/*.*']
  }

  s.public_header_files = 'Pod/Classes/**/*.h'
  # 依赖的系统库(framework)。多个可以使用英文逗号隔开
  s.frameworks = 'UIKit', 'MapKit'
  # 依赖的系统dylib库,依赖这种库时不需要写前面的"lib",如依赖"libz.tbd",只需要写上'z'就行了
  s.ios.library = 'z','c++'

  # 依赖的第三方库。如果依赖多个,不能使用依赖系统库的方式用英文逗号隔开,必须要另起一行,后面也可以指定版本号
  s.dependency 'AFNetworking', '~> 2.3'
  s.dependency 'Masonry'

end
4. 代码位置

配置文件修改好之后,将你的代码放到Pods/Development Pods/TestPoood下,然后在终端执行pod install后才能在Demo项目中调用。

5. 校验

在本地的工作(配置、代码)完成之后,提交SVN或Git服务器之前,最好先让Cocoapods做一下校验,校验通过后再提交,以确保你的库能够被其他项目正常导入。
在终端执行以下命令:

pod lib lint TestPoood.podspec

如果终端打印了TFFramework passed validation. 则说明校验通过,否则校验失败。
如果校验失败,可以查看终端输出的日志。
- NOTE是正常项;
- ERROR是错误项,需要根据后面的提示做出修改,否则无法通过校验。错误情况可能五花八门,在此也无法穷举,这里就考验大家解决问题的能力啦(~ ̄  ̄)~;
- WARN是警告项,建议根据提示做出修改,也可以通过在校验命令后拼接--allow-warnings忽略警告以完成校验,代码如下:

pod lib lint TestPoood.podspec --allow-warnings
6. 提交代码服务器

在以上功能都实现完成后,提交到SVN或Git服务器,打上tag(tag名为版本号,如1.0.0,不要加其他中英文字符),GitHub可以Draft a new release。别的项目接入时跟引用其他第三方库稍有不同,下面是引用示例。

在podfile文件中增加:

# SVN
pod 'TestPoood', :svn => 'http://192.168.101.1/svn/iOS/TestPoood/',:tag=>'0.1.0'
# Git
pod 'TestPoood', :git => 'https://github.com/RavenKite/TestPoood.git',:tag=>'0.1.0'

与引用常见的如AFNetworking不同的是(也可以跟引入AFN时一样,需要将.podspec文件上传到Cocoapods的服务器,后面的高级阶段会讲到),引用自己的库需要在后面增加源地址,源地址就是.podspec文件s.source中的路径,再之后的tag可根据需要使用指定的版本。

但是,开发状态时,由于可能会频繁的修改库中的文件,而在别的项目中接入该库时必须要指定一个版本号,这样频繁的修改文件后提交服务器然后打tag的方式过于繁琐且没必要,我们可以通过下面的方式,不指定版本号,而是直接引入库的开发环境时的代码,等到稳定后再使用上面的方式引入。

# SVN直接引用trunk目录下的源文件
pod 'TestPoood', :svn => 'http://192.168.101.1/svn/iOS/TestPoood/trunk'
# Git不指定后面的版本号,即会自动引用根目录中的代码,而不是tag中的
pod 'TestPoood', :git => 'https://github.com/RavenKite/TestPoood.git'
6. 修改和更新

如果版本号、库的公开类、私有类、目录结构、资源文件等变更时,需修改.podspec中的配置然后提交SVN或Git,之后在引用该库的工程中 pod update
注意,podspec中的版本号要与Pods->TestPood中的版本号一致,且每次提交SVN或Git也应该改变版本号(当然,开发过程中可不必如此)。

7. 完成

至此,你已经按照模板成功的制作出了支持Cocoapods导入的开源库。
但是,如果你的库中的代码较多,或者你喜欢把不同的类按照其功能性等方式分散在不同文件夹中的话,你会发现,制作出来的库是没有文件夹、完全扁平化的。然而,你会发现AFNetworking有文件夹的。如果你去它的GitHub地址查看AFNetworking.podspec文件的话,你会发现里面有s.subspec这个配置项,它的含义是创建一个子库,具体使用方式会在下面的高级阶段介绍,因为它对库的结构及代码耦合性有很高的要求。

高级阶段

如果你按照“初级阶段”介绍的方法成功的制作出了一个简单的库的话,那么你肯定会发现这其中最重要的就是.podspec文件,尤其是它内部的配置项,这才是决定了我们的代码能否支持Cocoapods导入的关键。

并且你可能会发现,“初级阶段”所讲的方式实际上是有很明显的缺点的:它是按照模板来的,结构过于固定而没有灵活性,需要自己把代码一点点的迁移到这个模板项目中来。而且这个项目结构……我个人感觉有点奇怪。另外就是,我没有提到怎么将.a或.framework添加进模板项目,因为我觉得采用这种模板的方式不适合制作闭源库。

那么,在所谓的“高级阶段”,就是要介绍怎样灵活的创建和配置.podspec文件,怎样让它与项目结合起来,甚至可以在不影响现有项目的基础上,将项目中任何一部分代码制作成开源库(闭源库还是要对项目结构做一些修改的),并支持Cocoapods导入。

好了,废话说完了,现在开始!

这次,我们不再按照模板创建项目,而是先手动创建好项目之后(如果你打算使用自己现有的项目,则可以跳过第一步),再用终端命令创建.podspec文件。
我先把下面所讲内容用到的Demo地址贴出来,希望能给大家提供一些参考。

1. 首先,创建好项目,项目结构如下图所示。

其中,TFClasses是库的类文件存放目录,TFAssets是库的资源文件存放目录。
对下图中包含.xcworkspace的项目结构有疑问的童鞋,我安利下自己的另一篇文章。(~ ̄▽ ̄)~

TestFramework项目目录结构

2. 创建podspec

打开终端,cd到TestFramework项目的根目录,即TestFramework.xcworkspace所在的目录(使用自己项目的童鞋根据自己项目情况cd到根目录);终端输入以下命令pod spec create 库名,回车。
创建成功后,打开TFFramework.podspec,你会发现里面有一大堆东西,不要担心,其中大多数为注释描述,关键的配置都与在“初级阶段”时所讲一致,只有少数地方需要我们略作修改。
创建podspec之后的项目目录结构是这样的:

创建podspec后的项目目录结构

其实,你也可以把podspec文件直接放在与TFClasses平级的目录下,但是下面几点中所说的相对路径就要根据实际情况做出调整了。

3. 修改库中源码路径:s.source_files

由于podspec文件是在项目根目录的,而存放库的类文件的目录TFClasses与它不在同一级。因此,需要修改为下面这样:

  # TFClasses后的通配符“**”意味包含该目录下所有的目录层级,“*.{h,m}”意味着包含目录中的所有后缀为.h或.m的文件
  s.source_files  = "TestFramework/TestFramework/TFClasses/**/*.{h,m}"
  # 如果库中的类只有一层目录,可以不要中间那一层的通配符,改成下面的方式
  s.source_files  = "TestFramework/TestFramework/TFClasses/*.{h,m}"
  # 或者直接全部使用通配符
  s.source_files  = "TestFramework/TestFramework/TFClasses/*.*"

需要注意的是,库中的所有类应该与项目中的其他类是完全解耦的,即库中的代码不依赖项目中的其他任何代码也能够正常运行,否则制作出的库在导入到别的项目中后肯定无法正常运行。

4. 修改库中引用的资源路径

s.resources.resourcess.resource_bundle = { }s.resource_bundles = { }
如果库中引用了资源文件,或者使用了Xib、Storyboard(不能直接放在TFClasses里面,即使使用了通配符"*.*",因为它们不是类文件,而是属于资源文件),需要为这些资源文件单独建立目录(如果不单独建立目录,也是可以放在TFClasses中的,只是需要指定文件后缀名,过于麻烦且结构混乱)。

  # 引用单个资源
  s.resource  = 'img.png'
  # 引用多个资源
  s.resources = ['TestFramework/TestFramework/TFAssets/*.*', 'TestFramework/TestFramework/TFText/*.*']

注意:上述方式是Cocoapods不建议的,但仍可以使用(这是因为有可能与导入该库的项目产生资源文件名冲突,因为资源文件都是平铺在mainBundle中的)。
Cocoapods推荐使用下面的方式:

  s.resource_bundle = {
    'TFAssets' => ['TestFramework/TestFramework/TFAssets.bundle/**/*.*']
  }
  # 导入多个资源目录或文件
  s.resource_bundles = {
    'TFAssets' => ['TestFramework/TestFramework/TFAssets.bundle/**/*.*']
  }

这种方式Cocoapods会在导入该库的项目中的mainBundle里创建一个bundle,名称就是“=>”左边的字符串。所以,如果你是直接将自己项目中的一部分代码制作成库的话,最好是将所有引用的资源放在一个bundle中,bundle名就是上面配置中的名称(所以,这时需要将上面所创建的TFAssets修改为TFAssets.bundle)。这样就能保证你在开发该库的项目中和导入该库的项目中调用资源文件的代码是一致的,否则将可能会出现找不到文件的情况。
如果你觉得这样比较麻烦,也可以直接使用第一种方法,但是要注意可能产生的命名冲突。

5. 创建子库:s.subspec

这是为了实现类似AFNetworking那样导入后在Pods里可以分成几个文件夹,就像下面这样:

AFNetworking目录结构

如果你仔细研究过AFNetworking的话,你会发现,它的每个文件夹(其实只有Serialization、Security、Reachability这三项)都是可以单独导入项目的,你可以在podfile里这么写:

    pod 'AFNetworking/Serialization'
    pod 'AFNetworking/Security'

为什么NSURLSession和UIKit不能单独导入项目?其实并不是这样,它们也是可以的,但是导入这两个的时候,也会同时导入另外三项,因为它的podspec是这样的:

s.subspec 'NSURLSession' do |ss|
    # 这里可以看出,NSURLSession是需要依赖Serialization、Reachability和Security的
    # 因此,导入NSURLSession时,也会同时导入另外三项
    ss.dependency 'AFNetworking/Serialization'
    ss.ios.dependency 'AFNetworking/Reachability'
    ss.osx.dependency 'AFNetworking/Reachability'
    ss.tvos.dependency 'AFNetworking/Reachability'
    ss.dependency 'AFNetworking/Security'

    ss.source_files = 'AFNetworking/AF{URL,HTTP}SessionManager.{h,m}', 'AFNetworking/AFCompatibilityMacros.h'
    ss.public_header_files = 'AFNetworking/AF{URL,HTTP}SessionManager.h', 'AFNetworking/AFCompatibilityMacros.h'
  end

  s.subspec 'UIKit' do |ss|
    ss.ios.deployment_target = '7.0'
    ss.tvos.deployment_target = '9.0'

    # 这里可以看出UIKit依赖NSURLSession,而NSURLSession又依赖另外三项
    ss.dependency 'AFNetworking/NSURLSession'

    ss.public_header_files = 'UIKit+AFNetworking/*.h'
    ss.source_files = 'UIKit+AFNetworking'
  end

因此,可以看出,这个所谓的子库(s.subspec)其实是“库中库”。如果你希望自己的库可以像AFNetworking那样有多层级的文件夹,必须要清晰的规划好自己库中的类,使得它们之间有一部分类是完全与其他类解耦的,有一部分类可能需要依赖其他子库中的类。但是,如果你规划的这些文件夹之间存在相互依赖的话……我相信,你最终肯定会放弃这么做的。

但是,我可不是为了劝退大家才这么说的,只是子库的使用确实有一定的前提,如果你的库不满足这样的前提,或者你嫌麻烦不打算使用子库的话,那么你可以直接跳过这一段了。

下面,开始正式介绍子库的使用方式。至于怎样解耦自己的库和规划文件夹,这里就不介绍了,大家各显神通吧╮( ̄  ̄)╭。

首先,要从podspec文件的第一行代码开始说起,为什么所有的配置项都是以“s”开头呢?能不能以别的什么字符开头?答案是当然可以,它就藏在第一行代码中:

# |s|就相当于给这个podspec空间创建了一个宏,在这个“命名空间”内,“s”就代表当前这个库。如果你想自定义一个字符,修改这里就行了
Pod::Spec.new do |s|

然后,再来看AFNetworking里关于子库的配置:

# 1. 子库内部的配置项与外面完全一致,你不需要再指定version、summary、author等固定信息,但至少需要指定source_files,其他配置项根据子库的实际情况确定
# 2. 这里的|ss|就相当于给Security子库创建了一个宏,它的作用域仅是这个子库。所以你会发现,AFNetworking所有子库的宏都是“ss”
# 3. Security是导入该库的项目中显示的子库的名称,它可以与你在开发该库的项目中的文件夹名称不同,只要你在子库中的source_files指定正确的路径即可。但为了避免麻烦,我想你一般都会选择两者一致的
s.subspec 'Security' do |ss|
    ss.source_files = 'AFNetworking/AFSecurityPolicy.{h,m}'
    ss.public_header_files = 'AFNetworking/AFSecurityPolicy.h'
    ss.frameworks = 'Security'
end

总之,子库的关键在于规划文件夹及各文件夹之间的解耦;其内部的配置与外面是完全一致的。

到了这里,应该能够很轻松的制作出一个开源库了。但是还想提醒一点,就是在开发库的过程中,尤其是对podspec做了修改,不要忘记多使用pod lib lint命令校验你的配置是否正确。

创建闭源库

上面一直在说开源库,接下来该介绍下闭源库了。所谓闭源库,就是指.a、.framework这样不公开源码的库。
既然是要闭源,那么开发和发布肯定不能在一个地址中进行。因此,你的podspec所在的目录应该只包含输出后的.framework或.a+headers,以及可能会引用的资源bundle。
至于怎么创建.framework或.a,网上都有比较多且详细的文章,这里就不赘述了。

那么我们直接开始~

一个制作好的闭源库,以.framework为例,应该类似于下面这样:


闭源库目录结构

关于闭源库的podspec,其中的配置项与上面所讲几乎完全一致,只是闭源库没有了s. source_files,需要修改为s. vendored_frameworks

下面是配置代码:

# 如果有多个.framework,可以使用英文逗号隔开
s.vendored_frameworks = 'TFFramework.framework', 'Other.framework'

# .a库是下面这个配置,多个也可以使用英文逗号隔开
s.vendored_libraries = 'libTFLibrary.a', 'libOther.a'

上传podspec到Cocoapods

上面提到过,如果你希望自己的库可以这样导入:

  pod 'TFFramework', '~>0.1.0'

而不是一定要指定地址:

  pod 'TFFramework' :git => 'https://github.com/RavenKite/TestFramework.git', :tag=>'0.1.0'

就必须要将该库的podspec上传到Cocoapods的服务器,这样你就可以在终端通过pod search搜索到它,并且可以用上面那种简化的方式导入。
但是,如果你的目的是制作私有库的话,还是不要将podspec上传到Cocoapods了。

1. 注册Cocoapods

在终端输入以下命令:

# 如果你之前注册过Cocoapods,但是会话过期了,也需要执行以下命令,但是可以不输入用户名,除非你需要修改
pod trunk register 邮箱 '用户名'

之后你会收到一封验证邮件,点击邮件中的链接进行验证。
验证通过后,执行以下命令,查看自己的信息:

pod trunk me

如果注册成功且会话有效,则会打印出你的个人信息。

2. 校验podspec的有效性

这个命令上面已经提到过了,只有校验通过(没有ERROR)的podspec才能上传Cocoapods。其中的警告是可以在命令后拼接--allow-warnings忽略的,但是建议还是根据提示做出修改,以达到没有警告的程度再进行上传。

pod lib lint TFFramework.podspec
# 忽略警告
pod lib lint TFFramework.podspec --allow-warnings
3. 上传podspec

需要注意的是,在上传之前,一定要在自己库的SVN或Git仓库创建tag,且tag版本与podspec中一致,否则将会上传失败。在GitHub上就是Draft a new release
经过第二步的校验通过且创建tag后,再执行上传命令:

pod trunk push TFFramework.podspec

# 如果你选择了不处理第二步中的警告就直接上传,需要在后面拼接'--allow-warnings'
pod trunk push TFFramework.podspec --allow-warnings

至于上传速度方面,以我的经验,一般都会很慢,所以要有耐心~
上传成功后,控制台的结果如下:


podspec上传成功
4. 搜索自己的库

检测是否上传成功的标识,就是能通过pod search搜索到自己的库:

pod search TFFramework
搜索TFFramework结果
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容

  • 项目组件化、平台化是技术公司的共同目标,越来越多的技术公司推崇使用pod管理第三方库以及私有组件,一方面使项目架构...
    swu_luo阅读 20,533评论 0 39
  • 一、创建的github仓库原文地址 1 进入Github网站www.github.com登陆自己的账号后 2 建立...
    freesan44阅读 3,869评论 2 12
  • 孩子四月中旬春假两周。本来约好和小伙伴一起旅行,奈何突发状况小伙伴临时爽约,只好带着小男生在帝都来个本地自由行。 ...
    良仔lz阅读 183评论 0 0
  • 当两个Web组件之间为链接关系时,被链接的组件通过getParameter()方法来获得请求参数.request....
    海纳百川_4d26阅读 129评论 0 0
  • 有人说,人生在世,孤独感是如影随形的。这话说的多么堂而皇之,像影子一样的孤独。 小时候的孤独是一个人,没有玩具没有...
    小懒虫qin阅读 523评论 0 1