Skip to main content

Project and dependencies

Reading an Xcode project from a terminal usually means opening Xcode. These commands answer the same questions without it (what's in here, how does it build, what does it depend on) and, for dependencies, change the answer.

What's in the project

$ sweetpad project info
SweetpadCIApp (project)
path: /path/to/SweetpadCIApp.xcodeproj
targets:
SweetpadCIApp
SweetpadCIAppTests
SweetpadCIMac
configurations:
Debug
Release
schemes:
SweetpadCIApp
SweetpadCIMac

That's the orientation command for a repo you didn't set up. sweetpad scheme list prints just the schemes, marking the one currently selected.

To start a project rather than inspect one, sweetpad project new scaffolds a minimal SwiftUI app. See Starting a project.

Build settings

sweetpad settings show prints the fully resolved build settings for the scheme: the same values xcodebuild will use, after every xcconfig, target, and project layer has been folded together:

sweetpad settings show
sweetpad settings show --target SweetpadCIAppTests

For one value, --key prints it bare, with no label, ready to capture:

$ sweetpad settings show --key PRODUCT_BUNDLE_IDENTIFIER
dev.sweetpad.ci.app
BUNDLE_ID=$(sweetpad settings show --key PRODUCT_BUNDLE_IDENTIFIER)

That's the resolved value. To see what your project file actually stores, before Xcode's defaults and any xcconfig get layered on, use the plumbing command below.

Swift Package dependencies

sweetpad dependency, or dep for short, reads and edits the Swift packages an Xcode project depends on, including which product is linked into which target:

$ sweetpad dependency list
keychain-swift (remote)
https://github.com/evgenyneu/keychain-swift
requested: branch master
locked: master @ 5e1b02b
link: KeychainSwift → IceCubesApp
link: KeychainSwift → IceCubesNotifications

sfsafesymbols (remote)
https://github.com/SFSafeSymbols/SFSafeSymbols
requested: from 4.1.1
locked: 4.1.1
link: SFSafeSymbols → IceCubesApp

The requested line is what your project asks for; locked is what Package.resolved pinned. A gap between them is usually the answer to "why is CI building a different version".

Adding a package

sweetpad dependency add https://github.com/apple/swift-collections --from 1.1.0

add resolves the package, then links a product into a target. Leave --product or --target out and you'll be prompted after the resolve, once SweetPad knows what the package offers:

sweetpad dependency add https://github.com/apple/swift-collections \
--from 1.1.0 --product Collections --target MyApp

Both flags repeat, for a package whose products go into several targets.

The requirement flags mirror SwiftPM's own:

FlagSwiftPM equivalent
--from 1.1.0from: "1.1.0", up to the next major.
--up-to-next-minor-from 1.1.0.upToNextMinor(from:)
--exact 1.1.0exact: "1.1.0"
--branch mainbranch: "main"
--revision <sha>revision: "<sha>"
--from 1.1.0 --to 2.0.0A half-open 1.1.0..<2.0.0 range.

A local package works too: pass a directory containing a Package.swift instead of a URL.

Updating and removing

update with no requirement flags re-resolves to the latest versions your requirements allow. With one, it rewrites the requirement (a bump, a pin, or a downgrade) and then re-resolves:

sweetpad dependency update # everything
sweetpad dependency update swift-collections # one package
sweetpad dependency update swift-collections --exact 1.1.4

remove drops a whole package, or just unlinks one product from one target:

sweetpad dependency remove swift-collections
sweetpad dependency remove swift-collections --product Collections --target MyAppTests

sweetpad dependency resolve just refreshes Package.resolved without changing anything else, which is the command for a fresh clone or a CI job. Every mutating command resolves on the way out; --no-resolve skips that when you're making several changes in a row.

warning

If your .xcodeproj is generated by XcodeGen or Tuist, editing it directly gets overwritten on the next regenerate. SweetPad refuses these edits unless you pass --force. See Tuist and XcodeGen; normally you want to edit the spec instead.

The pbxproj plumbing

sweetpad pbxproj edits project.pbxproj directly. It exists because the porcelain commands above can't express everything an Xcode project can hold, and because agents and scripts sometimes need to make a precise change rather than a convenient one.

It's genuinely low-level. Reach for it when nothing else fits:

GroupWhat it operates on
pbxproj settingsStored buildSettings entries, per configuration: show, set, unset.
pbxproj folderA target's synchronized source folders: list, add, remove.
pbxproj membershipPer-file target membership: list, add, remove, exclude, include.
pbxproj filerefFile references: the objects saying a file exists at all.
pbxproj groupThe navigator group tree: where a file appears in Xcode.
pbxproj resolveSemantic merge of a conflicted .pbxproj. See Git merge drivers.

Two distinctions the group names encode, and which trip people up in Xcode too:

A file reference is not membership. A file can exist in the project and be built by no target. fileref creates the reference; membership decides what compiles it.

A group is not a folder. The navigator tree is where a file appears in Xcode's sidebar, which is a separate question from where it lives on disk or what builds it. group moves things in the sidebar and nothing else.

pbxproj settings show is the counterpart to settings show: it prints what the project file stores, per configuration, rather than what those settings resolve to:

$ sweetpad pbxproj settings show
# project — Debug
ALWAYS_SEARCH_USER_PATHS = NO
CLANG_ANALYZER_NONNULL = YES
CLANG_CXX_LANGUAGE_STANDARD = gnu++14

The same generated-project guard applies here: these commands refuse to edit an XcodeGen or Tuist project without --force. See Tuist and XcodeGen.