Swift高级分享 - 使用Swift Package Manager管理依赖项

当Swift于2015年底开源时,随之而来的最令人惊讶和最有趣的新项目之一是Swift Package Manager。虽然它不是Swift项目的第一个依赖管理器,但它是第一个由Apple正式提供和支持的,许多开发人员认为这是一个非常好的消息。

然而,虽然服务器端Swift社区迅速采用Swift Package Manager作为构建服务器应用程序时管理依赖关系的首选工具,但它需要花费很长时间才能完全集成到Apple的其他开发人员工具链中。

但是现在,从Xcode 11开始,Swift Package Manager终于成为Apple开发人员工具套件中真正的一流公民 - 所以本周,我们来看看如何使用它来管理项目的各种依赖 - 内部和外部的。

Swift包的解剖结构

夫特封装基本上是一组被编译在一起以形成夫特源文件模块 -其然后可以共享和导入其他项目为一个单元。包可以是使用GitHub等服务共享的公共库,也可以是仅在少数项目中共享的内部工具和框架。

使用Package.swift 清单文件声明包的内容,清单文件放在每个包的根目录中。Swift包清单文件不是使用JSON或XML等数据格式,而是使用实际的Swift代码编写的 - 使用Package实例表示包的声明。

举个例子,假设我们正在研究todo列表应用程序,并且我们想要为TodoKit应用程序中共享的所有核心逻辑创建一个包 - 包括我们的数据库层,模型代码等等。上。首先,我们将创建一个新文件夹(名称与我们希望调用包的名称相匹配),然后我们将swift package init在其中运行以创建我们的包:

$ mkdir TodoKit
$ cd TodoKit
$ swift package init

在Xcode 11中,我们还可以使用File > New > Swift Packagemenu命令执行上述设置。

通过执行上述操作,Swift Package Manager现在将为我们的新包创建一个初始结构 - 其中包含一个Package.swift类似于以下内容的清单文件:

// swift-tools-version:5.1

import PackageDescription

let package = Package(
    name: "TodoKit",
    products: [
        // The external product of our package is an importable
        // library that has the same name as the package itself:
        .library(
            name: "TodoKit",
            targets: ["TodoKit"]
        )
    ],
    targets: [
        // Our package contains two targets, one for our library
        // code, and one for our tests:
        .target(name: "TodoKit"),
        .testTarget(
            name: "TodoKitTests",
            dependencies: ["TodoKit"]
        )
    ]
)

swift-tools-version该文件的顶部评论不只是一个评论,它也告诉雨燕的软件包管理器建立我们的包时使用何种版本的雨燕工具链。

默认情况下,Swift包管理器会将清单文件中定义的目标名称与磁盘上的相应文件夹相匹配,以确定属于每个目标的Swift文件。可以通过将其他参数传递给上面使用的API来覆盖该行为以及其他默认设置(例如构建设置,目标平台等)。

添加远程依赖项

除了促进软件包的创建之外,Swift软件包管理器的核心用例之一是将远程依赖项(例如第三方库)添加到项目中。任何可以通过Git获取的包都可以通过指定其URL以及我们希望应用于它的版本约束来添加:

let package = Package(
    ...
    dependencies: [
        // Here we define our package's external dependencies
        // and from where they can be fetched:
        .package(
            url: "https://github.com/johnsundell/files.git", 
            from: "4.0.0"
        )
    ],
    targets: [
        .target(
            name: "TodoKit",
            // Here we add our new dependency to our main target,
            // which lets us import it within that target's code:
            dependencies: ["Files"]
        ),
        .testTarget(
            name: "TodoKitTests",
            dependencies: ["TodoKit"]
        )
    ]
)

上面我们要导入任何版本的的文件包间4.0.05.0.0-留给了雨燕软件包管理器,以解决符合我们的整体依赖图最合适的版本,而默认为在该范围内的最新版本。

使用这样一个广泛的版本约束可能非常强大,因为如果我们要添加另一个需要特定版本的文件的依赖项,那么包管理器可以自由选择该版本(只要它在我们允许的版本范围内) - 制作我们不太可能最终得到一个无法解析的依赖图。

但是,有时我们可能希望锁定其中一个依赖项的特定版本 - 可能是为了避免在更高版本中引入的回归,或者是为了能够继续使用稍后删除的API。为此,我们可以将上述from:参数替换为.exact版本要求 - 如下所示:

.package(
    url: "https://github.com/johnsundell/files.git",
    .exact("4.0.0")
)

另一方面,我们可能希望使用比最新官方版本更进一步的依赖性修订- 例如,包括错误修复或尚未正确发布的新API。为此,我们有两种选择。

第一个选项是将我们的依赖关系指向一个特定的Git分支(如果该分支正在快速变化,这可能会非常危险),或锁定一个特定的提交哈希(风险较低,但也不太灵活,因为我们将每次我们想要更新该依赖项时都必须手动更改该哈希值):

