Swift Package Manager(SPM)

Swift Package ManagerSwift 包管理器,一般简称 SwiftPMSPM)是苹果官方提供的一个用于管理源代码分发的工具,用于管理Swift代码分发的工具。它与Swift构建系统集成在一起,可以自动执行依赖项的下载,编译和链接过程。该工具可以帮助我们编译和链接 Swift packages(包),管理依赖关系、版本控制,以及支持灵活分发和协作(公开、私有、团队共享)等。支持SwiftObjective-CObjective-C ++CC ++
SPM 软件包管理器包含在Swift 3.0及更高版本中,用于处理模块代码的下载、编译和依赖关系等,支持 macOSLinux。与CocoaPodsCarthage功能类似,不过比这两个更简洁,代码的侵入性更小,也不需要额外安装工具。

SPM版本

Xcode自带,可以在终端查看版本

 ~ swift package --version
Swift Package Manager - Swift 5.3.0

SPM管理添加三方库

SPM包含在Xcode 8.0和所有后续版本中。在 Xcode 11 发布的时候,通过libSwiftPM 集成来支持iOSwatchOStvOS 平台。也就是iOS11以后才能用于iOSwatchOStvOS 平台。并且需要库适配SPM
⚠️:也就是Xcode版本11iOS最低版本支持iOS8macOS 10.10tvOS 9.0watchOS 2.0,当然也支持Linux
如何判断一个库支不支持SPM?
查看库是否有Package.swift文件。

方式一:File -> Swift Packages -> Add Package Dependency

image.png

方式二:PROJECT -> Swift Packages -> Packages

image.png

两种方式最终会打开Choose Package Repository:
image.png

1.输入三方库链接,验证通过后界面如下:


image.png

