CocoaPods 框架的 Pod 作者指南

TL;DR:CocoaPods 0.36 将带来期待已久的对框架和 Swift 的支持。它尚未发布,尚未被认为是稳定的,但现在可以通过 [sudo] gem install cocoapods --pre 为每个人提供一个测试版。Pod 作者尤其希望尝试此版本,以确保他们的 Pod 将与即将发布的版本一起使用。这是因为如果用户项目中的单个依赖项需要成为框架,那么您的 Pod 也将成为框架。

CocoaPods 集成的框架有什么特别之处?

使用 CocoaPods,框架的设置方式与通过 Xcode 完成的方式非常相似。这是为了使整个集成可检查、可理解,并使我们能够释放现有整个工具链的力量。

许多工具仅在存在某些构建变量的 Xcode 环境中才能很好地协同工作。Cocoa Touch 框架使用 Clang 模块,还需要将它们导入并链接到您的 Swift 应用程序。因此,模块映射包含在构建的框架包中。

动态框架与静态库

那么这两种产品类型有什么区别?

动态框架是包,这基本上意味着它们是具有文件后缀 .framework 的目录,Finder 主要将它们视为常规文件。如果您点击框架,您将看到一个常见的目录结构

除了二进制文件之外,它们还捆绑了额外数据,在这种情况下,二进制文件可以动态链接,并为每个架构保存不同的切片。到目前为止,它与静态库相同。但是,框架保存以下附加数据

  • 公共头文件 - 这些头文件针对应用程序目标进行了剥离,因为它们仅对将框架作为代码进行编译分发很重要。公共头文件还包括为公共 Swift 符号生成的标头,例如 Alamofire-Swift.h
  • 整个内容的代码签名 - 在将框架嵌入应用程序目标时,必须重新计算此代码签名,因为头文件在之前已剥离。
  • 其资源 - 使用的资源,例如用于 UI 组件的图像。
  • 托管动态框架和库 - 这可能是 Apple 提供的所谓 Umbrella Frameworks 的情况。CocoaPods 中不会出现这种情况。
  • Clang 模块映射Swift 模块 - 这些主要是内部工具链工件,它们携带有关 API/标头可见性和模块链接能力的声明。
  • Info.plist - 这指定作者、版本和版权信息。

关于捆绑资源的一个警告是,到目前为止,我们必须将所有资源嵌入到应用程序包中。这些资源由 [NSBundle mainBundle] 以编程方式引用。

Pod 作者能够使用 mainBundle 引用来包含 Pod 带入应用程序包的资源。但是对于框架,您必须确保通过获取对框架包的引用来更具体地引用它们,例如

