---
title: "The Bit After the Code"
date: "2026-06-13"
publish_date: "2026-09-02"
excerpt: "Writing a recipe-saving iOS app took weeks. Getting it onto my mum's iPad took one very long Saturday afternoon, seven fastlane failures, and a surprising amount of App Store Connect API."
tags: ["ios", "fastlane", "testflight", "app-store", "swift", "cognito"]
author: "Martin Harris"
canonical: "https://blog.nakomis.com/2026-06-13-the-bit-after-the-code"
approved: true
---

# The Bit After the Code

![A smartphone on a wooden desk with a warm coffee mug beside it, showing a clean recipe app interface, soft window light, shallow depth of field, editorial photography style](images/the-bit-after-the-code-1.png)

The app was done. Or done enough — a Share Extension that plucks recipes off web pages, an AI extraction fallback for the ones that don't publish structured data, a SwiftUI list view, DynamoDB on the backend, Cognito for auth. My wife Yvonne had been waiting for it for months. All I had to do was get it onto her iPad.

That took an entire Saturday afternoon. In the end I sideloaded it directly via Xcode mid-morning so she could actually use it, then spent the rest of the day fighting the deployment pipeline so future builds would go out properly. She was happily saving recipes by lunch. I was not.

This is a post about the bit after the code: TestFlight, App Store Connect, code signing, screenshot requirements, and `fastlane` saying no in more ways than I'd care to count.

## fastlane fails almost immediately

I already had fastlane set up. The `beta` lane builds the app and uploads to TestFlight. The build number is derived from `git rev-list HEAD --count` — monotonically increasing, no API calls, good enough for a single-branch workflow. (If you build from multiple branches, be aware this can jump backwards; a CI-managed counter or per-branch offset keeps TestFlight numbers reliably monotonic.) I ran it.

```
[!] Git repository is dirty!
```

The `ignored/` directory, containing the Evernote import scripts I'd never tracked, was showing as untracked. Not dirty exactly, but fastlane's `ensure_git_status_clean` counts untracked files. Fair enough. I added it to `.gitignore`.

Ran it again. This time: `ios/fastlane/README.md` and `ios/fastlane/report.xml` — auto-generated by fastlane itself. Added those too. Then on the next run, `increment_build_number` had run on the previous failed attempt and left the tracked `Info.plist` files dirty. Reset them with `git checkout -- Info.plist`.

The underlying lesson: audit your `.gitignore` before the first run, and add a `git checkout -- Info.plist` step as a preflight or cleanup action so reruns start clean. Committing the whole `fastlane/` directory up front is the other approach — it avoids chasing generated files one at a time.

Eventually it built.

## The archive succeeds. The export doesn't.

```
error: exportArchive No profiles for 'com.nakomis.recipator' were found
```

The project uses automatic signing. For debug builds in Xcode, automatic signing just works — partly because Xcode is running interactively, signed in with a developer account. For an `app-store` export from the command line, it needs permission to create or fetch a distribution profile. The fix is one argument, and it's worth adding to both the build and export phases to cover all the places provisioning may need to update:

```ruby
build_app(
  scheme:        "Recipator (Production)",
  export_method: "app-store",
  xcargs:        "-allowProvisioningUpdates",
  export_xcargs: "-allowProvisioningUpdates",
)
```

## The upload needs an API key, not a password

The IPA built. The upload failed:

```
Missing password for user martin@nakomis.com, and running in non-interactive shell
```

Fastlane was trying to authenticate with my Apple ID and password — which works fine interactively, but not in a background process. The reliable solution is an App Store Connect API key: generated in App Store Connect under Users and Access → Keys (App Store Connect API), downloaded as a `.p8` file, no passwords, no 2FA, works in CI. Password-based auth is brittle and generally unsuitable in CI due to 2FA prompts; App Store Connect API keys are the reliable path. The `.p8` file itself should never be committed to source control; scope the key to the minimum role needed.

The simplest approach is to use the key file exactly as downloaded from Apple — `app_store_connect_api_key` can read the `.p8` content directly without any encoding. I wanted a keychain-backed local store that would also fall back to environment variables for CI, so I stashed three pieces (Key ID, Issuer ID, raw `.p8` content) in the macOS keychain using `security add-generic-password`. The Key ID and Issuer ID aren't secrets, but stashing them in the "password" field of their own keychain items means they can be pulled out with the same `-w` flag as the actual key content — the naming is just for retrieval convenience, not because they're passwords:

```ruby
asc_key_id      = ENV["ASC_KEY_ID"]      || `security find-generic-password -s app-store-connect -a key-id      -w`.strip
asc_issuer_id   = ENV["ASC_ISSUER_ID"]   || `security find-generic-password -s app-store-connect -a issuer-id   -w`.strip
asc_key_content = ENV["ASC_KEY_CONTENT"] || `security find-generic-password -s app-store-connect -a key-content -w`.strip
```

Those values then get passed to the upload action:

```ruby
upload_to_testflight(
  api_key: app_store_connect_api_key(
    key_id:      asc_key_id,
    issuer_id:   asc_issuer_id,
    key_content: asc_key_content,
  )
)
```

If you store the key content as base64 instead of raw PEM, add `is_key_content_base64: true` to the `app_store_connect_api_key` call — without it, fastlane will fail to parse the key.

## Apple rejects the upload for orientation reasons

The upload worked. Apple's servers chewed on it for a minute and then:

```
Invalid bundle. The orientations
"UIInterfaceOrientationPortrait, UIInterfaceOrientationLandscapeLeft,
UIInterfaceOrientationLandscapeRight" were provided for the
UISupportedInterfaceOrientations Info.plist key, but you need to include all of
"UIInterfaceOrientationPortrait, UIInterfaceOrientationPortraitUpsideDown,
UIInterfaceOrientationLandscapeLeft, UIInterfaceOrientationLandscapeRight"
to support iPad multitasking.
```