可以通过3种方式集成VersionBranch以及Commit
Version有4个选项:

  • Up to Next Major : 大版本更新 当前指定的版本号到下一个大版本号之间的最新版本,例如 5.0.0 ~ 6.0.0(不包含 6.0.0
  • Up to Next Minor : 小版本更新 当前指定的版本号到下一个次版本号之间的最新版本,例如 5.0.0 ~ 5.1.0(不包含 5.1.0
  • Up to Next Ranger : 指定的两个版本号之间的最新版本,例如 5.1.0 ~ 5.2.2(不包含 5.2.2
  • Up to Next Exact : 指定使用某一具体的版本号

2.完成后


image.png

3.管理/更新依赖


image.png

image.png
  • Reset Package Caches:重置依赖包的缓存。
  • Resolve Package Versions:生成 Package.resolved文件,确定依赖的版本信息。
  • Update to Latest Package Versions:根据配置的依赖包版本控制规则,对依赖包进行升级。

4.使用

image.png

这个时候Target种已经引入进来了。
image.png

至此,整个依赖流程就已经处理完了。

SPM是如何管理RXSwift的呢?

在引进RXSwift库后在SPM项目中并没有相关代码,也没有类似CocoaPodsPodfile文件,在项目的RxSwift右键Show In Finder查看下具体位置:

/Users/binxiao/Library/Developer/Xcode/DerivedData/SPM-eahnayohsullgthbkkmzyjocrsnv/SourcePackages/checkouts/RxSwift

也就是Xcode项目编译缓存DerivedData中。

image.png

那么配置文件在哪里呢?
直接去SPM.xcodeproj中找,在project.pbxproj中搜索RXSwift就能找到配置信息了。这也就是SPM存放配置信息的地方。
image.png

CocoaPods与SPM能否一起使用?

能够同时使用:


image.png

生成Swift Package

初始化一个Swift Package项目有两种方式:命令行和Xcode

命令行中的SPM

swift package init:

➜  SwiftPackageByCommandLine swift package init
Creating library package: SwiftPackageByCommandLine
Creating Package.swift
Creating README.md
Creating .gitignore
Creating Sources/
Creating Sources/SwiftPackageByCommandLine/SwiftPackageByCommandLine.swift
Creating Tests/
Creating Tests/LinuxMain.swift
Creating Tests/SwiftPackageByCommandLineTests/
Creating Tests/SwiftPackageByCommandLineTests/SwiftPackageByCommandLineTests.swift
Creating Tests/SwiftPackageByCommandLineTests/XCTestManifests.swift

目录结构如下:

image.png

SPM 管理的每个 Package相当于 Xcode.Project。目录下必须含有 Package.swiftSources代码文件夹(链接系统的包除外)

  • Package.swift:描述库的配置和属性,相当于CocoaPods.podspec.podfile文件。
  • Source/TargetName:存放源码,TargetName用来区分Target

swift package 命令

swift package init --help
OVERVIEW: Initialize a new package

OPTIONS:
  --name   Provide custom package name
  --type   empty|library|executable|system-module|manifest

--name

指定Package名称 ,默认上一级文件夹名称。

--type:

  • empty:空包
    Source 文件夹下什么都没有,也不能编译
  • library:静态包
    Source 文件夹下有个和包同名.swift文件,里面有个空结构体
struct library {
    var text = "Hello, World!"
}
  • executable: 可执行包(默认)
    Source文件夹下有个main.swift文件,在 build 之后会在.build/debug/目录下生成一个可执行文件,可以通过 swift run 或者直接点击运行,启动一个进程。
➜  SwiftPackage cd executable
➜  executable swift package init --type executable
Creating executable package: executable
Creating Package.swift
Creating README.md
Creating .gitignore
Creating Sources/
Creating Sources/executable/main.swift
Creating Tests/
Creating Tests/LinuxMain.swift
Creating Tests/executableTests/
Creating Tests/executableTests/executableTests.swift
Creating Tests/executableTests/XCTestManifests.swift
➜  executable swift build
[3/3] Linking executable
➜  executable swift run
Hello, world!
  • system-module:系统包
    这种包是专门为了链接系统库(例如 libgitjpeglibmysql 这种系统库)准备的,本身不需要任何代码,所以也没有 Source 文件夹,但是需要编辑 module.modulemap 文件去查找系统库路径 (Swift 4.2 已经被其他方式取代)
  • manifest:只包含一个Package.swift文件。

其它命令用法可以通过swift package --help查看。

swift build

swift build用于编译packageSPM 使用llbuild 作为其底层编译引擎,提供快速和正确的增量编译,它也被 Xcode 新的编译建系统使用,是 Swift 开源项目的一部分。

SPM 构建 package 的编译环境是在一个沙盒中被隔离独立的,无法随意执行命令或 shell 脚本,因此保证了其安全性。同时也支持基于 XE.framework 单元测试,并发测试,或者指定测试某些场景(Test Filtering)等。

swift run

swift run: 用于编译并运行一个可执行文件,该命令是在 Swift 4 中新增加的,相当于:

$ swift build
$ .build/debug/executable(可执行文件)

swift test

swift test: 用于运行 package 中的单元测试

Xcode中的SPM

File -> New -> Swift Packageshift + control + command + N

image.png

或者:File -> New -> Project(command + shift + n)
image.png

修改SwiftPackageByXcode.swift如下:

public struct SwiftPackageByXcode {
    
    public init() {
        
    }
    
    public var text = "Hello, World!"

    public func test() {
        print(self.text)
    }
    
}

⚠️:Xcode验证Swift Package需要打开Package.swift编译看是否通过。

宿主工程依赖本地Package(以SPM为例):

直接将整个Package拖入宿主工程,然后在Frameworks添加(目前没有找到类似CocoaPods 直接:path => ''指向本地的方式):

image.png

测试编译运行通过

引用远程依赖

上传项目至github(记的打tag
引入到之前的SPM工程中:

image.png

调用SwiftPackageByXcodetest方法进行验证:

//ViewController.swift
import UIKit
import RxSwift
import HandyJSON
import SwiftPackageByXcode

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        SwiftPackageByXcode().test()
    }
}

输出:

Hello, World!

至此生成自己的SPM库并且使用就已经完成了。

SPM-OC

生成Swift Package for OC

直接创建Swift Package,将默认Sources/PackageName目录下的.Swift文件删除,加入C,C++以及OC的代码,Package目录结构如下:

image.png

如果头文件不想放在include目录下,要么配置publicHeadersPath要么进行软连接。

软连接:进入include目录,终端执行ln -s。可以写一个脚本进行维护。

//进入include目录,终端执行
ln -s ../test.h  test.h

Package配置:

    name: "SPMLibraryForOC",
    products: [
        .library(
            name: "SPMLibraryForOC",
//            type: .static,
            targets: ["SPMLibraryForOC"]
        ),
    ],
    targets: [
        .target(
            name: "SPMLibraryForOC"
            //源文件路径,默认Sources/TargetName
//            path: "Sources/SPMLibraryForOC"
            //默认路径 path/include
//            : "Sources/SPMLibraryForOC/include"
        )
    ]

如果对已经存在的库进行适配,需要自己根据目录配置path(默认Sources/TargetName)以及publicHeadersPath(默认path/include)。
具体的配置可以参考一些开源的第三方OC库的适配,比如:AFNetworking

使用

添加Swift Package依赖,桥接文件(XXX-Bridging-Header.h)中导入头文件:

#import 
#import 
#import 
#import 

调用:

import RxSwift
import HandyJSON
import SwiftPackageByXcode

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        //调用三方 Swift Package
        let rxError = RxError.argumentOutOfRange
        print("RxSwift \(rxError)")
        
        //调用自定义Swift Package
        print("SwiftPackageByXcode")
        SwiftPackageByXcode().test()
        
        //调用三方 Swift Package OC
        let manager = AFHTTPSessionManager(baseURL: URL(string: "https:www.baidu.com"))
        print("AFNetworking \(manager)")
        
        //调用自定义Swift Package OC/C/C++
        testC()
        SPMTestOC().testOC()
        SPMTestMixed().testCplus()
    }
}

