Skip to main content
Version: Next

An iOS app and a CocoaPods library

You will archive an app and upload it to App Store Connect. You will also publish a pod to trunk. Four Apple file formats carry a version between them.

The layout

pods/core/AcmeCore.podspec the library, published to CocoaPods trunk
apps/ios/Podfile the app's dependencies, including AcmeCore
apps/ios/App/Info.plist CFBundleShortVersionString and CFBundleVersion
apps/ios/Acme.xcodeproj/project.pbxproj MARKETING_VERSION and CURRENT_PROJECT_VERSION
dispat.json

The configuration

dispat.json
{
"scripts": {
"stamp": "dispat writer App/Info.plist Acme.xcodeproj/project.pbxproj --set-version \"$DISPAT_NEW_VERSION\" --set-build \"$GITHUB_RUN_NUMBER\"",
"archive": "xcodebuild archive -scheme App -archivePath build/App.xcarchive",
"upload": "xcrun altool --upload-app -f build/App.ipa -u \"$APPLE_ID\" -p \"$APP_PASSWORD\"",
"lint": "pod lib lint",
"trunk": "pod trunk push AcmeCore.podspec"
},
"spaces": {
"pods": {
"path": "pods",
"flow": {"build": "lint", "publish": "trunk"},
"autoVersion": {"enabled": true, "range": "~> {version}"}
},
"apps": {
"path": "apps",
"flow": {"beforeBuild": "stamp", "build": "archive", "publish": "upload"},
"autoVersion": {"enabled": true, "manifests": "all", "range": "~> {version}"}
}
},
"packages": {
"core": {"manifestNames": ["AcmeCore"]}
}
}

Set range: "~> {version}" because CocoaPods writes optimistic requirements the Ruby way. It does not use the npm way.

Use manifestNames to connect the pod name AcmeCore to the folder called core. dispat then recognises a pod 'AcmeCore' line anywhere as this package.

The stamp script exists because of where Xcode keeps versions. autoVersion writes a package's own version into the manifests directly in its folder, but an iOS project keeps Info.plist and project.pbxproj one level deeper in App/ and Acme.xcodeproj/. Call dispat writer in beforeBuild to write both files along with the build number.

A release

$ git commit -m "feat(core)^: background refresh"
$ dispat
12:49:43 INF release started root=.
12:49:43 INF ● changed baselineFromInitials=true bump=minor channel=stable dueToProviders=[] ownCommits=1 package=core reason=direct space=pods version="1.2.0 -> 1.3.0"
12:49:43 INF ● changed baselineFromInitials=true bump=patch channel=stable dependsOn=["core"] dueToProviders=["core"] ownCommits=0 package=ios reason="propagated from core" space=apps version="0.4.1 -> 0.4.2"
12:49:43 INF release plan ready held=0 packages=2 releasing=2
12:49:43 INF manifest reconciled manifest=AcmeCore.podspec package=core ranges=0 stage=version version=1.3.0 versionWritten=true
12:49:43 INF version succeeded package=core stage=version version=1.3.0
12:49:43 INF build started package=core stage=build version=1.3.0
12:49:43 INF build succeeded package=core stage=build version=1.3.0
12:49:43 INF publish started package=core stage=publish version=1.3.0
12:49:43 INF manifest reconciled manifest=Podfile package=ios ranges=1 stage=version version=0.4.2 versionWritten=false
12:49:43 INF version succeeded package=ios stage=version version=0.4.2
12:49:43 INF App/Info.plist package=ios stage=build version=0.4.2
12:49:43 INF version written package=ios stage=build version=0.4.2
12:49:43 INF build number written package=ios stage=build version=0.4.2
12:49:43 INF Acme.xcodeproj/project.pbxproj package=ios stage=build version=0.4.2
12:49:43 INF version written package=ios stage=build version=0.4.2
12:49:43 INF build number written package=ios stage=build version=0.4.2
12:49:43 INF 2 manifest(s): 0 applied, 0 skipped, 0 missing package=ios stage=build version=0.4.2
12:49:43 INF build started package=ios stage=build version=0.4.2
12:49:43 INF published package=core stage=publish tag=core@1.3.0 version=1.3.0
12:49:43 INF build succeeded package=ios stage=build version=0.4.2
12:49:43 INF publish started package=ios stage=publish version=0.4.2
12:49:43 INF published package=ios stage=publish tag=ios@0.4.2 version=0.4.2
12:49:43 INF done cancelled=0 failed=0 held=0 published=2 skipped=0 took=0.6s unchanged=0

The pod publishes first. dispat reconciles the app's Podfile to ~> 1.3.0. Both project files end up carrying 0.4.2 with CFBundleVersion and CURRENT_PROJECT_VERSION set to the run number:

<key>CFBundleShortVersionString</key>
<string>0.4.2</string>
<key>CFBundleVersion</key>
<string>87</string>

What dispat reads and writes

$ dispat scanner
apps/ios/Acme.xcodeproj/project.pbxproj xcode com.acme.app@0.4.1 build 41
apps/ios/App/Info.plist plist com.acme.app@0.4.1 build 41
apps/ios/Podfile cocoapods
dependencies AcmeCore ~> 1.2.0
dependencies Alamofire ~> 5.9
pods/core/AcmeCore.podspec cocoapods AcmeCore@1.2.0
dependencies Alamofire ~> 5.9
4 manifest(s), 3 dependency declaration(s)
FileIdentityVersionBuild counterDependencies
Info.plistCFBundleIdentifierCFBundleShortVersionStringCFBundleVersionnone
project.pbxprojPRODUCT_BUNDLE_IDENTIFIERMARKETING_VERSIONCURRENT_PROJECT_VERSIONnone
Podfilenonenonenoneevery pod line
*.podspecnameversionnoneevery s.dependency

Writes to project.pbxproj reach every build configuration so Debug and Release do not drift apart. Point an autoVersion.replace rule at the file that really holds the number when a value defers to something else. dispat skips these values rather than flattening them. For example, $(MARKETING_VERSION) in a plist points at the build setting, and a podspec saying s.version = Acme::VERSION points at a Ruby constant.

dispat reports a pod inside a test target as a dev dependency. Pods pinned by :git or :path name a place rather than a version. dispat never rewrites them.

Worth knowing

  • CFBundleVersion must increase for every upload, even when the marketing version does not change. That is the whole reason it is separate. The stamp script uses the CI run number rather than anything derived from the release.
  • A version write never moves a build counter, and --set-build never moves a version. You get two numbers and two writes.
  • pod trunk push needs a session. Put this in flow.login so it happens once per space.
  • App Store Connect is asynchronous. The upload succeeding is not the release being live. The tag records what you shipped, and review is downstream of it.
  • Swift Package Manager needs no manifest support here. Package.swift resolves versions from git tags. The tag dispat writes is the whole story, exactly as with Go.

See also