Spm
Swift Package Manager
Intro
Versatile yet out of beta package manager. Great support in built in Xcode IDE.
Still initial adoption pain back in 2020, 2021. I think so past two years it has matured quite a bit.
Setup
You can utilized the Xcode SPM functionality in built with version 11
Great for linking different packages and frameworks to Xcode with first class support.
No messy .xcodeproj / .xcworkspace file
Create
swift package
apple docs | swift package - create a standalone package xcode
Sample Package.swift
let package = Package(
name: "test-apple",
platforms: [
.iOS(.v16),
.tvOS(.v16)
],
products: [
.library(
name: "DummyUnit",
targets: ["DummyUnit"]),
],
dependencies: [
.package(url: "git@github.com:repo_name/test.git",
branch: "important-events")
],
targets: [
.target(
name: "DummyUnit",
dependencies: [],
path: "Sources/product_path"
),
]
)
Directory
Delete local build cache
If you want to delete the cache via CLI
swift package purge-cache
Note - this command is only applicable from the $pwd where there is a Package.swift file present.
Package.resolved file
If it's a traditional .xcodeProj we don't have the Package.swift / .resolved file either. It is hidden under the project file. You can right click and browse its contents, below is the location of it.
.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
Options
Resolve Package Versions
This tries to take latest pull - origin kind of methodology. So if 9.3.3 tag had been published before with hash ending at 352EA and now the remote server deleted it and again push few commits and tagged it 9.3.3 it will have the latest commit with new hash like 231EB. Usually its recommended to not do this kind of tagging or releasing & just follow SemVer but sometimes you can't choose the cross team you're working with.
Digress: I wish I could be picky enough like Linus Torvalds, only liking to work with people I respect.
Reset Package Cache
This basically does a soft nuke of the cache and tries to pull it again if the package already had cache setup.
Update Package versions
This command will update the package depending on the ruleset defined in SPM with Semantic Versioning Major.Minor.Patch
So 9.3.3 has minor versions allowed in update rules could update to 9.x.x
Cache
SwiftPM cache
Cache directory for SPM
~/Library/Caches/org.swift.swiftpm/
Install size
These store the actual repositories of downloaded or cloned dependencies.
SwiftPM Metadata
This location stores the Repositories resolved meta data. You need to delete the meta data if some repository made a change on the same tag and couldn't even follow Semantic Versioning to make sure that the new change isn't propagated everywhere which makes / breaks the whole dependency chain.
~/Library/org.swift.swiftpm/security/fingerprints
swift pm | ReleaseNotes/5.4.md#package-dependency-caching
Delete Cache
Full Swift Package manager cache directory
rm -rf ~/Library/Caches/org.swift.swiftpm
rm -rf ~/Library/org.swift.swiftpm
Or a single repository cache
To reset the cache for a single package:
- Navigate to ~/Library/Caches/org.swift.swiftpm/repositories and deleting the folder and lock file related to the package
- Then, in Xcode, run File-->Swift Packages-->Reset Package Caches
## Dependency
Adding a branch as a dependency or a tag.
```swift
dependencies: [
.package(url: "git@github.com:company-player/dracula.git", branch: "br-logging"),
.package(url: "git@github.com:company-player/dracula.git", exact: "0.3.0"),
]
Similar SPM | Xcode cache issue
Bundling
A note with Swift package manager bundling directories.
Swift Package Manager world, bundling basically boils down to the following:
Bundle.modulereturns the bundle relative to the call site. so if you call that inFrameworkCore/AppInfo.swiftit will return theFrameworkCorebundle since that file and type are associated with that bundle. if you call it inFrameworkCoreTestKit/Mock.swiftit will return the test kit bundle since that's where that file lives. so on and so forth- if you call
Bundle.main, that will return the "product" or the bundle containing the current executable. you generally don't want to call this anymore unless you know you want the app layer bundle
Fetching
latest
You have a dependency pointed to specific branch and you push some commits on the dependency's branch. But Xcode and SPM would always use the cache version of that dependency. Since we didn't go for SemVer approach and just a branch SPM and xcode isn't smart enough to automatically pull in latest or fetch origin. Unless you nuke the cache and SPM manifest metadata with git commits for every package dependency located in ~/Library directory.
You can avoid that hassle by just updating your packages and invalidating previous cache and Xcode / SPM is smart enough to abide by those Semver rules or just update to the latest commit on the specified branch.
Note: Manually selecting the library and right clicking to select option Update doesn't update it for me Xcode 15 beta 6 with package dependency defined using a branch: "branch_name"
Updating all packages
You can do update by following this option in the menu bar.
Xcode -> File ->
Packages ->
Update to Latest Package versions
Updating individual package
You can also update a specific version on a package dependency by right clicking on it and updating specifically.
Cross Platform
// Package.swift
#if !os(Windows)
dependencies.append(.package(url: "https://github.com/danger/swift.git", from: "3.12.1"))
targets.append(.target(name: "DangerDeps", dependencies: [.product(name: "Danger", package: "swift")]))
products.append(.library(name: "DangerDeps", type: .dynamic, targets: ["DangerDeps"]))
#endif
platform-specific-code-in-swift-packages
Migrating to SPM
migrating-to-spm-from-mix-of-embedded-frameworks-and-static-libraries
Moved to directories Sources and Tests
update the info.plist files location or build scripts. Make sure its building properly.
Then initialize the repo with SPM on root of the project.
swift package init --type library
Find checksum of your framework zip binary
swift package compute-checksum framework_name.xcframework.zip
convert Fat framework to Xcframework
medium | convert to universal framework
Swift ObjectiveC Swift Mixed library
SO | Post
Objc C library SPM target include header
Maybe I need to set my library to type .dynamic in order to support linking properly. Checkout other project which can help us identify why its not working.
binary_xcframework
Create binary xcframework out of Swift package project.
SO | create SPM package into xcframework
SPM Errors
Notes
Xcode should be quit before deleting the references and cache of SPM. Since Xcode reactively tries to resolve those dependency midway while you're trying to resolve it manually.
Sometimes regenerating your SSH keys on your dev machine is helpful to isolate that permission issues, but since Xcode 14.1, it has been bubbling up better errors to the developer to show what failed and what went wrong.
This script from a stackoverflow user also goes through selectively deleting cache from Derived Data and SPM cache manifest files which is more efficient than nuking the whole cache as its more time consuming and resource intensive.
Relevant thread which is tracked on swift forum
Package Registry
what is swift package registry
Currently only jfrog is supporting this and Github promised to support in 2019.
Exposing Library & Target
When working with Swift Package managers you need to expose every folder as a different library in order to import them in the project itself. Swift Packages treat every directory or folder independent and you can't just directly access them if they are out of the default scope of Package_Name -> Sources library package name.
products: [
.library(
name: "DummyUnit",
targets: ["DummyUnit"]),
],
targets: [
.target(
name: "DummyUnit",
dependencies: [.product(name: "testLib",
package: "product_name")],
path: "Sources/product_path"
),
]
In order to access the directory as a library in your codebase, you would have to add dependencies if they are seperate repositories.
To import just use the following syntax as long as the library builds correctly and is embedded into the project product target in General Settings -> Frameworks, libraries and Embedded Content
import DummyUnit
For more information on Frameworks refer my mind map docs
Local Package dependency
You can point a swift package to local option in order to do faster prototyping instead of waiting for xcode to close / fetch / resolve SPM package directory and then make it usable to edit a file.
+1 Less .xcodeproj xcworkspace headaches.
Local dependency path in package.swift url takes both local and remote paths in its initializer.
dependencies: [
// Local Package
.package(url: "/Users/ksave/git/cloud/packageName", branch: "28-events")
.package(url: "file:///Users/ksave/git/cloud/packageName", from: "6.6.6")
// Remote package
.package(url: "git@github.com:companyName/packageName.git", branch: "21-events")
],
Change set in .resolved file
"identity" : "dependency_name",
- "kind" : "remoteSourceControl",
- "location" : "git@github.com:org-name/dependency_name.git",
+ "kind" : "localSourceControl",
+ "location" : "/Users/username/git/cloud/dependency_name",
Sometimes it gives out an error or warning as follows if you have both local dependency and cloud dependency added as a package in Swift PM.
'project-parent-package-name' dependency on 'git@github.com:source/package_name_3.git' conflicts with dependency on '/Users/userName/git/cloud/package_name_3' which has the same identity 'package_name_3'. this will be escalated to an error in future versions of SwiftPM.
Add local SPM package doesn't work in Pure Package.swift project opened in Xcode 16 beta 3. But I could add local package in .xcworkspace or .xcproj file.
Another thing
For some reason my local package gets added via Package.swift with absolute URL local path and still doesn't reflect the right change-set. But if I add it via add package -> local GUI option on a .xcodeproj file in Xcode GUI Package Dependencies It reflects the local change-set appropriately. Maybe a cache issue? Don't know and don't want to bother learning more about it.
An unknown error not found (-1) issue
KMP_interop
android KMP - iOS_interop could be used in similar fashion for linking local .xcframework or .lib / script to make development easier. I haven't tried this yet but I reckon most of the Android KMP or flutter dev folks would utilize similar approach to integrating iOS / apple ecosystem.
Pitfalls
Some pitfalls of using SPM and Build Configuration in Xcode
Unexpected Duplicate tasks
Multiple commands produce framework libraries. Probably deleting SPM cache and metadata. also .resolved SPM project helps me to get over this build error. Reset Package graph and Resolve Packages as well.
Circular dependency
Also experienced it in Carthage build command
SO resolve circular dependency swift PM
swift forums circular dependency
SO | resolve circular dependency
Best option is to refactor the code or move dependency one way rather than two ways. Or move it to third package and adopt one way dependencies on both A & B dependencies.
Timeline
Supporting Binary dependencies in SPM
Added in Swift 5.3
So carthage and cocoapods were used in lot of projects if your dependencies had images, data files, close source code, binaries.
Artifactory flavors
We can customize our endpoints depending on certain build conditions which we can source via ENV variables.
let domain = ProcessInfo.processInfo
.environment["PARTNERS"] != nil ?
"partners.artifactory.company.com"
: "artifactory.company.com"
let package = Package(
name: "sdkManagerPackage",
platforms: [.iOS(.v15), .tvOS(.v15)],
products: [
.library(
name: "sdkManagerPackage",
targets: ["sdkmanager", "clientmanager", "configservice"]
)
],
targets: [
.binaryTarget(
name: "sdkmanager",
url: "https://\(domain)/artifactory/dtm-libs-releases/com/company/mobile/sdk-manager-xcframework/0.7.20/sdk-manager-xcframework-0.7.20.zip",
checksum: "saf237sfasa57f7"
),
],
)
Resources
https://www.youtube.com/watch?v=QmBZ9wJguS4
SwiftPM: Same sources, multiple targets