输出:

RxSwift Argument out of range.
SwiftPackageByXcode
Hello, World!
AFNetworking , operationQueue: {name = 'NSOperationQueue 0x7fe71b008b90'}>
test c function
2021-03-16 17:06:30.362943+0800 SPM[65233:17898750] test oc function
2021-03-16 17:06:30.363060+0800 SPM[65233:17898750] testCplus function
testCplusHotpotCat

库中依赖其他库

需要在Package.swift文件中添加依赖,直接在dependencies中加入保存Xcode就会自动去拉取了。

//依赖库配置
dependencies: [
    //第三方有规范的格式: 大版本.小版本.测试版本
    .package(url: "https://gitee.com/guaizaizaiguai/Alamofire.git",
             from: .init(5, 4, 1)
    )
],

Package.swift配置

Package.dependencies

Package.dependencies:用于添加包的依赖。在执行 Swift build 时会自动执行一个 swift package resolve 命令,该命令会解析 Package.swift 的依赖,并生成对应的 package.resolved 文件。类似于CocoaPods 中的 Podfile.lockCarthage 中的 Cartfile.resolved。当执行依赖解析的时候,会优先解析这个文件,不存在时才会解析 Package.swift。在上传时把它忽略掉。

主工程 package.resolved

如果主工程依赖了Swift Package那么package.resolved目录在项目目录/项目名称.xcodeproj/project.workspace/xcshareddata/swiftpm/

image.png

⚠️:如果项目引入了Cocoapods,目录为:项目目录/项目名.xcworkspace/xcshareddata/swiftpm
内容如下:

{
  "object": {
    "pins": [
      {
        "package": "AFNetworking",
        "repositoryURL": "https://gitee.com/guaizaizaiguai/AFNetworking",
        "state": {
          "branch": null,
          "revision": "ffae2391ab0c29dc88eb0a58d2f5b2c2c27cadbf",
          "version": "4.0.1"
        }
      },
      {
        "package": "RxSwift",
        "repositoryURL": "https://gitee.com/mirrors/RxSwift",
        "state": {
          "branch": null,
          "revision": "7e01c05f25c025143073eaa3be3532f9375c614b",
          "version": "6.1.0"
        }
      },
      {
        "package": "SPMLibraryForOC",
        "repositoryURL": "https://gitee.com/guaizaizaiguai/SPMLibraryForOC.git",
        "state": {
          "branch": "master",
          "revision": "d96ac816a5ddf4732d29f8ffadf2d3e52a99b1b2",
          "version": null
        }
      },
      {
        "package": "SwiftPackageByXcode",
        "repositoryURL": "https://gitee.com/guaizaizaiguai/SwiftPackageByXcode.git",
        "state": {
          "branch": "master",
          "revision": "80993a21fd03b0334e0e5a25fe29d04d45d7e472",
          "version": null
        }
      }
    ]
  },
  "version": 1
}

自己 Package 的 package.resolved

如果自己的Swift Package依赖了其它Package那么也会在根目录生成Package.resolved,里面记录依赖库的版本信息。