If your iPad app supports multitasking (i.e. `UIRequiresFullScreen` is not set to `true`), you must declare all four orientations, including upside-down portrait. The build had actually warned about this (`All interface orientations must be supported unless the app requires full screen`) but warnings in a long build log are easy to miss. One block added under the iPad target's `info:` dictionary in `project.yml` (Apple's device modifiers are lowercase — `~ipad`, not `~iPad`, or the key is silently ignored):

```yaml
UISupportedInterfaceOrientations~ipad:
  - UIInterfaceOrientationPortrait
  - UIInterfaceOrientationPortraitUpsideDown
  - UIInterfaceOrientationLandscapeLeft
  - UIInterfaceOrientationLandscapeRight
```

XcodeGen regenerated, rebuilt, re-uploaded.

## Skipping the encryption compliance dialog

While on the subject of things that cause unnecessary upload round-trips: the encryption compliance dialog that appeared next is avoidable. Adding `ITSAppUsesNonExemptEncryption = false` to Info.plist eliminates it permanently — but set this only if you use no custom encryption beyond Apple's TLS/ATS. Recipator only uses HTTPS, so this is accurate, and future builds skip the dialog entirely. If you later add non-exempt cryptography (CryptoKit with custom schemes, for example), you'll need to remove or update this key to stay compliant.

![A macOS terminal window showing a long sequence of fastlane build steps, green success checkmarks and red error lines alternating, dark theme, even lighting](images/the-bit-after-the-code-2.png)

## A sign-out bug that was actually a session-sharing bug

Waiting for Apple's servers to process each upload left me staring at the app — which is exactly when you notice the edge cases you'd been ignoring. After signing out, tapping "Sign In" would skip the login form and authenticate automatically. Cognito's hosted UI was reusing a session cookie from Safari.

The fix sounds like it would break autofill, but doesn't. Setting `prefersEphemeralWebBrowserSession = true` on `ASWebAuthenticationSession` means the browser session doesn't share cookies with Safari — so Cognito can't auto-authenticate. But iOS Password AutoFill operates at the OS level, independently of browser session sharing. The credential sheet still appears; the user still gets one-tap autofill. They just can't be signed in without explicitly doing so. The trade-off is you won't get Safari SSO or managed-account cookie reuse — for a consumer app this was exactly what we wanted, but if you're building for enterprise SSO flows it may not be.

The previous approach — calling Cognito's `/logout` endpoint to clear the server-side session — was showing a "Recipator Wants to Use login.nakomis.com" system prompt immediately on sign-out, then hitting Cognito's "Invalid request" error because the `logout_uri` was wrong. Simpler to eliminate it entirely.

## Screenshots for a phone that doesn't exist yet

App Store submissions require screenshots at specific pixel dimensions. The iPhone 17 Pro Max simulator (the largest available at the time of writing) produces screenshots at 1320×2868 pixels (6.9"). App Store Connect has accepted the 6.9" slot directly since the 16 Pro Max shipped in 2024, so those can go up as-is. What actually caught me out was the older size classes: App Store Connect still lists 6.7" and 6.5" as valid submission slots for listings that don't supply a dedicated 6.9" set, and I wanted mine covered too. The aspect ratios are nearly identical, so a straight resize to 1284×2778 (6.7") works without cropping or distortion, and as of June 2026 uploading the 6.7" size also satisfies the 6.5" slot. One `sips` command, built into macOS:

```bash
sips -z 2778 1284 iphone-screenshot.png --out iphone-screenshot-6_7.png
```

These dimensions and slot requirements are current as of mid-2026 — always verify against Apple's current Screenshot specifications in the App Store Connect Help before each submission, since Apple adds and retires size classes as new devices ship. If you want to remove the manual step entirely, fastlane's `snapshot` action captures screenshots on the required simulators at the correct sizes; `deliver` then uploads them.

For the iPad, the latest 13-inch iPad Pro simulator produces 2064×2752, which Apple does accept for the 13" slot.

## What I actually learned

The things the documentation doesn't quite tell you:

- **`-allowProvisioningUpdates`** is the missing flag for command-line `app-store` exports with automatic signing. Add it to both `xcargs` and `export_xcargs` to cover all the places provisioning may need to update.
- **App Store Connect API keys** are the reliable path for CI. Password-based auth is brittle and generally unsuitable in CI due to 2FA. Keep the `.p8` out of source control and scope the key to the minimum role needed. If you store the key content as base64, set `is_key_content_base64: true`.
- **If you support iPad multitasking, you must declare all four orientations** — including upside-down portrait — or the upload is rejected outright.
- **`ITSAppUsesNonExemptEncryption = false`** in Info.plist eliminates the compliance dialog permanently — but only set it if you use no custom encryption beyond Apple's TLS/ATS, and update it if that changes.
- **`prefersEphemeralWebBrowserSession = true`** prevents Cognito SSO auto-login without breaking Password AutoFill — the two mechanisms are independent. You do lose Safari SSO, which matters for enterprise flows but not consumer apps.
- **6.9" screenshots are accepted directly, but older slots still need covering** — resize to 1284×2778 (6.7") for listings without a dedicated 6.9" set. As of June 2026, the 6.7" size also satisfies the 6.5" slot, but check Apple's current Screenshot specifications before each submission.

By the time the submission went in, it was evening. Yvonne had been using the app all afternoon. The build sitting in TestFlight review felt almost anticlimactic by that point.

---

*Martin Harris connects cloud infrastructure to the real world. If you've ever spent longer getting an app onto your wife's iPad than you did writing it, you're in the right place.*

{{donate}}