// Depending on a branch (master in this case):
.package(
    url: "https://github.com/johnsundell/files.git",
    .branch("master")
)

// Depending on an exact commit:
.package(
    url: "https://github.com/johnsundell/files.git",
    .revision("0e0c6aca147add5d5750ecb7810837ef4fd10fc2")
)

能够不仅通过版本指定依赖关系,而且通过Git修订版指定依赖关系,也可以非常有用,以便从分叉存储库而不是从原始存储库中临时获取依赖关系。

例如,假设我们在一个外部依赖项中发现了一个错误,并且我们已经在该项目的fork中实现了它的修复。而不是必须等待该修复程序合并到原始存储库,然后发布 - 我们可以简单地将该依赖项指向我们的fork的URL,然后指定master为我们的分支目标,以便能够直接使用我们的修补版本。

使用本地包

当并行处理多个不同的包时,例如将项目拆分为多个较小的库时,使用本地依赖关系有时可能非常有用 - 并且大大改善了迭代次数。

不是从URL下载,而是直接从磁盘上的文件夹添加本地软件包依赖项 - 这两者都允许我们导入我们自己的软件包而不必担心版本控制,并且还使我们能够直接编辑项目中的依赖项源文件那是用它。

例如,以下是我们如何将本地CalendarKit包添加为依赖项TodoKit- 只需指定其相对文件夹路径:

let package = Package(
    ...
    dependencies: [
        .package(
            url: "https://github.com/johnsundell/files.git",
            .exact("4.0.0")
        ),
        // Using 'path', we can depend on a local package that's
        // located at a given path relative to our package's folder:
        .package(path: "../CalendarKit")
    ],
    targets: [
        .target(
            name: "TodoKit",
            dependencies: ["Files", "CalendarKit"]
        ),
        .testTarget(
            name: "TodoKitTests",
            dependencies: ["TodoKit"]
        )
    ]
)

除了能够直接编辑依赖项之外,本地包引用在构建自定义开发人员工具时也非常有用。例如,我们可以使用Swift Package Manager 在应用程序的存储库中构建命令行工具,然后使用本地依赖项将我们的一些应用程序代码导入该工具 - 例如,我们的模型或网络代码。

平台和操作系统版本限制

除了我们明确添加到项目中的内部包和第三方库之外,我们的代码很可能还依赖于特定范围的平台和操作系统版本 - 以便能够访问正确的API和系统框架。

虽然默认情况下假定所有Swift软件包都是跨平台的(并且版本不可知),但是在我们的清单文件中platform初始化时添加参数Package,我们可以将代码限制为仅支持给定的一组平台和操作系统版本 - 像这样,如果我们想构建一个包含iOS 13特定代码的包:

// swift-tools-version:5.1

import PackageDescription

let package = Package(
    name: "TodoSwiftUIComponents",
    platforms: [.iOS(.v13)],
    ...
)

就像在Xcode中为应用程序选择最小部署目标一样,使用该platforms参数使我们能够使用仅在平台或OS版本的子集上可用的API - 例如SwiftUI或Combine。我们当然也可以指定多个平台和版本 - 例如,我们可以附加.macOS(.v10_15)到上面的数组以添加对macOS Catalina的支持。

将包添加到Xcode项目

从Xcode 11开始,现在可以使用Xcode的新Swift Packages选项直接添加Swift包并将其导入到app项目中,该选项位于File菜单中。使用这种新的集成,我们可以轻松地将第三方库作为Swift软件包导入,它还可以让我们利用Swift软件包管理器的强大功能来改进代码库的模块化。

通过创建我们的代码库的不同部分单独的包-就像TodoKitCalendarKit以及TodoSwiftUIComponents从之前的例子-我们都可以提高我们的应用程序中分离原则,也使我们的代码可以很容易地在不同的平台或扩展重用。

例如,通过在我们的模型代码的单独包中定义我们的UI组件,不存在意外地将视图代码与模型代码混合的风险- 这可以帮助我们随着时间的推移维护更加可靠的架构 - 并且它还使我们能够轻松地共享我们在多个目标之间的核心UI组件。

结论

虽然Swift Package Manager不再是一个全新的工具,但它现在可以用于所有Apple平台上的应用程序这一事实使它具有更广泛的吸引力 - 而且感觉就像是Swift包的*“新起点”*作为一个概念。能够使用相同的包管理器构建从服务器端应用程序,命令行工具和脚本到iOS应用程序的任何东西,也非常强大 - 并且可能使我们的部分代码能够在更多的环境中重用。

你怎么看?您之前是否使用过Swift Package Manager,或者您现在可以尝试将它集成到Xcode中吗?请通过加我们的交流群 点击此处进交流群 ,来一起交流或者发布您的问题,意见或反馈。

谢谢阅读~点个赞再走呗!?

原文地址 https://www.swiftbysundell.com/articles/managing-dependencies-using-the-swift-package-manager/

你可能感兴趣的:(Swift高级分享 - 使用Swift Package Manager管理依赖项)