feat(app): iOS-skallen — Capacitor, Xcode-projektet og et CI-job, der bygger den

Femte trin mod iOS-appen. Fladen fra trin 3 pakket ind i en rigtig app.

- `app/`: Capacitor 8 med SPM (ingen CocoaPods). `npm run sync` bygger web
  med VERDANDE_TARGET=app og kopierer det ind i Xcode-projektet; `npm run
  open` åbner det på en Mac. Bundle-id `dk.nolimit.verdande`.
- `app/ios/` er committet. Webbygget og den genererede konfiguration i det er
  ikke — sync lægger dem der.
- `NSAllowsLocalNetworking`: en hjemmeserver uden certifikat kan nås med http
  på det lokale net; alt på internettet skal stadig være https.
- Ikon og opstartsbillede tegnes fra web/static/icon.svg
  (scripts/generate-ios-assets.js). Kvadratisk og uden gennemsigtighed, fordi
  iOS runder hjørnerne selv.
- CI-jobbet "iOS app shell" bygger og synker på Linux og tjekker, at
  app-bygget ikke registrerer en service worker. Ingen Xcode i CI; det dækker
  alt op til den.
- docs/ios.md: hvordan appen adskiller sig fra en browser, hvordan den bygges,
  hvad der er ejerens skridt (Apple-konto, signering, TestFlight), og hvad der
  mangler — notifikationer via APNs venter på en beslutning om en relay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
kristianwind
2026-10-23 14:21:44 +02:00
co-authored by Claude Opus 5.5
parent b9028e8f7a
commit ce1e12417b
27 changed files with 2121 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
/**
* Renders the iOS app's icon and launch image from static/icon.svg.
*
* node scripts/generate-ios-assets.js
*
* Same reasoning as generate-icons.js: a script, not a test, run when the icon
* changes, with Chromium as the SVG renderer because it is already here.
*
* Two things differ from the web icons, and both are Apple's rules rather than
* taste. The app icon is square with no transparency — iOS draws the rounded
* corners itself, and rounded corners already in the image show as a dark ring
* inside its own mask — so the tile's `rx` is dropped and the result flattened.
* And the launch image is the rune small on the tile colour, the size
* Capacitor's template asks for, so the app opens on the colour it is about to
* draw instead of on white.
*/
import { chromium } from '@playwright/test';
import { readFileSync, writeFileSync } from 'node:fs';
const ICONS = '../app/ios/App/App/Assets.xcassets';
const svg = readFileSync('static/icon.svg', 'utf8').replace(/ rx="[^"]*"/, '');
const tile = /<rect[^>]*fill="(#[0-9a-fA-F]{6})"/.exec(svg)?.[1];
if (!tile) throw new Error('static/icon.svg has no tile colour to paint the launch image with');
const browser = await chromium.launch();
const page = await browser.newPage();
async function render(file, size, markSize) {
await page.setViewportSize({ width: size, height: size });
await page.setContent(
`<style>html,body{margin:0;padding:0;background:${tile}}` +
`div{width:${size}px;height:${size}px;display:grid;place-items:center}` +
`svg{display:block;width:${markSize}px;height:${markSize}px}</style><div>${svg}</div>`
);
writeFileSync(file, await page.locator('div').screenshot({ omitBackground: false }));
console.log(`${file} ${size}×${size}`);
}
await render(`${ICONS}/AppIcon.appiconset/AppIcon-512@2x.png`, 1024, 1024);
// Capacitor's template names three files for the one launch image; all the same.
for (const name of ['splash-2732x2732.png', 'splash-2732x2732-1.png', 'splash-2732x2732-2.png']) {
await render(`${ICONS}/Splash.imageset/${name}`, 2732, 640);
}
await browser.close();