~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain-ios

1 collaborator
0
1
main
113 lines · 4.5 KB Raw History
# Development

## Requirements

- Xcode 26+
- [XcodeGen](https://github.com/yonaskolb/XcodeGen)
- [just](https://github.com/casey/just)
- [xcbeautify](https://github.com/cpisciotta/xcbeautify)
- [SwiftFormat](https://github.com/nicklockwood/SwiftFormat)
- [SwiftLint](https://github.com/realm/SwiftLint)

```bash
brew install xcodegen just xcbeautify swiftformat swiftlint
```

## Setup

Generate the Xcode project and open it:

```bash
just generate
open Grain.xcodeproj
```

If using a non-Xcode editor with SourceKit-LSP (e.g. Zed), run once after generating:

```bash
xcode-build-server config -scheme Grain -project Grain.xcodeproj
```

No local backend needed — `just sim` and `just device` both hit the production API at grain.social.

### Device builds

Device builds require an Apple Developer account. Create a `.env` in the repo root:

```
APPLE_TEAM_ID=YOUR_TEAM_ID
BUNDLE_ID=com.yourorg.grain
BUNDLE_NAME=Grain
```

Then re-run `just generate` before building.

Pass your device UDID directly or via an env var:

```bash
just device 00000000-0000000000000000   # explicit UDID
just device $iphonemax                  # via shell env var
```

Find your device UDID with `xcrun devicectl list devices`.

## Commands

```bash
just sim               # Build + install + launch on simulator (production API)
just sim-local         # Build + install + launch on simulator (local/dev API)
just sim-next          # Build + install + launch on simulator (next.grain.social)
just sim-fresh         # Same as `just sim`, but uninstalls first (wipes app sandbox)
just sim-local-fresh   # Same as `just sim-local`, but uninstalls first
just device DEVICE_ID  # Build + install to a plugged-in iOS device
just test              # Run tests (iPhone 17 Pro Max simulator)
just generate          # Regenerate Xcode project from project.yml
just basemap-style     # Regenerate the MapLibre style assets (needs node + npm)
just format-fix        # Fix formatting in-place
just lint-fix          # Fix lint violations
just release           # Bump build, archive, upload to App Store Connect
```

The `*-fresh` variants `xcrun simctl uninstall` your app's bundle id before installing, which clears URLCache, UserDefaults, on-disk caches (FeedCache, LabelDefinitionsCache), and Documents/Caches. Pass `SIM_UDID=<udid>` to target a specific simulator (otherwise uses the booted one).

The location screens draw grain's own [Protomaps](https://protomaps.com)
basemap through MapLibre — the same tiles as grain.social, served from
`tiles.grain.social`, with no API key. The style ships as two bundle
resources under `Grain/Resources/Basemap`, generated from the same theme
package the web uses by `tools/basemap`; the output is committed, so a build
needs neither node nor npm. Label fonts and sprite icons come from Protomaps'
public asset host until the tile Worker serves them.

> **Note:** The Xcode project is generated from `project.yml` — run `just generate` after adding or removing Swift files, or after pulling changes that touch `project.yml`.

## Releasing

Builds ship to TestFlight from Xcode Cloud. Because `Grain.xcodeproj` is XcodeGen
output and isn't committed, `ci_scripts/ci_post_clone.sh` runs on every Xcode
Cloud build to pull git-lfs assets, generate the project, and copy the committed
package pins into place.

Build numbers are assigned by Xcode Cloud, which overwrites `CFBundleVersion`
with its own run counter at archive time — `CURRENT_PROJECT_VERSION` in
`project.yml` is ignored on CI. They only need to be unique and increasing
within a marketing version, so a new train starting at a low number is fine.

`MARKETING_VERSION` is still bumped by hand. Once a marketing version is
approved on the App Store that train closes, and further uploads against it fail
with `90186`/`90062` — so raise it in `project.yml` before releasing against an
already-approved version.

`just release` still archives and uploads from your Mac if you need it, but
don't mix the two: it uploads `project.yml`'s `CURRENT_PROJECT_VERSION`, which
is far ahead of Xcode Cloud's run counter, and once a higher build number exists
on the current train Xcode Cloud's next run would be rejected as not increasing.

When SPM dependencies change, refresh the committed pins — Xcode Cloud disables
automatic package resolution, and the live file lives inside the gitignored
`.xcodeproj`:

```bash
just generate
xcodebuild -resolvePackageDependencies -project Grain.xcodeproj -scheme Grain
cp Grain.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved .
```