# Knotless for iOS A native app for reading knotless: inbox, pulls and their diffs, repositories and profiles. SwiftUI, iOS 18 and up, no dependencies. It reads knotless.dev's XRPC queries and writes through its procedures. The project is generated, so edit `project.yml`, not an `.xcodeproj`: brew install xcodegen ios/run.sh # builds and runs in the simulator KNOTLESS_ACTOR= ios/run.sh inbox ## Signing in The app signs in through a browser, ending with the session a page's cookie would hold, kept in the keychain and sent as that cookie (`docs/design/serve.md`, Logging in). A debug build can also ask as an account without signing in: with goat logged in and `KNOTLESS_ACTOR` set, `run.sh` mints an hour's service token for each method into `build/.tokens.json` and hands the app its path. That code isn't in a release build. ## TestFlight Raise `CURRENT_PROJECT_VERSION` in `project.yml` for each upload, then: cd ios && xcodegen generate xcodebuild archive -project Knotless.xcodeproj -scheme Knotless -configuration Release \ -destination 'generic/platform=iOS' -archivePath build/Knotless.xcarchive -allowProvisioningUpdates xcodebuild -exportArchive -archivePath build/Knotless.xcarchive -exportPath build/export \ -exportOptionsPlist ExportOptions.plist -allowProvisioningUpdates `build/export/Knotless.ipa` goes up with Xcode's Organizer or Transporter. Signing in needs `webcredentials` on Apple's CDN (`https://app-site-association.cdn-apple.com/a/v1/knotless.dev`), as a release build can't use developer mode: it can take a day or two after the file changes. ## Making everyone update An appview's `dev.knotless.describeServer` can name the oldest build it lets run. A build older shows only a screen with a way to update, in place of the whole app, with the reason if there is one: # in .knotless/workflows/deploy.yml, landed as any pull KNOTLESS_IOS_MINIMUM_BUILD: "5" KNOTLESS_IOS_MINIMUM_REASON: "This version can lose what you write." The number is `CURRENT_PROJECT_VERSION`. **Raise it only once that build, or a newer one, is live for the people it stops**, on the App Store and on TestFlight: the screen is all they can see, and there is nothing else to send them to. Taking the setting out lets them back in, the next time the app comes to the front. The app asks when it comes to the front and keeps the answer, so a build stopped stays stopped offline, and one that has never been told runs. A server that doesn't answer changes nothing. Its button opens TestFlight in a TestFlight build and the App Store in any other. Only builds from 5 up do this: older ones ignore the setting. ## Sizes and colours Every colour, space, corner radius, icon and avatar size is a token in `Knotless/Support/Theme.swift` or `Knotless/Support/Palette.swift`: take one by name, and add one there when none fits. The spaces follow the scale in `web/src/tokens.css`. Never take an iOS colour, as `Color(.systemBackground)` or `Color.secondary`, directly: the tokens stand for those where there's no theme, and for a theme's colours where there is. ## Themes The app is drawn in the themes the pages are. Settings, Appearance chooses: Same as knotless.dev (the default) follows the account's `appearancePref`, day and night pair included; Automatic, Light and Dark are iOS's own colours; then the themes the pages offer by name, and the account's own, those it published and those it added. A palette leaves out what the pages fill in, and `Palette(record:)` fills it in as `tokens.css` does. A theme's colours come in as each is drawn: a token is a dynamic `UIColor` that reads the theme in use (`Themes`), and a trait carried from the root (`ThemeTrait`) draws them again when it changes, without rebuilding a screen. As on the pages, only the main button takes a colour, the theme's `primary` under its `onPrimary`; a state's pill is its colour under `onSolid`; and a button beside the main one is `quietButton`, on the theme's surface. ## Looking at a screen `just ios-look ` draws a screen in the simulator, light and dark, from all the way down, as pictures in `target/look`, with no server and no account. Look at what you changed before you push it: just ios-look pull-open just ios-look pull-open --set getMergeStatus.checks=failed --top --light just ios-look pull-open --tap "Add to merge queue" --light just ios-look pull-open --theme dracula --dark just ios-look appearance --tap Ember Its answers are what `harness/shots/fixtures.json` recorded for the web pages, with what that lacks in `look-answers.json`. A write does nothing, so a `--tap` that writes shows what's under way, not what comes after. `ios/look.mjs` lists the screens and options. Each run starts with Appearance as a new install has it, whatever the simulator was left in. It needs Xcode, so no check runs it. ## Tests `KnotlessUITests` taps through the app against live knotless.dev. It needs the network and those tokens, and CI doesn't build iOS, so it is run by hand: TEST_RUNNER_KNOTLESS_TOKENS=$PWD/ios/build/.tokens.json \ xcodebuild test -project ios/Knotless.xcodeproj -scheme Knotless \ -destination 'platform=iOS Simulator,name=iPhone 17 Pro' \ -derivedDataPath ios/build -allowProvisioningUpdates CI can't build the app, as Xcode needs macOS. On a pull, its `ios` check compiles `Knotless/API/Models.swift` on Linux and reads the recorded XRPC answers with it: `ios/check-models.sh` runs the same here. ## Diagrams A Mermaid block in Markdown is drawn by Pele, as the pages draw it (`Knotless/Support/Diagram.swift`). `Knotless/Vendor/pele.js` is the version `web/package-lock.json` pins: after raising it there, `just ios-pele` copies it here, and the `ios` check fails while the two differ. ## Icons `Assets.xcassets` holds Octicons (MIT), the same set the web pages draw.