依赖其它Swift Package(5个类型)
详细信息可以在Package.Dependency中查看。
name参数(库名称)一般都省略(在库名称和URL中地址相同的情况下,官方文档这样描述:The name of the package, or nil to deduce it from the URL.

  • git source + 确定版本号
//指定版本号
.package(url: "https://gitee.com/guaizaizaiguai/Alamofire.git",
         .exact("5.4.1")
),

//以下这两种方式也可以,如果版本错误会往上找5.4.1~6.0.0
.package(url: "https://gitee.com/guaizaizaiguai/Alamofire.git",from: "5.4.1"),

//这种方式是库名称和URL中的Alamofire.git不匹配。如果匹配会在URL中查找。 The name of the package, or nil to deduce it from the URL.
.package(name: "Alamofire", url: "https://gitee.com/guaizaizaiguai/Alamofire.git", from: "5.4.1")

//第三方有规范的格式: 大版本.小版本.测试版本
.package(url: "https://gitee.com/guaizaizaiguai/Alamofire.git",
         from: .init(5, 4, 1)
),

//如果版本不太规范比如只有两位版本号的库
.package(url: "https://gitee.com/guaizaizaiguai/Alamofire.git",
         from: .init(stringLiteral: "5.4")
),
  • git source + 版本区间
    枚举值在Package.Dependency.Requirement中定义
//指定5.4.1~6.0.0
.package(url: "https://gitee.com/guaizaizaiguai/Alamofire.git",
         .upToNextMajor(from: "5.4.1")
),

//指定5.4.1~5.5.0
.package(url: "https://gitee.com/guaizaizaiguai/Alamofire.git",
         .upToNextMinor(from: "5.4.1")
),

//指定区间 5.0.1~5.5.6
//.package(url: "https://gitee.com/guaizaizaiguai/Alamofire.git", "5.0.1"..."5.5.6"),
.package(url: "https://gitee.com/guaizaizaiguai/Alamofire.git", "5.0.1"..<"5.5.6"),
  • git source + commit
//指定commit
.package(url: "https://gitee.com/guaizaizaiguai/Alamofire.git",
         .revision("commit")
),

-git source + 分支

//指定分支
.package(url: "https://gitee.com/guaizaizaiguai/Alamofire.git",
         .branch("master")
),
  • 本地路径
.package(path: "../Example"),

条件依赖
以前可以在.package中指定条件,目前好像已经去除了,可以在Target中分别配置了。

//⚠️目前已经没有这个的配置了
.package(url: "https://..",majorVersion:1),
.package(url: "https://...", from: "1.0.0", when: .testing),
.package(url: "https://...", from: "2.0.0", when: .os(.linux),

平台配置(Package.SupportedPlatform)

这个 Struct 用于设置包的依赖平台和版本:

//平台配置
platforms: [
    .iOS(.v10), //枚举定义在 IOSVersion 中
    .macOS(.v10_12),// MacOSVersion
    .watchOS(.v5),//WatchOSVersion
    .tvOS(.v10),//TVOSVersion
   //也可以直接指定字符串版本号
   //.macOS("10.15"), 
   //.iOS("13")
],
image.png

具体版本可以分别在IOSVersionMacOSVersionWatchOSVersionTVOSVersion中查看。
⚠️:虽然这个属性是个数组,同一平台只允许设置1个值,否则报错:

//平台配置
platforms: [
    .iOS(.v10),
    .iOS(.v13)
],

//Failed to parse the manifest file
//found multiple declaration for the platform: ios

Package.Product

ProductPackage 编译后对外的产物,当执行完 Swift build 之后,就会在 .build/debug 下生成对应的可执行文件/静态库 (.a)/动态库( .dylib):

  • 可执行文件
    创建Package指定typeexecutable
//库配置
products: [
    // Products define the executables and libraries a package produces, and make them visible to other packages.
    .executable(
        name: "SwiftPackageByXcode",
        targets: ["SwiftPackageByXcode"]),
],
  • 静态库或者动态库
//库配置
products: [
    // Products define the executables and libraries a package produces, and make them visible to other packages.
    .library(
        name: "SwiftPackageByXcode",
        //动态库 or 静态库(默认)
        type: .static, //.dynamic
        targets: ["SwiftPackageByXcode"]),
],

参数说明

  • name:package导出产物的名称,也就别的地方引用import PackageName
  • executable:可执行文件
  • library:库文件
  • targets:包含的target
  • type:LibraryType分为staticdynamic

Package.Target

targetPackage 的基本构件,和 xcodeproject 一样,Package 可以有多个 target。
一般分为三种类型:

  • 常规Target
  • 测试Target
  • 系统库Target

分别对应

/// The type of this target.

public enum TargetType : String, Encodable {
    case regular
    case test
    case system
    case binary
}

public static func target(name: String, dependencies: [PackageDescription.Target.Dependency] = [], path: String? = nil, exclude: [String] = [], sources: [String]? = nil, publicHeadersPath: String? = nil, cSettings: [PackageDescription.CSetting]? = nil, cxxSettings: [PackageDescription.CXXSetting]? = nil, swiftSettings: [PackageDescription.SwiftSetting]? = nil, linkerSettings: [PackageDescription.LinkerSetting]? = nil) -> PackageDescription.Target

public static func testTarget(name: String, dependencies: [PackageDescription.Target.Dependency] = [], path: String? = nil, exclude: [String] = [], sources: [String]? = nil, cSettings: [PackageDescription.CSetting]? = nil, cxxSettings: [PackageDescription.CXXSetting]? = nil, swiftSettings: [PackageDescription.SwiftSetting]? = nil, linkerSettings: [PackageDescription.LinkerSetting]? = nil) -> PackageDescription.Target

public static func systemLibrary(name: String, path: String? = nil, pkgConfig: String? = nil, providers: [PackageDescription.SystemPackageProvider]? = nil) -> PackageDescription.Target=

Target配置

  • name:名称。
  • dependencies:依赖项
    这里的dependenciesPackage.Dependency不是一个东西。这里可以依赖
    Package.Dependency 中的内容或者依赖另一个target这里只需要写Package 或者 Target 的名字字符串(Target.Dependency 这个枚举也实现了ExpressibleByStringLiteral)。
//导入依赖
dependencies: [
    "Alamofire",
    .byName(name: "Alamofire"),
    .target(name: "Alamofire"),
    .product(name: "Alamofire", package: "Alamofire"),
    .target(name: "Alamofire", condition: .when(platforms: [.iOS]))
],
  • path:target 的路径,默认是[PackageRoot]/Sources/[TargetName]
  • sources:源文件路径。默认TargetName文件夹下都是源代码文件,会递归搜索。
    SPM 会自动包含磁盘上当前 packageSources 目录中的源文件,且 Sources 目录下的各子文件夹中的代码会自动与同名的 target 关联,而不需要在清单文件中显式声明。
  • resources:资源文件。
  • exclude: 需要被排除在外的文件/文件夹,这些文件不会参与编译。
  • publicHeadersPathc家族库的公共头文件地址。
  • swiftSettings:定义一个用于特定环境(例如Debug)的宏。
swiftSettings:[
    .define("ENABLE_SOMETHING", .when(configuration: .release)),
    .define("ENABLE_OTHERTHING", .when(platforms: [.iOS], configuration: .release)),
    .define("ENABLE_OTHERTHING", .when(configuration: .debug)),
    .unsafeFlags(["-cross-module-optimization"]),
    .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release))
]