# in Objective-C
[NSBundle bundleForClass:<#ClassFromPodspec#>]

# or in swift
NSBundle(forClass: <#ClassFromPodspec#>)

这将同时适用于框架和静态库。在极少数情况下,您希望直接或间接地引用主包,例如通过使用 [UIImage imageNamed:]

改进后的资源处理的优点是,当资源具有相同的名称时,它们不会发生冲突,因为它们是由框架包命名的。此外,我们不必自己将构建规则应用于资源,例如资产目录和情节提要需要编译。这应该会减少使用包含许多资源的 Pod 的项目的构建时间。

模块名称

Clang 模块的名称仅限于 C99ext 标识符。这意味着它们只能包含字母数字字符和下划线,并且不能以数字开头。通过查看官方规范存储库,我们发现了一些不符合这些要求的流行 Pod。

以前,作为 Pod 作者,您可以使用 header_dir 来自定义用户目标中标头的名称前缀。例如,如果您的 Pod 名称为 123BánànâKit,您可以将其设置为 BananaKit,它可以通过 import <BananaKit/BananaKit.h> 获得,而不是 #import <123BánànâKit/BananaKit.h>

我们仍然支持这种用法,但也引入了一个新的属性 module_name,您可以在 Podspec 中声明该属性。此新属性的优点在于它将被正确地进行 lint 和验证,否则我们将从 header_dir 选项开始。如果任一属性不存在,那么我们将使用规范名称来匹配 Clang 模块名称要求。

简而言之,请查看以下 Swift 代码段,它简洁地表达了我们确定模块名称的方式。

//let c99ext_identifier: String -> String?
func module_name(spec: Specification) -> String {
  return spec.module_name
    ?? c99ext_identifier(spec.header_dir)
    ?? c99ext_identifier(spec.name)!
}

模块映射

模块映射是头文件声明,它形成 Clang 模块的公共(或私有)接口。幸运的是,这些头文件已被设计为可以保持在后台,并且开发人员可以利用已知和现有的结构,而无需学习 DSL。默认模块映射基本上始终相同

framework module BananaKit {
  umbrella header "BananaKit.h"

  export *
  module * { export * }
}

这仅明确引用了一个文件:保护伞头文件。

您可以在保护伞头文件中导出框架的公共 API,以及所有传递导入的头文件。Clang 将负责制作可由 Objective-C 和 Swift 导入的模块导出。

在这种情况下,传递导入是什么意思?

传递关系是一个数学概念

每当元素a与元素b相关,而b又与元素c相关时,则a也与c相关。

这里我们有头文件的二元关系,它导入另一个头文件。传递闭包意味着所有头文件都由您从某个文件导入的头文件导入,因此也间接导入到该文件。对于由这些头文件集合导入的所有头文件,情况也是如此。当然,您从应用程序目标中了解此属性,每当您导入导入其他头文件的头文件时,其中定义的类和符号也将在您的应用程序代码中可用。对于保护伞头文件中的导入语句,也必须应用相同的情况,并通过 Clang 模块影响模块可见性。

什么是保护伞头文件?

对于我们的示例,它可能如下所示

#import <Foundation/Foundation.h>

@import Monkey;

#import "BKBananaFruit.h"
#import "BKBananaPalmTree.h"
#import "BKBananaPalmTreeLeaf.h"

FOUNDATION_EXPORT double BananaKitVersionNumber;
FOUNDATION_EXPORT const unsigned char BananaKitVersionString[];

最初的目的是索引目录的所有公共标头,以便为导入/包含提供简写,以访问库的完整 API。随着时间的推移,它们开始涵盖越来越多的目的

  • 使用(Cocoa Touch)框架:它们允许通过动态生成的 C 代码快速访问其 Info.plist 中定义的版本控制值。因此,它们必须定义一个接口才能使它们可访问。这些是 Xcode 模板中以 FOUNDATION_EXPORT 为前缀的常量声明。
  • 使用Clang 模块:它们用于定义模块的公共接口。
  • 使用Swift:它们是框架模块的桥接标头,这实质上意味着您在框架内从 Swift 中调用的所有 Objective-C 代码都必须是其公共 API 的一部分。

现有 Podspec 的当前情况

Xcode 中从未有过可声明的伞形标头。因此,Pod 作者从未指定过一个。

尽管始终有一个已知的模式,即有一个公共标头,它传递性地导入所有其他公共标头。情况并非总是如此。

出于此原因,CocoaPods 承担责任并生成一个自定义伞形标头(例如 Pods-iOS Example-AFNetworking-umbrella.h)。这是通过自定义模块映射注入的,这样我们就不会遇到名称歧义。否则,默认模块映射会假设它与框架同名,而这可能已被采用。

我们生成的标头导入所有声明的公共标头。这也为版本控制常量定义了 FOUNDATION_EXPORT,其名称由 CocoaPods 用于框架集成。此外,这避免了在某些特殊情况下的问题:例如,AFNetworking 有一个子规范,它为 UIKit 提供了类别,它有自己的批量导入标头 AFNetworking+UIKit.h,它没有被 AFNetworking.h 标头导入以实现 OSX 兼容性。

要在 Swift 中使用此子规范而不使用生成的伞形头文件,你需要创建一个桥接头文件并使用导入,例如 #import <AFNetworking/AFNetworking+UIKit.h>。使用生成的伞形头文件,如果你在 Podfile 中包含了子规范,你只需要 import AFNetworking。如果你的 pod 无法开箱即用,你可以使用 pod lib lint --use-frameworks <YourPod.podspec> 来检查问题所在。我们尝试在不同的流行 pod 中使用它,有时会遇到由错误配置的公共头文件导致的问题。

关于公共头文件

Podspec 中的公共头文件通过 s.public_header_files = ["Core/*.h", "Tree/**.h"] 声明。

如果你不包含此规范,那么你的所有头文件都将是公共的。在大多数情况下,不建议这样做。

通常,你应该确保你的头文件是自包含的,并且只公开 Pod 用户使用的实现部分。这有几个优点

  • 它允许你重构私有实现部分,而无需发布重大更新,这使得版本迁移更容易,并且允许你专注于进一步改进你的 pod,而不是向用户解释 API 如何更改。
  • 它阻碍了错误使用,因为你需要修改头文件访问权限才能使用或操作类或属性,而这些类或属性不打算在外部使用。

常见的头文件陷阱

如果你有一个像这样的头文件

/// BKBananaFruit.h

#import "BKBananaTree.h"
#import "monkey.h"

@interface BKBananaFruit
@property (nonatomic, weak) BKBananaTree *tree;
- (void)peel:(Monkey *)monkey;
@end

如果你收到如下错误,不要被它迷惑。

你可以在框架中包含头文件,但不能包含带引号的头文件,这些头文件不在框架的公共头文件的范围内。因此,在这种情况下,你有两个选择:通过从公共头文件声明中排除头文件 BKBananaFruit.h 来使其变为私有,或使用系统导入来导入猴子。

-#import "monkey.h"
+#import <monkey/monkey.h>

Xcode 怪异之处

我们在开发期间遇到过几次此错误。

<unknown>:0: error: could not build Objective-C module 'BananaKit'

如果你在 Xcode 中开发框架,并且更改头文件可见性以修复前面描述的构建问题,并尝试通过执行清除操作(在 Xcode 中为 ⌘+ ⇧+K)来确保构建状态干净,则可能会出现上述错误。在这种情况下,从文件系统中手动删除产品构建目录(即 DerivedData)可能会有所帮助。

可用性

CocoaPods 仅支持 OS X 10.9 及更高版本上的 Swift,以及 iOS 8 及更高版本。

原因如下

  • 如 Apple 多次声明,Swift 受 OS X 10.9/iOS 7 及更高版本支持。
  • 不支持使用 Swift 构建静态归档。
  • 所有版本的 OS X 均支持动态框架。
  • iOS 8 之前的版本不支持动态框架

    ld: 警告:嵌入式 dylib/框架仅在 iOS 8 或更高版本上运行。

由此我们可以得出结论,不可能在早于 OS X 10.9 和 iOS 8 的任何平台上支持 Swift。

要在支持 iOS 7 的应用上使用 Swift 库,必须手动将文件复制到应用项目中。

更新

要安装 CocoaPods 的最新 Beta 版,可以运行

$ [sudo] gem install cocoapods --prerelease

在 1.0 版本之前,我们强烈建议你保持 CocoaPods 为最新版本。有关面向用户的更多更改,可以查看我们的即将发布的官方版本博客文章

有关所有详细信息,请查看PR