define:条件编译,相当于

#if ENABLE_SOMETHING
  ...
#endif

unsafeFlags:设置的 Swift package 不能用作依赖项。

linkerSettings: [
    .unsafeFlags(["-Xlinker", "-rpath", "-Xlinker", "@executable_path/../../../lib/swift/macosx"], .when(platforms: [.macOS])),
]),
  • cSettings:c家族语言设置
cSettings: [
  .headerSearchPath("path/relative/to/my/target"),
  .define("DISABLE_SOMETHING", .when(platforms: [.iOS], configuration: .release)),
],
  • linkerSettings: 用于链接一些系统库。比如:
.target(
    name: "OpenGLLibrary",
    dependencies: ["Cglew", "Cglfw"],
    linkerSettings: [
        .linkedFramework("OpenGL")
    ]),
  • cxxSettings: A CXX-language build setting,定义在CXXSetting中。
let versionStr = "10.3.0"

 cxxSettings:[
    .headerSearchPath("aztec"),
    .define("REALM_DEBUG", .when(configuration: .debug)),
    .define("REALM_VERSION_MAJOR", to: String(versionStr.split(separator: "-")[0].split(separator: ".")[0]))
 ]
  • systemLibrary:导入系统库
    GLFWGLEW 都是由 C 编写的库,其中CglewCglfw是制作的可以在Swift中调用的模块。这里首先需要通过HomebrewmacOS上按安装CglewCglfw
 ...
targets: [
    ....
    .systemLibrary(
        name: "Cglew",
        pkgConfig: "glew",
        providers: [
            .brew(["glew"])
        ]),
    .systemLibrary(
        name: "Cglfw",
        pkgConfig: "glfw3",
        providers: [
            .brew(["glfw"])
        ]),
]

其中SystemPackageProvider可以选brewaptyum
配置完了后还要创建对应的Target目录和头文件以及module.modulemap文件。

  • providers:是可选的,在目标库没有被安装时,它为SPM提供了用于安装库的方式的提示。
  • pkConfig:指定pkConfig文件的名称SPM可以通过它找到要导入的库的头文件和库搜索路径pkConfig的名称可以在库的安装路径的lib/pkconfig/xxx.pc中找到。

⚠️:目前SPM支持编译其他语言,如:C/C++/Objective-C 等,但是目前不支持将这些语言与Swift混合编译放在同一个target中,需要分别放在不同的target

image.png

swiftLanguageVersions

支持的Swift语言版本,可以同时设置多个版本。

//支持的 Swift 语言版本
swiftLanguageVersions: [
    .v5
]

cLanguageStandard

用于package中所有C目标的C语言标准,具体定义在CLanguageStandard中,

cLanguageStandard:.c11,

cxxLanguageStandard

用于package中所有C++目标的C++语言标准,具体定义在CXXLanguageStandard中。

cxxLanguageStandard: .cxx14

添加资源文件

SPM 令人诟病的一个问题就是无法在包里添加资源文件。具体可以观看官方视频

image.png

配置
对于一些使用目的明确的文件类型,比如下面图中的这些。开发者不需要在 Package.swift 文件中配置任何东西,因为 Xcode 知道这些类型的文件是代表什么,比如 .xcassets 文件代表图片、颜色资源, xib 代表用户界面文件等。

image.png

对于一些使用目的不太明确的文件类型(如下图中的一些文件类型),则需要在 package.swift 文件中配置。例如纯文本文件,这种文件中的数据可能是需要在运行时被加载而计算或者展示,也可能只是一个开发者文档。
image.png

对于意义不明的文件,需要在package.swift 中根据规则配置:
image.png

targets: [
    //自己库的target
    .target(
        name: "SwiftPackageByXcode",
        //导入依赖
        dependencies: [
           "Alamofire",
        ],
        //排除
        exclude:[
            "Readme.txt"
        ],
        //资源
        resources:[
            .process("image.png"),
            .copy("BundleData")
        ]),
],
  • Media.xcassetTest.storyboard 文件,Xcode 能明确知道它代表什么,所以不需要在这个配置文件中配置
  • Readme.txt 文件是说明文件,写在targetexclude 属性中,Xcode 就不会把它编译进包里
  • image.pngBundleData不能自动识别的类型并且需要被加载到package 中则配置在resources属性中。
    对于resource 属性,静态方法:process()copy()process() 是推荐的方式,它所配置的文件会根据具体使用的平台和内置规则进行适当的优化。比如在运行时将 storyboard 或者 asset catalog 转换成适当的形式,也包括压缩图片等。如果文件类型无法识别,或者不能根据平台做任何优化,就只会被简单的拷贝(copy() ,目录的复制会递归进行深复制)。

1.对于不需要被外部引用的,例如内部的开发者文档 README ,需要配置在 target.excludes 属性中。
2.对于运行时有用到,可以被系统根据平台优化的文件,比如各种图片,需要配置在 target.resource.process 属性里
3.对于运行时有用到,不存在优化的文件,比如各种图片,需要配置在 target.resource.copy 属性里

构建
当一个 App 使用 package 时,这个 package 包括源文件和资源文件。在编译时首先会将 Package 中每个 target 的源文件编译成 module 链接到 App中,然后这些 target 中的资源文件则会被加工成 bundle 放到这些 module 中。

image.png

Apple 平台中,AppApp extension 都是 bundle 集合,这些 packagebundle 就是 App 的一部分,所以不需要做其他处理,就能在运行时获取这些 bundle
当被编译到一个 unbundle 产物时,比如脚本工具,则需要在脚本启动的同时加载资源 bundle

访问资源文件
在编译有资源文件的Package 时,会自动创建并添加到 module 中一个文件: resource_bundle_accessor.swift,内容如下:

import Foundation
extension Bundle {
    static let module = Bundle(path: "\(Bundle.main.bundlePath)/path/to/this/targets/resource/bundle")
}

对于 SwiftOC 调用:

//Swift
let path = Bundle.module.path(forResource: "image", ofType: "png")

//OC
NSString *path = [SWIFT_MODULE_BUNDLE pathForResource:@"image", ofType:@"png"];

由于 module 是内部属性,这种方式只能访问自己模块内部的资源文件,无法跨模块访问。如果想在一个公共模块提供外部模块使用的资源,则需要自己创建一个资源访问器。类似Cocoapodsresource_bundle 的功能,可以采用 bundle 路径方式访问。

本地化

LanguageandLocaleIDs
1.配置默认语言

name: "SwiftPackageByXcode",
//默认语言,需要配置在 platforms 前面
defaultLocalization:"en",

//平台配置
platforms: [
    .iOS(.v10), //枚举定义在 IOSVersion 中
],

2.工程文件
根据需要的语言创建对应的文件夹,文件名为对应的语言,后缀命名成.lproj ,并在文件夹中创建 .strings 或者 .stringsdict 文件:

image.png

3.使用

Button(
    action:testAction,
    label: {
    Text("TestKey",
    bundle: Bundle.module
    )
    .font(.title)})

二进制依赖

Swift Package 在集成二进制文件时不需要任何特殊的设置,它们也是一个普通的Package.product,在 targetdependencies里通过名字指定即可。
⚠️:Swift 5.3之后可用。目前仅支持xcframework格式(也就是说.a,.dylib,.framework需要包装成xcframework)。更多讨论

package集成二进制文件

Swift 5.3新增了一种新的Target类型binaryTarget来指定打包好的二进制文件。
在分发二进制依赖时注意点:

  • 目前只支持苹果平台,为了实现的便捷复用了已有的 XCFramework 格式,它支持动态和静态链接,并且可以同时支持多个平台。
  • SPM支持zip和原始xcframework文件以实现二进制依赖性。如果使用zip 要确保xcframework位于根目录。
  • 支持本地路径或者 https 链接。
  • 在使用本地路径时指向的可以是 XCFramework 的路径或者是 XCFramework 压缩后的 zip 文件,而https链接则只能指向 zip 文件。
  • Swift不会对二进制依赖性进行验证,意味着需要自己确保提供正确且有效的文件。

URL方式

.binaryTarget(
   name: "HotpotCat",
   url: "https://github.com/binxiao0604/HotpotCatTest/releases/download/1.0.0/HotpotCat.xcframework.zip",
   //1.下载下来HotpotCat.xcframework.zip
   //2.需要在本工程计算,swift package compute-checksum /Users/zaizai/Desktop/HotpotCat/HotpotCat.xcframework.zip
   checksum: "f529b4ec593dbad862c3d4e5775506caedeed6f6b3a0d9199eb31a4db11c5318"
)

可以通过执行swift package compute-checksum 生成校验和。 需要确保首先下载二进制文件以计算校验和。

image.png

1.先将zip下载到本地。
2.在Swift Package工程计算。
问题:
1.artifact of binary target 'HotpotCat' has changed checksum; this is a potential security risk so the new artifact won't be downloaded
这里更改版本也就是URL和checksum需要清理缓存重新编译

原始文件方式(RAW FILE)

可以通过将二进制文件放入源中来添加二进制文件
目录结构:


image.png
.binaryTarget(
    name: "Cat",
    path: "framework/Cat.xcframework"
),

编译后的产物:URL引入和本地引入的XCFramework最终会以.framework存在。

image.png

那么下载的远程XCFramework在哪呢?
SourcePackages->artifacts->PackageName->XXX. xcframework
image.png

Package中调用CatHotpotCat

//HopotPackage.swift

import Cat
import HotpotCat

open class HotpotPackageTest {
    public init(){}
    
    open func test() {
        let cat = Cat()
        cat.cat()
        HPTest().hpTest()
    }
}

完整配置:

let package = Package(
    name: "HopotPackage",
    platforms: [
        .macOS("10.15"),
        .iOS(.v13)
    ],
    products: [
        .library(
            name: "HopotPackage",
            targets: ["HopotPackage"]),
    ],
    dependencies: [
    ],
    targets: [
        .target(
            name: "HopotPackage",
            //如果要在本项目使用,需要依赖Target
            dependencies: ["Cat","HotpotCat"]
        ),
        .binaryTarget(
            name: "Cat",
            path: "framework/Cat.xcframework"
        ),
        .binaryTarget(
           name: "HotpotCat",
           url: "https://github.com/binxiao0604/HotpotCatTest/releases/download/1.1.0/HotpotCat.xcframework.zip",
           //1.下载下来HotpotCat.xcframework.zip
           //2.需要在本工程计算,swift package compute-checksum /Users/zaizai/Desktop/HotpotCat/HotpotCat.xcframework.zip
           //3.这里更改版本也就是URL和checksum需要清理缓存重新编译
           checksum: "7d8bdb4297ff837ffe2ae87d211283c8fa5b5ab9199540b1617da1a63ce638a2"
        )
    ]
)

Demo地址

其它Package集成包含二进制文件的Package

包含了二进制文件的 Package 在集成时不需要任何特殊的设置,它们也是一个普通的 Package.product,和普通Package一样:

  1. 工程的Package dependencies添加依赖
  2. targetdependencies 里通过名字指定
dependencies: [
//  .package(url: "https://github.com/binxiao0604/HotpotPackage.git", .branch("master"))
.package(name:"HopotPackage",url: "https://gitee.com/guaizaizaiguai/HotpotPackage.git", .branch("master"))
],
targets: [
    .target(
        name: "HotpotPackageTest",
        dependencies: ["HopotPackage"]),
    
    .testTarget(
        name: "HotpotPackageTestTests",
        dependencies: ["HotpotPackageTest"]),
]

调用:

// HopotPackageTest.swift
open class HotpotPackageTest {
    public init() {}
    open func test() {
        print("HotpotPackageTest test function");
        HotpotPackage().test()
    }
}

//HotpotPackageTestTests.swift
func testExample() {
    // This is an example of a functional test case.
    // Use XCTAssert and related functions to verify your tests produce the correct
    // results.
    HotpotPackageTest().test()
}

输出:

HotpotPackageTest test function
XCFramework Cat Test
Test XC Framework

HotpotPackageTest demo

主工程引用

直接添加Package然后调用:

image.png

HotpotPackage().test()

输出:

XCFramework Cat Test
Test XC Framework

对于非xcframework形式的二进制,引入直接报错。必须为xcframework格式

image.png

对于非xcframework二进制:
.a,.dylib ->.framework -> . xcframework

其它

1.通过预处理命令区分编译环境
Package 可以通过 SPM 执行 swift build 进行编译,也可以通过生成 xcodeproj 通过 Xcode 进行编译,两者的编译环境并不相同,生成的可执行文件也不是同一个地址,所以可以通过 SWIFT_PACKAGE 区分编译环境:

#if SWIFT_PACKAGE
import Foundation
#endif

2.Xcode运行package
swift build 默认不会生成 packageName.xcodeproj这种 Xcode 可以直接打开的工程文件。可以通过swift package generate-xcodeproj命令行生成一个 .xcodeproj 文件, 然后就可以通过 Xcode 运行项目了。

SPM 与 Cocoapods 和 Carthage 对比

对比项 SPM Cocoapods Carthage
原理 Swift构建系统集成在一起,可以自动执行依赖项的下载,编译和链接过程 Cocoapods会将所有的依赖库都放到另一个名为Pods的项目中,然后让主项目依赖Pods项目 自动将第三方框架编程为Dynamic framework(动态库)
语言 SwiftCCXXOC SwiftCCXXOC OCSwift
兼容性 兼容 CocoaPodsCarthage 兼容CarthageSPM 兼容 CocoaPodsSPM
库支持力度 大部分支持,但少于CocoaPods 多,基本大部分都支持 大部分支持,但少于CocoaPods
复杂度
管理中心 没有统一管理的中心,没有更新中心服务器的文件索引这种耗时步骤 有统一管理的中心,有更新中心服务器的文件索引这种耗时步骤 没有统一管理的中心,没有更新中心服务器的文件索引这种耗时步骤
自动化 需要有一定目录格式 提供各个源管理仓库配置文件,更新仓库文件索引可能会很慢。 Carthage只会帮你把各个库下载到本地,具体的 Project 配置需要自己处理
侵入性
编译速度
源码 可见 可见 不可见
生态 不够成熟,还有很多待优化项,官方开发,Xcode 自集成 成熟
缓存 项目根目录的缓存,不同项目需要重新下载 除了项目根目录的缓存,还有本地缓存体系,不同工程会直接从本地copy。 项目根目录的缓存,不同项目需要重新下载

错误问题

1.contains mixed language source files; feature not supported
https://www.reddit.com/r/SwiftPM/comments/iay38g/build_c_and_swift_package/
1个Target只能支持swift或者oc不能混编。

2.Argument 'dependencies' must precede argument 'publicHeadersPath'
dependencies必须放在publicHeadersPath前面。

3.An unknown error occurred. SecureTransport error: connection closed via error (-1)

image.png

一般都是网络问题,建议开代理或者切换源。

4.An unknown server error occurred. Make sure a connection is established and the server is accessible.
一般都是网络问题,建议开代理或者切换源。

5.public headers directory path for 'SPMLibraryForOC' is invalid or not contained in the target
publicHeadersPath路径配置有问题

6.Failed to parse the manifest file
一般遇见这个错误代表Package.swift解析失败。

demo:
SPMTest
SwiftPackageByXcode
SPMLibraryForOCGitee版本(如果github连接有问题建议复制库到)码云。

参考:

SPM官网
Swift 论坛
Creating Swift Packages
WWDC 2018 SECTION 411
官方pdf文档
Swift packages: Resources and localization
Swift CI 持续集成
SPM GitHub 社区贡献
Trunk Snapshots / 快照
Swift Bug 跟踪
Swift 版本演化
Package.swift官方示例
https://developer.apple.com/forums/tags/wwdc20-10169
Creating a personal access token
github.com/apple/swift-evolution
https://blog.csdn.net/xinshou_caizhu/article/details/103325571
https://www.jianshu.com/p/ce49d8f32f77
https://juejin.cn/post/6844903619553132552
https://blog.csdn.net/sinat_35969632/article/details/108251008
https://blog.csdn.net/olsQ93038o99S/article/details/108612725
https://blog.csdn.net/weixin_39968592/article/details/111201874

你可能感兴趣的:(Swift Package Manager(SPM))