Signal Hub logoSignal Hub

iOS / Swift news and articles

iOS / Swift
Updated just now
Date
Source

12 articles

Dev.to (iOS)
~3 min readMay 6, 2026

Shipped my first iOS app entirely with Codex — what worked, what surprised me

Just shipped Rushi — my first iOS app, built solo with Codex over the past 4 months. Wanted to share what I learned, especially for anyone considering Codex as their primary coding partner for an iOS project. I'd never written Swift before. Started with Codex as my pair-programmer, learning Swift by doing as I built. The app is a Buddhist toolkit — sutra reader + 108-bead mala counter + sutra calligraphy practice. Free for the first week, then $1.99 — early users keep it free forever via App Store purchase history. SwiftUI scaffolding from natural-language descriptions — first iteration was usable; ~80% of layouts hit on first generation Localizing 17 languages — Codex generated all the .strings files from English seeds with cultural awareness (e.g. it correctly chose Vietnamese honorifics for Buddhist terms; same for Tibetan transliteration) SwiftData migration code when I changed schemas mid-project CoreText fallback chain for CJK fonts — UIFont in iOS 17 has bugs with Chinese serif fonts, and Codex correctly recommended bypassing it with CTFontDescriptor explicit fallback Apple Search Ads keyword research — Codex generated competitive keyword lists that matched what ASA actually showed as high-volume Apple-specific UX nuances (haptic timing, large title behavior, dynamic type quirks) — needed multiple rounds with screenshots Audio concurrency — Codex tended to over-engineer with actors when AVAudioPlayer would do App Store metadata (privacy labels, age rating) — Codex would sometimes suggest things that didn't match actual data flow ("data collection: false" while suggesting analytics SDK in another file) Show Codex the entire SwiftUI view file when iterating — partial context leads to half-broken layouts When testing, paste the exact Xcode error text including line numbers — Codex maps stack traces well For App Store review, ask Codex to write your privacy label statements based on your actual data flow, not your intent — it catches "I think we don't collect X" mistakes Don't let Codex pick your dependency list — it prefers "popular" libraries over "minimal" ones; for a solo iOS app you want the latter Apple review: 2 days, no rejections 17 UI languages, 9 full-text languages 0 crashes in TestFlight beta Open-sourced everything: sutra texts CC0 1.0, app source on GitHub Codex token usage during development: ~50M input tokens, ~3M output tokens Repo (with the open-source sutra texts): github.com/hooosberg/Rushi Vibe coding made me anxious; building this calmed me down. Happy to answer specific Codex/SwiftUI/CoreText questions in the comments.

Dev.to (iOS)
~4 min readMay 6, 2026

Indie iOS Q3 2026 Forecast: Where the Indies Will Win (and Where They Won't)

TL;DR: Q3 2026 is shaping up as the hardest indie iOS quarter in 5 years. iOS 26 SDK requirement, AI-native apps everywhere, $1B+ in subscription saturation. Indie wins are still possible but in narrower niches than 2024-2025. Below: the 5 niches with green lights and 5 with red. Three big shifts compressed into 3 months: All new submissions must target iOS 26 SDK + Xcode 26. Apps built against iOS 17 won't pass upload check. This locks out indie devs who haven't updated their toolchain. Every iOS app launch in 2026 advertises "AI-powered." This is now noise. Audiences are filtering out generic AI claims and looking for specific AI utility. The App Store top 100 is dominated by subscription apps. New subscription apps face brutal LTV/CAC math. Adapty 2026: 12% conversion for hard paywall vs 2% for freemium subscription. Indies should default to one-time pricing. Apps with verifiable privacy claims (Privacy Manifest declares zero collection, nm shows zero networking symbols) will outperform comparable subscription/ad-supported apps in 2026 due to regulatory tailwinds. Examples: notes apps, calculators, timers, decision tools. Anything that can run 100% offline. For my AutoApp portfolio: 4 apps × $1.99 IAP × verifiable privacy. Q3 forecast: small but real revenue. Generic "AI assistant" apps will saturate. Industry-specific ones (e.g., "AI for indie iOS devs," "AI for solo lawyers," "AI for solo therapists") will find audiences. The win condition: the indie has domain expertise the AI lacks. The app sells the indie's curation, not the AI. Apps that work on iOS + macOS + watchOS together (handoff, shared data) will outperform iOS-only competitors. Apple's Continuity is mature in 2026 and users notice when apps don't use it. Indies have an advantage: they can ship across all 3 platforms in 1 codebase via SwiftUI. Big-team apps often skip Watch. General fitness is saturated (Apple Health, Strava, Whoop). But hyper-niche (climbers, BJJ practitioners, ultra-runners, indie athletes) is wide open. The audience is small but devoted. $4.99 one-time for a niche tool can outearn $9.99/month subscription for a general one. Other indie hackers buy from indie hackers. The audience overlap is high, the trust premium is real. For me: 5 Gumroad SKUs targeting indie iOS devs. The buyer profile is "person who has shipped at least 1 app." Tight market, high LTV. ChatGPT, Claude, Gemini, dozens more. New "AI chat" apps need a specific differentiator. "I made it" isn't enough. Notion, Linear, Asana, Monday, ClickUp. Plus 100s of "X for Y" subscription productivity apps. New entrant needs $50k+ CAC budget per year just to be visible. Not indie scale. Network effects punishing. Without 1M+ users, the app feels empty. Indie scale can't reach 1M+ users in a quarter. Saturated, ToS minefield, regulatory scrutiny. Don't. App Store policy changes 2024-2025 made these harder. Apple's review is slower for these categories. Indies should skip. For my AutoApp Q3 portfolio (after the 4 current apps clear Apple Review): TipJar Now (planned) — QR-code tip jars for indie creators. Niche 5. HabitHash (planned) — privacy-first habit tracker. Niche 1. Both are utility apps with one-time IAP. If Q3 conversion math holds, each at ~$200-500 MRR by Day 180. If you're starting an indie iOS project in Q3 2026, pick ONE green-light niche. Avoid the red lights. Default to one-time IAP unless you have a clear case for subscription. The math: One-time $1.99-9.99 IAP × hard paywall = 12% conversion (Adapty 2026) Cold-start indie audience reach = 100-500 unique visitors per app per month (organic) Conversion math = 12-60 paying customers per month per app At $1.99 = $24-119 / month per app (small but real) At $9.99 = $120-600 / month per app For 4-app portfolio: $480-2400 / month at Day 90 if all 4 hit baseline. Real indie scale. Subscription productivity. Generic AI. Crypto. Social. Dating. These are venture-capital problems, not indie problems. If you're not raising money, don't optimize for VC metrics. Adapty 2026 indie iOS conversion benchmark, my 60-day timeline article, my Q3 distribution research. If you're picking a Q3 2026 indie iOS niche and want the playbook I used (real numbers, real timeline): iOS Indie Launch Playbook ($19).

Dev.to (iOS)
~3 min readMay 6, 2026

Apple ASC API: Real JWT Auth + V1/V2 Path Quirks (2026 Edition)

TL;DR: 4 indie iOS apps, 600+ ASC API calls in 60 days. Here's the JWT auth code, the V1/V2 path traps that cost me 6 hours, and the resource model gotchas. import jwt, time, uuid def asc_token(key_id: str, issuer_id: str, p8_path: str) -> str: with open(p8_path) as f: priv = f.read() return jwt.encode( payload={ "iss": issuer_id, "iat": int(time.time()), "exp": int(time.time()) + 1200, # 20 min max "aud": "appstoreconnect-v1", }, key=priv, algorithm="ES256", headers={"kid": key_id, "typ": "JWT"}, ) 3 things you need from ASC: Key ID (10-char string) Issuer ID (UUID) .p8 private key file Get all three at: appstoreconnect.apple.com → Users and Access → Keys → API Keys. Apple's "v2" resources still use /v1/... paths. Example: # beta tester INVITATIONS — V2 resource POST /v1/betaTesterInvitations ← yes, /v1/ {"data": {"type": "betaTesterInvitations", "attributes": {...}}} # beta GROUP relationships — V1 POST /v1/betaGroups/{id}/relationships/betaTesters There is no /v2/ prefix in URLs even for V2 resource types. The "version" lives in the resource type name only. appAvailabilities resource is read-only for existing apps. You can POST to create, but you cannot PATCH to update. To change app availability (e.g., add territories): POST /v1/appAvailabilities with new full state (Apple deletes the old) Or use CDP web UI (no programmatic UPDATE) I lost 2 hours trying every PATCH variation before finding this in their changelog. When creating a resource that references another: # WRONG — Apple rejects this {"data": {"type": "betaTesters", "relationships": { "betaGroups": {"data": [{"type": "betaGroups", "id": "abc123"}]} }}} # RIGHT — must use ${...} string format for inline ID {"data": {"type": "betaTesters", "relationships": { "betaGroups": {"data": [{"type": "betaGroups", "id": "${beta_group_id}"}]} }}} Documented in the Apple changelog as "string template format". Easy to miss. If you add a tester to App A and App B, you get two tester records with the same email. They have different IDs. # Get all tester records for an email tester_records = asc_api.get(f"/v1/betaTesters?filter[email]={email}") # Returns multiple records, one per app the tester is on So when "removing" a tester, you must remove from each app's record separately. The state field on a tester record is always null unless you query through the betaGroup endpoint: # state always null GET /v1/betaTesters?filter[email]=foo@bar.com → state: null # state actually populated GET /v1/betaGroups/{group_id}/betaTesters → state: "ACCEPTED" Apple's /v1/builds endpoint silently ignores the sort parameter. Documented elsewhere as "sort not supported", but the API returns 200 instead of 400. Workaround: get all builds, sort client-side by attributes.uploadedDate. class ASCAuth: def __init__(self, key_id, issuer_id, p8_path): self.key_id = key_id self.issuer_id = issuer_id self.p8_path = p8_path self._token = None self._expires = 0 def token(self): now = int(time.time()) if not self._token or self._expires - now < 120: self._token = asc_token(self.key_id, self.issuer_id, self.p8_path) self._expires = now + 1200 return self._token def headers(self): return {"Authorization": f"Bearer {self.token()}", "Content-Type": "application/json"} 20-min token TTL with 2-min refresh buffer. Reuses the token across requests. Build state polling (cron job) TestFlight tester invitations (batch) App availability changes (per territory) IAP creation (NOT pricing — pricing requires CDP web UI as of 2026) Beta group management Localizations CRUD Submission for App Review What it cannot do: IAP price tier selection (CDP only) Public app version visibility toggles (some require CDP) Apple Pay merchant config (manual) Paid Apps agreement signing (manual) Full ASC API helper class with JWT + retry + 18 wrapped endpoints: AutoApp Dashboard ($39) includes: asc_api.py (JWT auth + 18 endpoints) asc_diag.py (diagnostic CLI for build state, tester state, agreement state) asc_iap_pricing_v3.py (7-step CDP IAP pricing flow) asc_status_poll.py (hourly cron) If you're building anything indie iOS in 2026, the ASC API is your friend. The CDP fallback is your acquaintance. Plan for both.

Dev.to (iOS)
~3 min readMay 6, 2026

[SC] Manejo de Memoria en Swift Concurrency

Comprensión durante la lectura ¿Qué es Swift Concurrency y por qué es relevante para el manejo de memoria? Swift Concurrency es un modelo de concurrencia de Swift que se basa en Tasks para ejecutar trabajo de forma asíncrona. Es relevante para el manejo de memoria porque las Tasks puede retener instancias y provocar comportamientos inesperados debido a su efecto retardado. Task en Swift Concurrency, y en qué se parecen a los closures normales? Un Task puede retener una referencia de forma fuerte tanto de forma implícita como explícita (con el "capture list"). Una Task podría estar reteniendo un objeto que, en cierto momento se asume que ya fue liberado. Sin embargo, debido a su naturaleza asíncrona, el efecto de haber retenido dicho objeto puede aparecer más tarde, y esto puede ser difícil de rastrear. Task captura self de forma fuerte (strong reference) y el objeto se establece en nil antes de que la tarea termine? El objeto se libera después de que la Task termina. [weak self] dentro de una Task? Al usar [weak self], ARC puede liberar self durante la suspensión del Task. Cuando la tarea reanuda después de await, self puede ser nil por lo que las llamadas posteriores a self no se ejecutan. actors) gestionan su memoria de forma independiente y serializan el acceso a sus propiedades? Los actores sincronizan el acceso a su memoria a través de un executor. De este modo, todo cliente que quiera modificar una propiedad tendrá que hacerlo a través de un método accesor async, y se puede leer directamente accediendo con await a la propiedad. fetchData() captura self fuertemente y el viewModel se pone en nil? ¿Por qué ocurre ese orden? @MainActor final class ContentViewModel { deinit { print("Deinit!") } func fetchData() { Task { await performNetworkRequest() updateUI() // ⚠️ Captures self strongly! } } func updateUI() { print("Update UI!") } func performNetworkRequest() async { print("Perform network request") try? await Task.sleep(for: .seconds(1)) print("Finished network request") } } var viewModel: ContentViewModel? = .init() viewModel?.fetchData() viewModel = nil print("Set viewModel to nil") Imprime: // Perform network request // Set viewModel to nil // Finished network request // Update UI! // Deinit! El deinit se llama al final, incluso aunque se puso viewModel = nil porque la Task retuvo el objeto. performNetworkRequest() cuando se usa [weak self] y el objeto se libera a mitad de la ejecución? La petición simulada termina después de que se libera el objeto en cuestión. Luego, cuando el flujo de control regresa al Task, las instrucciones siguientes no se ejecutan porque self es nil. Task puede mantener vivo un objeto más tiempo del esperado? ¿Cuáles son las dos formas de capturar self dentro de una Task que muestra el artículo, y cuál es la consecuencia de cada una? Revisión y síntesis ¿Cuál es la conclusión principal del artículo sobre el manejo de memoria en Swift Concurrency respecto al código normal? ¿Qué impacto práctico puede tener una retención prolongada de objetos en el rendimiento de una aplicación? ¿Qué temas o ejemplos adicionales anticipa el artículo que se cubrirán en la siguiente lección? Bibliografía Van der Lee, A. (2025). Swift Concurrency Course [Curso en línea]. avanderlee.com.

Dev.to (iOS)
~4 min readMay 6, 2026

[SC] Control del dominio de aislamiento por defecto en Swift 6.2

Comprensión durante la lectura ¿Qué significa exactamente "dominio de aislamiento por defecto" y por qué importa en Swift Concurrency? Los tipos de datos (funciones y propiedades globales) en Swift 6.2 viven en algún tipo de aislamiento, bien sea nonisolated o un actor específico. Como la mayoría de aplicaciones comienzan en el hilo principal y solo cambian de hilo si se introduce concurrencia explícitamente (lo que implica que la mayoría de tipos de datos vivirían en el mismo dominio de aislamiento MainActor), hay una funcionalidad opcional en Xcode 26 que permite marcar todos los tipos de datos dentro de un dominio de aislamiento por defecto. Este dominio de aislamiento puede ser MainActor (anteriormente era nonisolated por defecto). Esta funcionalidad es importante porque, antes de que existiera, aparecía un montón de advertencias en todos los tipos de datos debido a que se asumía que todas eran nonisolated, pese a que de alguna manera estaban relacionadas con tipos de datos de MainActor. Silenciar una advertencia por un lado, podía levantar otra por otro lado. @MainActor en todas partes? La aplicación de iOS podía iniciar en @MainActor y no cambiar de hilo, sino hasta ejecutar alguna tarea específica. El compilador no podía asumir que todas las clases no marcadas con @MainActor vivían dentro del dominio de MainActor, incluso aunque el desarrollador supiera que así ocurría, y que no había ningún riesgo de introducir una carrera de datos. Por esta razón, el desarrollador tenía que marcar todas las clases con @MainActor. Ahora, todas traen @MainActor implícito y, en caso de no querer este dominio de aislamiento, se tiene que cambiar manualmente. nonisolated y @MainActor como dominio por defecto? nonisolated por defecto, indica que el código no está aislado en ningún dominio de aislamiento específico, por lo que el compilador no puede asegurar la ausencia de carreras de datos. ATENCIÓN: Esto no significa que HABRÁ una carrera de datos - Puede no haberla. Solo quiere decir que no se puede asegurar que no la haya. @MainActor por defecto, indica que todo el código está aislado en MainActor, a no ser que se indique lo contrario. Esto asegura que no haya carreras de datos entre los llamados. En un proyecto de Xcode hay que cambiar "default actor isolation". En el caso del paquete de Swift, se puede aplicar el setting SwiftSetting.defaultIsolation(_:_:), teniendo en cuenta que puede recibir como aislamiento MainActor.self o nil. Tomar como referencia el siguiente código para configurar un paquete de Swift: import PackageDescription let package = Package( name: "MyPackage", targets: [ .target( name: "MyTarget", swiftSettings: [ .define("ENABLE_FEATURE_A"), .define("DEBUG_MODE", .when(configuration: .debug)), .enableUpcomingFeature("ExistentialAny") ] ), .testTarget( name: "MyTargetTests", dependencies: ["MyTarget"] ), ] ) La mayoría de código de una app inicia en el hilo principal y permanece allí hasta que explícitamente se introduce concurrencia. En este escenario, si todo corre secuencial, no hay carreras de datos, así que asumir que el código está en nonisolated provoca falsas alarmas. En lugar de eso, es mejor asumir por defecto que el código corre en el hilo principal. class, structs y actor cuando se usa @MainActor como valor por defecto? Al usar @MainActor como valor por defecto, class y struct (junto con atributos y métodos), vivirán enMainActor. Al crear unactor, sus métodosinitydeinitseránnonisolated` y el resto estarán aislados dentro del dominio de aislamiento definido por él mismo. En el caso de que class y struct conformen un protocolo, estos heredarán el dominio de aislamiento definido para el protocolo. No rompe proyectos existentes el cambio es opcional. Si no se aplica, entonces el código asume por defecto nonisolated. Hay que configurarlo por módulo. Van der Lee, A. (2025). Swift Concurrency Course [Curso en línea]. avanderlee.com.

Dev.to (iOS)
~5 min readMay 6, 2026

Verify Your Own iOS App Privacy Claims with nm (Real Test, Real Result)

TL;DR: Privacy Manifest declarations are just XML. The binary is the proof. Here's how to use nm and otool to verify your own iOS app actually does what its Privacy Manifest claims — and why every privacy-first indie should ship with this verification command in their README. Apple's Privacy Manifest (introduced 2024, mandatory 2025+) is a YAML/plist file declaring what data your app collects. Apple's review reads this and shows users a labeled "Data this app collects" page in the App Store. But the manifest is just a declaration. Nothing physically prevents your app from collecting data not declared. Real privacy-first apps need a verification step. Here's the one I use. I have 4 indie iOS apps with one shared claim: zero data collection, zero networking, zero analytics SDKs. The Privacy Manifest declares it. The binary backs it up. The verification: nm (the symbol-table reader, comes with Xcode) shows every external symbol your binary references. If the binary uses URLSession, NSURLConnection, Network.framework, or any analytics SDK — the symbols appear in nm output. If they're absent, the binary literally cannot phone home. nm -gU AutoChoice.app/AutoChoice | grep -iE 'URL|HTTP|Network|Analytics' Breakdown: nm — list symbols -g — only externally visible symbols (the ones the OS would link to) -U — only undefined symbols (the ones the binary needs but doesn't provide itself; these are calls into iOS frameworks) AutoChoice.app/AutoChoice — the binary inside the .app bundle (extract from .ipa or pull from device) Run it on my AutoChoice binary: $ nm -gU AutoChoice.app/AutoChoice | grep -iE 'URL|HTTP|Network|Analytics' (no output) Zero output = zero networking symbols in the binary. The app cannot make a network request even if it wanted to. Most iOS apps have hundreds of networking symbols: $ nm -gU SomeOtherApp.app/SomeOtherApp | grep -iE 'URL|HTTP|Network' | head U _CFURLCopyAbsoluteURL U _CFURLCreateAbsoluteURLWithBytes U _CFURLCreateWithString U _NSURLConnectionDidFailLoadingError U _NSURLProtectionSpaceHTTPS U _NSURLSessionConfigurationDefault ... (300+ more) 300+ networking symbols means: URLSession.shared.dataTask(with: ...) is somewhere in the binary. Could be analytics, could be feature loading, could be ads. The point is: it CAN make requests. A truly offline app shouldn't have these. # Analytics SDKs (look for known framework names) nm -gU AutoChoice.app/AutoChoice | grep -iE 'Firebase|Mixpanel|Amplitude|Adjust|AppsFlyer|Segment' # Tracking IDs nm -gU AutoChoice.app/AutoChoice | grep -iE 'Advertising|IDFA|IDFV|TrackingTransparency' # Background tasks nm -gU AutoChoice.app/AutoChoice | grep -iE 'BackgroundTask|BGTask' For my apps: all empty. Add to your app's README + privacy page: ## Verify the privacy claim yourself Privacy Manifest is a declaration. The binary is the proof. After downloading from the App Store: 1. Get the binary from Settings → General → iPhone Storage → [App] → Show Data 2. Or extract from .ipa (rename .ipa to .zip, unzip, find Payload/<App>.app/<App>) 3. Run: nm -gU AutoChoice.app/AutoChoice | grep -iE 'URL|HTTP|Network' Empty output = no networking symbols in the binary = the app cannot phone home. Then take a screenshot of the empty output and host it on your privacy page. That's a verifiable claim, not a marketing claim. Three reasons: They have networking they don't disclose. Most apps use SDKs that phone home for analytics, crash reporting, or feature flags. Removing all of them means losing those tools. They don't think users will check. Most users won't run nm. But the few who do (security researchers, privacy bloggers) will tell their networks. It's harder than it looks. SwiftUI apps often pull in Combine which transitively includes URLSession. Building a truly offline app requires deliberate architecture choices. For my apps, the deliberate choices were: No analytics SDK No crash reporting (use Apple's built-in only, which doesn't go through your binary) No remote config (use bundled defaults) No image CDN (bundle all images at compile time) No font CDN (use system fonts only) This costs maybe 5% feature flexibility for 95% privacy verifiability. Worth it for indie apps where privacy is the differentiator. Static frameworks: If you statically link a framework that uses networking, nm will show its symbols too. So check your build configuration — dynamic linking isolates the framework, static linking embeds it in your binary. Swift symbol mangling: Swift symbols are mangled. Use swift demangle to read them: nm -gU MyApp.app/MyApp | swift demangle | grep -i 'network' Apple framework symbols: Some Apple frameworks (Combine, SwiftUI) reference URL types internally. These are fine — they're part of the OS, not your code making requests. nm -U shows all undefined symbols including these. Filter for non-prefixed ones (_$s is Swift, _NS is Foundation): nm -gU MyApp.app/MyApp | grep -E '^\s+U _[a-z]' | head This shows only your symbols, not Apple's. Privacy is a verifiable property, not a marketing claim. Apps that say "we respect your privacy" without showing the binary verification are doing PR. Apps that publish the nm command and the empty output are doing actual privacy. In 2026 with regulators (GDPR, EU DMA, US state privacy laws) tightening, the verifiable claim is increasingly the legal claim. PR claims will fail audits. All 4 of my iOS apps' Privacy Manifests + verification scripts: github.com/jiejuefuyou AutoChoice / AltitudeNow / DaysUntil / PromptVault — all MIT licensed, all empty nm output If you're building a privacy-first iOS app and want the full 60-day verifiable-privacy playbook: iOS Indie Launch Playbook ($19) — includes the Privacy Manifest configuration that passes Apple Review on first try.

Dev.to (iOS)
~4 min readMay 6, 2026

10 ASC API Scripts Every Indie iOS Dev Should Have

TL;DR: Apple's App Store Connect API exposes most of what you need for indie launches. Here are the 10 scripts I built across a 60-day experiment that paid back hours of bureaucracy. From my $499 ASC API Toolkit on Gumroad. The Apple ASC web UI is fine for 1 app. For 4+ apps it becomes a clicking exercise. Apple's App Store Connect API handles 90% of routine bureaucracy via JSON. The remaining 10% (e.g., IAP price tier setup) needs CDP automation — which I covered in previous article. This article: the 10 scripts I actually use weekly. Authenticate via JWT generated from your private .p8 key: from authlib.jose import jwt import time def gen_token(key_id: str, issuer_id: str, key_file: str) -> str: with open(key_file, 'rb') as f: private_key = f.read() headers = {"alg": "ES256", "kid": key_id, "typ": "JWT"} payload = { "iss": issuer_id, "exp": int(time.time()) + 1200, # 20-min token "aud": "appstoreconnect-v1" } return jwt.encode(headers, payload, private_key).decode() All 10 scripts below use this token in the Authorization: Bearer <token> header. import requests def list_apps(token: str): r = requests.get("https://api.appstoreconnect.apple.com/v1/apps", headers={"Authorization": f"Bearer {token}"}) for app in r.json()["data"]: print(f"{app['attributes']['bundleId']}: {app['attributes']['name']}") Output: com.jiejuefuyou.autochoice: AutoChoice com.jiejuefuyou.altitudenow: AltitudeNow com.jiejuefuyou.daysuntil: DaysUntil com.jiejuefuyou.promptvault: PromptVault def list_builds(token, app_id): r = requests.get(f"https://api.appstoreconnect.apple.com/v1/builds", headers={"Authorization": f"Bearer {token}"}, params={"filter[app]": app_id, "limit": 5}) for b in r.json()["data"]: attrs = b["attributes"] print(f"{attrs['version']} ({attrs['buildNumber']}): {attrs['processingState']}") The undocumented gotcha: new builds don't auto-add to TF groups. You have to associate manually. def add_build_to_group(token, build_id, group_id): payload = {"data": [{"type": "builds", "id": build_id}]} r = requests.post( f"https://api.appstoreconnect.apple.com/v1/betaGroups/{group_id}/relationships/builds", headers={"Authorization": f"Bearer {token}", "content-type": "application/json"}, json=payload ) return r.status_code == 204 This single call saves 4-6 hours of TF debugging per app launch. Worth it. When a tester says "I never got the email": def resend_invite(token, app_id, tester_id): payload = { "data": { "type": "betaTesterInvitations", "relationships": { "betaTester": {"data": {"type": "betaTesters", "id": tester_id}}, "app": {"data": {"type": "apps", "id": app_id}} } } } r = requests.post("https://api.appstoreconnect.apple.com/v1/betaTesterInvitations", headers={"Authorization": f"Bearer {token}", "content-type": "application/json"}, json=payload) return r.status_code == 201 A tester record can exist (INVITED) without being ACCEPTED. INVITED can't install. def tester_state(token, app_id, tester_email): r = requests.get("https://api.appstoreconnect.apple.com/v1/betaTesters", headers={"Authorization": f"Bearer {token}"}, params={"filter[email]": tester_email, "filter[apps]": app_id}) if not r.json()["data"]: return "NOT FOUND" state = r.json()["data"][0]["attributes"].get("state", "UNKNOWN") return state # INVITED / ACCEPTED / etc def app_availability(token, app_id): r = requests.get(f"https://api.appstoreconnect.apple.com/v1/apps/{app_id}/appAvailability", headers={"Authorization": f"Bearer {token}"}) return r.json()["data"]["attributes"] Useful for debugging "App not available in your region" issues. The base IAP creation works via API (price tier is the part that doesn't): def create_iap(token, app_id, product_id, name, iap_type="NON_CONSUMABLE"): payload = { "data": { "type": "inAppPurchasesV2", "attributes": { "name": name, "productId": product_id, "inAppPurchaseType": iap_type }, "relationships": { "app": {"data": {"type": "apps", "id": app_id}} } } } r = requests.post("https://api.appstoreconnect.apple.com/v2/inAppPurchases", headers={"Authorization": f"Bearer {token}", "content-type": "application/json"}, json=payload) return r.json() ⚠️ V2 API but legacy V1 path (/v1/inAppPurchases is also a thing for Subscription IAPs). Pay attention to inAppPurchasesV2 vs inAppPurchases based on what you need. def agreement_state(token): r = requests.get("https://api.appstoreconnect.apple.com/v1/agreementsForReadyForSale", headers={"Authorization": f"Bearer {token}"}) # API path varies by year — check current docs return r.json() If your paid_apps agreement is unsigned, ALL distribution (including TestFlight) is gated. Check this Day 1. def pending_submissions(token, app_id): r = requests.get(f"https://api.appstoreconnect.apple.com/v1/apps/{app_id}/appStoreVersions", headers={"Authorization": f"Bearer {token}"}, params={"filter[appStoreState]": "WAITING_FOR_REVIEW,IN_REVIEW"}) return r.json()["data"] Lets you build a "what's in the queue" dashboard without manual ASC web UI clicks. def build_details(token, build_id): r = requests.get(f"https://api.appstoreconnect.apple.com/v1/builds/{build_id}", headers={"Authorization": f"Bearer {token}"}) return r.json()["data"]["attributes"] For diagnosing "build rejected" or "processing stuck" states. IAP price tier setup — Apple's API doesn't expose tier selection. CDP + Playwright is the workaround. Full writeup. Localization batch — does work via API but gets verbose; my actual implementation handles N apps × M locales × 5 fields each. Worth its own article. Crash analytics — Apple's API exposes some but not all; symbolicated crashes need additional tooling. Submit for Review — works via API but has 50+ required fields; treating it as a single function loses nuance. These are in the full ASC API Toolkit ($499) which has 60+ Python scripts including all the edge cases. If you have 1 iOS app: API isn't worth it. Web UI is fine. If you have 4+ apps or are launching multiple per quarter: API saves 30-40 hours per launch cycle. At indie rates, that's $1500-2000 saved per launch. Worth automating. The 10 scripts above are MIT-licensed: github.com/jiejuefuyou/autoapp-toolkit orchestrator/asc-tools/. For the production toolkit (60+ scripts including price tier CDP automation, batch localization, etc.): ASC API Toolkit on Gumroad ($499). Want the 60-day playbook that produced this toolkit (real numbers, real launches): iOS Indie Launch Playbook ($19).

Dev.to (iOS)
~4 min readMay 6, 2026

Indie iOS Q3 2026 Forecast: Where the Indies Will Win (and Where They Will Not)

TL;DR: Q3 2026 is shaping up as the hardest indie iOS quarter in 5 years. iOS 26 SDK requirement, AI-native apps everywhere, $1B+ in subscription saturation. Indie wins are still possible but in narrower niches than 2024-2025. Below: the 5 niches with green lights and 5 with red. Three big shifts compressed into 3 months: All new submissions must target iOS 26 SDK + Xcode 26. Apps built against iOS 17 won't pass upload check. This locks out indie devs who haven't updated their toolchain. Every iOS app launch in 2026 advertises "AI-powered." This is now noise. Audiences are filtering out generic AI claims and looking for specific AI utility. The App Store top 100 is dominated by subscription apps. New subscription apps face brutal LTV/CAC math. Adapty 2026: 12% conversion for hard paywall vs 2% for freemium subscription. Indies should default to one-time pricing. Apps with verifiable privacy claims (Privacy Manifest declares zero collection, nm shows zero networking symbols) will outperform comparable subscription/ad-supported apps in 2026 due to regulatory tailwinds. Examples: notes apps, calculators, timers, decision tools. Anything that can run 100% offline. For my AutoApp portfolio: 4 apps × $1.99 IAP × verifiable privacy. Q3 forecast: small but real revenue. Generic "AI assistant" apps will saturate. Industry-specific ones (e.g., "AI for indie iOS devs," "AI for solo lawyers," "AI for solo therapists") will find audiences. The win condition: the indie has domain expertise the AI lacks. The app sells the indie's curation, not the AI. Apps that work on iOS + macOS + watchOS together (handoff, shared data) will outperform iOS-only competitors. Apple's Continuity is mature in 2026 and users notice when apps don't use it. Indies have an advantage: they can ship across all 3 platforms in 1 codebase via SwiftUI. Big-team apps often skip Watch. General fitness is saturated (Apple Health, Strava, Whoop). But hyper-niche (climbers, BJJ practitioners, ultra-runners, indie athletes) is wide open. The audience is small but devoted. $4.99 one-time for a niche tool can outearn $9.99/month subscription for a general one. Other indie hackers buy from indie hackers. The audience overlap is high, the trust premium is real. For me: 5 Gumroad SKUs targeting indie iOS devs. The buyer profile is "person who has shipped at least 1 app." Tight market, high LTV. ChatGPT, Claude, Gemini, dozens more. New "AI chat" apps need a specific differentiator. "I made it" isn't enough. Notion, Linear, Asana, Monday, ClickUp. Plus 100s of "X for Y" subscription productivity apps. New entrant needs $50k+ CAC budget per year just to be visible. Not indie scale. Network effects punishing. Without 1M+ users, the app feels empty. Indie scale can't reach 1M+ users in a quarter. Saturated, ToS minefield, regulatory scrutiny. Don't. App Store policy changes 2024-2025 made these harder. Apple's review is slower for these categories. Indies should skip. For my AutoApp Q3 portfolio (after the 4 current apps clear Apple Review): TipJar Now (planned) — QR-code tip jars for indie creators. Niche 5. HabitHash (planned) — privacy-first habit tracker. Niche 1. Both are utility apps with one-time IAP. If Q3 conversion math holds, each at ~$200-500 MRR by Day 180. If you're starting an indie iOS project in Q3 2026, pick ONE green-light niche. Avoid the red lights. Default to one-time IAP unless you have a clear case for subscription. The math: One-time $1.99-9.99 IAP × hard paywall = 12% conversion (Adapty 2026) Cold-start indie audience reach = 100-500 unique visitors per app per month (organic) Conversion math = 12-60 paying customers per month per app At $1.99 = $24-119 / month per app (small but real) At $9.99 = $120-600 / month per app For 4-app portfolio: $480-2400 / month at Day 90 if all 4 hit baseline. Real indie scale. Subscription productivity. Generic AI. Crypto. Social. Dating. These are venture-capital problems, not indie problems. If you're not raising money, don't optimize for VC metrics. Adapty 2026 indie iOS conversion benchmark, my 60-day timeline article, my Q3 distribution research. If you're picking a Q3 2026 indie iOS niche and want the playbook I used (real numbers, real timeline): iOS Indie Launch Playbook ($19).

Dev.to (iOS)
~4 min readMay 6, 2026

TestFlight to App Store: 60-Day Timeline Across 4 iOS Apps

TL;DR: I shipped 4 iOS apps to TestFlight in a 60-day window. None are in the App Store yet. Below is the actual day-by-day timeline of what each step took, what blocked, and what would speed up the next round. AutoChoice — decision wheel (Lifestyle category) AltitudeNow — barometric altitude reader (Health & Fitness) DaysUntil — countdown app (Productivity) PromptVault — AI prompt manager (Productivity) All built with SwiftUI, StoreKit 2, $1.99 one-time IAP, zero analytics SDKs, MIT-licensed source on GitHub. Day 1: Apple Developer Program sign-up ($99/year) Day 2: ASC API key generation (issuer, key file, role assignments) Day 3-4: 4 GitHub repos created, xcodegen scaffolds, CI pipelines (GitHub Actions) Day 5: First app skeleton compiles + runs in simulator Day 6-9: 4 app SwiftUI views + StoreKit 2 IAP wired Day 10-11: Privacy Manifest declarations (zero data collection) Day 12-13: App icons, screenshots, Info.plist localization (en + ja) Day 14-15: First TestFlight upload — AutoChoice This is where the timeline got real. Day 16: TF invite sent, tester reports "App not available or doesn't exist" Day 17-22: 6 hours of debugging across 4 apps. Root causes: New build not auto-added to TF group (Apple requirement, not documented) Tester ACCEPTED state vs INVITED state mismatch Apple distribution cache requires DELETE + POST cycle to refresh Apple ID region locked Paid agreement unsigned (blocking ALL distribution including TF) I wrote up the complete debug tree: TestFlight Install Fail Debug Tree. Day 31-32: First IAP creation via ASC API (V1 path) Day 33-35: Pricing tier setup — Apple's API doesn't expose tier selection for IAPs Day 36-40: CDP automation built to drive ASC web UI (7-step JS-click flow). Full writeup. Day 41-43: Localizations for IAPs (Japanese + Simplified Chinese display name + description) Day 44-45: 1024×1024 IAP promo image, review screenshots, review notes Day 46-48: 3/4 testers ACCEPTED state confirmed (one still INVITED) Day 49-51: First TF builds passed Apple's automated review Day 52-55: Cross-platform content engine built (Substack + dev.to + Twitter + Zhihu) Day 56-58: B2B funnel (15 lead-capture pages, $0 stack) Day 59-60: Affiliate program setup, sales pages, Day 60 milestone retrospective Where I am Day 60: 4 apps in TestFlight, 0 in App Store. IAPs created + priced ($1.99 USD × 4 apps). All localizations + screenshots pending. Ranked by hours wasted: Phase Hours What ate the time TF install debugging 18 5 separate root causes, no central docs ASC API V1/V2 inconsistencies 12 Path mismatches (V2 resources at v1 paths) IAP price tier (no API) 8 Built CDP automation as workaround Privacy Manifest debugging 4 Apple's validation is opaque Cross-app secret bootstrapping 6 9 failed PAT/SSH attempts before SSH key worked Screenshot generation 3 OCR + ASO requires per-region If I were starting Day 1 again: Apple's Paid Apps agreement blocks ALL distribution (including TestFlight) if unsigned. I didn't realize this for 2 weeks. Sign on Day 1, even if you don't plan to charge yet. The Apple Review queue is 24h-7d. Whatever you have on Day 5 — that's the version you submit, even with rough edges. Iterate during review. I built autoapp-toolkit's verifier at Day 8. Should have built it at Day 1 — it catches 90% of submission rejections. 4 apps × launch = compounding work. Should have launched 1, learned, then sequenced others 2 weeks apart. I built CDP + Playwright drivers at Day 30+. The infra paid back 10× in the next 30 days. Should have built it Day 5. The non-negotiable parts: App Review: 24h-7d (varies by load) TF tester invite acceptance: 1-72h (tester-controlled) Apple agreement processing: 4-24h IAP review: 24-72h after first submission These add up to 1-3 weeks of pure waiting baked into any launch. Plan around it. Now that the bureaucracy is mostly solved: 2 new SwiftUI apps in pipeline (TipJar Now, HabitHash) — leverage Day 1 lessons Watch + Widget extensions for the 4 existing apps (post App Review) B2B consulting kickoff: target 1 paid client by Day 90 Gumroad SKU expansion: catalog now $572, growing By Day 90 (next milestone post): real revenue numbers from App Store + Gumroad. Until then: this is the honest position. All 4 apps source code on GitHub (MIT): autoapp-hello (AutoChoice) autoapp-altitude-now (AltitudeNow) autoapp-days-until (DaysUntil) autoapp-prompt-vault (PromptVault) Plus the orchestration toolkit: autoapp-toolkit — the 32-check verifier, ASC API tools, cross-app secret manager. If you're indie iOS planning a 2026 launch, the iOS Indie Launch Playbook ($19) has the 50pp condensed version of this 60-day timeline + the ASC bureaucracy chapter.

Dev.to (iOS)
~6 min readMay 6, 2026

I Researched 10 iOS Distribution Channels for 2026. Here is What Indie Devs Should Skip.

TL;DR: Most "iOS launch" advice for 2026 still recommends channels that don't work for $0-budget indies. After 60 days of running an indie iOS portfolio with one AI agent, here's what the research shows: ASO + 1 thoughtful Product Hunt + dev.to writeups beats spread-thin multi-channel attempts. I'm at Day 60 of an indie iOS dev experiment (4 apps shipped to TestFlight, $0 paying customers yet). Distribution is the bottleneck. I spent today's morning doing fresh WebSearch on: "indie iOS developer 2026 best distribution channels" "indie iOS app launch checklist 2026 ASO product hunt reddit" Reading 2026-fresh content from: App Launch Checklist 2026 — AppLaunchFlow ASO Best Practices 2026 — AppLaunchFlow Indie Developer App Launch Checklist 2026 — AppScreenshotStudio ASO for Indie Developers $0 Budget — AppDrift App Store Statistics 2026 — SQ Magazine Top 10 App Categories by Revenue 2026 — NicheMetric Plus first-hand data from running my own portfolio. "5.2% of apps are paid. Most developers opt for freemium or ad-based models." — SQ Magazine 2026 If you're a $0-budget indie planning a one-time-IAP iOS app, you're swimming against the current. The App Store ranking algorithm, editorial team, and revenue stats all favor subscription-based apps. The advice you read is calibrated for that majority. That said, indie ROI doesn't require winning the App Store ranking. It requires reaching the small audience that wants what you made. Channels that work for that: "Apple now extracts text from screenshot captions using OCR (Optical Character Recognition) and uses it for keyword indexing." — AppLaunchFlow 2026 ASO in 2026 is harder than 2024. Apple's algorithm reads: Title (30 char) + Subtitle (30 char) — auto-indexed Keywords field (100 char, comma-separated, no repeats) Screenshot caption text via OCR — new in 2026 Localizations per region What works: Long-tail variants over single high-comp words. Localized screenshots. First 2 frames carry 70% of conversion. What doesn't: Brand-only titles. Subscription tags in keyword field if you're not subscription-pricing. Rare for indies. But possible for unique angles: Privacy-first apps (App Store loves the Privacy Manifest narrative) Accessibility-focused apps Watch + iOS interplay iPad-native designs For the AutoApp portfolio: 4 apps × Privacy Manifest declaring zero data collection × verifiable via nm -gU. That's an editorial pitch I haven't sent yet but should. Still works in 2026 for indie iOS. Strict rules: Tuesday or Wednesday only (Monday = news noise, Friday = weekend dead zone) Need a "hunter" with 1k+ followers ideally Prepare GIFs, screenshots, founder pitch beforehand Comment-engagement matters as much as upvotes For me: not yet leveraged. Saving Product Hunt launch for the strongest of the 4 apps post-Apple-Review. Works for technical apps with strong hook. View-to-install ratio is ~50:1 at best. I had one Show HN attempt (autoapp-toolkit, the orchestration layer). Result: ~120 views, 0 conversions, but 1 GitHub star. Worth the post for the SEO/trust signal, not the conversions. Strict self-promo rules per subreddit. The good ones for indie iOS: r/iOSProgramming — dev side r/sideproject — broader indie r/indiehackers — milestone posts r/apple — consumer-side, but heavy moderation Cap at one good post per quarter per subreddit. Karma drops if you spam. Works only with follower base. Indie iOS niche on X is small but engaged. ~50-200 view-per-tweet baseline for active indie accounts. Threads outperform single tweets 3-5×. Technical writeups about your app's architecture build developer audience, not consumer audience. But: builds trust → eventual indie sales of code/courses/SaaS. This article you're reading is the play. Each technical writeup = ~500-1500 views, ~5-10 reactions, 1-3 conversations. Stack 8-12 articles over 60 days = compounding inbound. Pay or partnership with smaller iOS reviewers (10k-50k subs). Cost vs install: $100-300 for ~50-200 installs. ROI depends on app price + LTV. For $1.99 IAP apps: doesn't pay back. For $499 SKU like my ASC API Toolkit: 1 sale = $499, breakeven at ~5 installs from a $300 sponsorship. Worth testing. Works for visual apps (decision wheels, photo tools, fitness). Doesn't work for utility/productivity. For AutoChoice (decision wheel) — visual, demoable in 5 seconds, fits TikTok format. Worth one organic attempt post-launch. Nearly impossible for indies without warm intro. Skip. The PR distribution services charge $200-500 to get you on second-tier outlets that don't drive installs. The exception: 9to5Mac sometimes covers privacy-first apps. Pitch via direct email to a specific writer who's covered similar angles in the past. If I were starting over Day 1: Build the lead-capture funnel BEFORE the apps (15 static HTML pages took 3 days; would have given me 60 days of conversion data instead of 21). Submit for App Review on Day 5, not Day 30. Wait clock starts when you submit. Iteration happens during review. Pick ONE primary channel and cut others by 90%. I tried Substack + dev.to + Twitter + Zhihu + 公众号. Most got single-digit traffic. dev.to was the only one where one piece broke 1k views. If I'd put 90% effort into dev.to from Day 1, I'd be at 3-5k cumulative views by now. Set up the affiliate program at Day 1. Mine launched at Day 60. 60 days of compounding lost. (writeup) Write 2-3 technical articles per app launch. Architecture, debug stories, real numbers. Compounding inbound. "Starting April 28, 2026, all new iOS submissions must target the iOS 26 SDK and be built with Xcode 26." — multiple 2026 sources If your app is built against iOS 17 or earlier, your binary won't pass App Store upload. Check your Xcode build settings before submitting. For indies in 2026 (per NicheMetric data + 2026 trend): Health & Fitness (0.80x revenue multiplier) — meditation, workout, nutrition, sleep, posture Finance (0.85x multiplier) — budgeting, expense tracking, investment education Productivity — calendar, note-taking, focus tools (medium difficulty) Lifestyle / Utility — niche but underserved Avoid: gaming (whale economics, AAA dominates), social (network effects need scale), dating (saturated + ToS minefield). 2 new SwiftUI apps in pipeline (TipJar Now — utility/finance tier; HabitHash — health/productivity) Cross-link strategy: every dev.to article links to relevant Gumroad SKU + relevant Substack issue Affiliate program now LIVE, 30% commission across 5+ products Submit AutoChoice for App Review first (visual, demoable, fits Product Hunt) This research synthesized from: App Launch Checklist 2026 — AppLaunchFlow ASO Best Practices 2026 — AppLaunchFlow Indie Developer App Launch Checklist 2026 — AppScreenshotStudio ASO for Indie Developers $0 Budget — AppDrift App Store Statistics 2026 — SQ Magazine Top 10 App Categories by Revenue 2026 — NicheMetric Plus first-hand data from my 60-day indie iOS experiment If you're indie iOS planning a 2026 launch, my iOS Indie Launch Playbook ($19) has the 50-page condensed version of all this with a real timeline and ASC checklist. 30-day refund.

Dev.to (iOS)
~3 min readMay 6, 2026

Auto-Pricing 3 iOS IAPs via Browser Automation: The 7-Step CDP Flow

TL;DR: ASC has no public API for setting IAP price tiers. CDP + Playwright + JS clicks (not Playwright clicks) gets it done in ~30s per IAP. 7-step flow with exact selectors below. Apple's App Store Connect API lets you create IAPs, manage availability, and submit for review — but does not let you set the price tier. The tier is gatekept behind their web UI. For a single IAP this is fine. For 3+ apps with similar tiers, manual setup means clicking through ~15 modal steps × 3 apps × 7 steps each = 100+ clicks. I'd rather automate it once. page.locator('button:has-text("添加定价")').click() # TimeoutError: subtree intercepts pointer events ASC's React SPA wraps every interactive element in a <dialog> overlay during transitions. Playwright's intelligent click waits for "stable" — which never happens because animations keep firing. After 30s, timeout. page.evaluate Bypass Playwright entirely and use the DOM API directly: page.evaluate(""" () => { const btn = Array.from(document.querySelectorAll('button')) .find(b => (b.innerText||'').trim() === '添加定价' && b.offsetParent); if (!btn) return 'NOT FOUND'; btn.scrollIntoView({block: 'center'}); btn.click(); return 'OK'; } """) Native Element.click() doesn't go through Playwright's stability checks. The React event handler fires immediately. For each IAP, after navigating to /apps/<app_id>/distribution/iaps/<iap_id>: 添加定价 (Add Pricing) — opens region selection modal 选取 (in dialog) — opens tier dropdown $1.99 (in dialog, exact text match) — selects tier 下一步 (in dialog) — review per-region prices auto-converted from USD base 下一步 (in dialog) — go to final confirm 确认 (in dialog) — commit 保存 (page header button) — save the IAP record After step 7, verify: body = page.evaluate("document.body.innerText") section = body[body.index("价格时间表"):body.index("价格时间表") + 800] saved = "添加定价" not in section If 添加定价 is gone from the section, pricing is locked in. ASC SPA hydration is slow. After page.goto(url, wait_until="domcontentloaded"), you need: 22-25s before the page is interactive 4-6s after each click for next step to render Skip these and you'll race the SPA. The :has-text("$1.99") selector matches both $1.99 and $19.99 — use exact innerText === '$1.99' instead. 3 IAPs (AutoChoice / AltitudeNow / DaysUntil) priced at $1.99 USD in 4 minutes wall-clock. Apple-side state confirmed via the same API I bootstrap with for app/IAP IDs. Full script (~140 LOC, Python 3.10+, Playwright 1.40+): I'll share via the AutoApp newsletter — 60-day indie iOS dev real-time log: https://autoappnotes.substack.com This works for price tier selection only. Localizations / images / Submit-for-Review are separate flows (and the API can do those). Apple may change CSS selectors or button text any week. The brittle parts: 添加定价, 选取, 下一步, 确认, 保存. If your locale is English, swap to "Add Pricing" / "Select" / "Next" / "Confirm" / "Save". Don't run this in headless mode without first verifying the flow visually. CDP + visible Chrome catches selector drift in 5 seconds vs 5 hours in CI. Want the full pricing automation playbook? iOS Indie Launch Playbook on Gumroad — 50pp PDF, real numbers from shipping 4 apps in 60 days, including the ASC bureaucracy chapter.

Dev.to (iOS)
~14 min readMay 6, 2026

Liquid Glass, Material 3, And A Lot Of Plumbing

It has been one of those weeks where the diff is bigger than the headline. The headline is short — Codename One now ships modern native themes: an iOS "liquid glass" look and an Android Material 3 look, bundled into the iOS and Android ports, on by default in the Playground, and selectable from a brand new menu in the simulator. The diff behind that headline is several thousand lines across the platform ports, the simulator, the GUI plumbing, and a small army of screenshot tests. What is Codename One? Codename One is an open-source framework for building native iOS, Android, desktop, and web apps from a single Java or Kotlin codebase. Learn more at codenameone.com. The theme behind the work is simple: Codename One should look modern out of the box on every platform we ship to, and it should feel fast. Almost everything in the past week of commits is in service of one of those two goals. The easiest way to see any of this is the Playground. The Playground now defaults to iOS Modern when the device toggle is set to iPhone and Android Material 3 when it is set to Android, in both light and dark mode. No setup, no pom.xml, no build hints — just open the page, drop in any of the standard components, and the modern look is what you get. If the past releases of Codename One looked dated to you, the Playground is where to start. The simulator is the second-easiest place. We will get to that. For most of Codename One's life the iOS native theme has been the venerable iOS 7 flat theme, and the Android native theme has been Holo Light. Both still ship — backwards compatibility has always been one of our most important goals — but they are no longer where we want a brand new app to start. We spent the bulk of this week building two new themes that target current platform aesthetics: iOS Modern — Apple system colors (accent #007aff light / #0a84ff dark, grouped-form surfaces, the system separator palette), pill borders for tabs, an iOS-Settings-style MultiButton, CHECK_CIRCLE-style checkbox glyphs, and translucent surfaces for Dialog and TabsContainer so they read as glass-frosted on top of whatever is behind them. It is not a real UIVisualEffectView backdrop — that is a port-side primitive we have not built yet — but the look is much closer to the iOS 26 vibe than anything we have shipped before. Android Material 3 — the Material 3 baseline tonal palette (primary #6750a4 light / #d0bcff dark, surface-container tiers, elevated containers approximated tonally because real elevation drop-shadows are still on the to-do list), plus all the Material density and padding choices — Roboto-ish proportions, a top-tab bar with the underline-by-color treatment, the standard square checkbox glyph. Each theme covers the usual ~25 UIIDs: base (Component, Form, ContentPane, Container), typography (Label, SecondaryLabel, TertiaryLabel, SpanLabel*), buttons (Button, RaisedButton, FlatButton with .pressed and .disabled), text input, selection controls, toolbar, tabs, side menu, list, MultiButton, dialog/sheet, FAB, and all the supporting separator and popup pieces. Both themes have full light and dark coverage. The shipping CSS sources sit in the repo at native-themes/ios-modern/theme.css and native-themes/android-material/theme.css for anyone who wants to read what each UIID is doing. This is the ShowcaseTheme capture from the new screenshot suite, run on iOS in light and dark. Same Form, same components, swap Display.setDarkMode(...) and re-resolve. The form is built like this: Container row = new Container(BoxLayout.x()); row.add(new Button("Default")); Button raised = new Button("Raised"); raised.setUIID("RaisedButton"); row.add(raised); form.add(row); TextField tf = new TextField("hello@example.com"); form.add(tf); Container toggles = new Container(BoxLayout.x()); CheckBox cb = new CheckBox("Remember me"); cb.setSelected(true); toggles.add(cb); RadioButton rb = new RadioButton("Agree"); rb.setSelected(true); toggles.add(rb); form.add(toggles); SpanLabel body = new SpanLabel("Body copy …"); form.add(body); That gives you the full picture in one screen: The Default button uses the stock Button UIID. The Raised button uses RaisedButton, which cn1-derives from Button and adds a tinted pill on top of the iOS system blue — that is the iOS Modern accent in both modes. The TextField is a single rounded-rect surface with the iOS system gray fill, the same shape Apple uses in Settings. CheckBox and RadioButton use the new optional @checkBoxCheckedIconInt / @radioCheckedIconInt theme constants to swap to CHECK_CIRCLE / CHECK_CIRCLE_OUTLINE glyphs — Reminders-app aesthetic on iOS while Android keeps the standard square check. The SpanLabel body uses the theme's base font and inherits transparent backgrounds so it never paints over a translucent parent. The full screen source is DarkLightShowcaseThemeScreenshotTest.java. Same ShowcaseTheme source on Android. The Material 3 baseline palette gives Default the primary container color and Raised the elevated-surface tone, with the dark variant flipping the relationship correctly via the dark color-role mapping. Padding and font sizing follow Material density, which you can see in how compact the same Form lays out compared to iOS. This is the DialogTheme capture against the screenshot suite's textured diagonal-stripe backdrop. The backdrop is intentional — it lets reviewers see whether anything that is supposed to be translucent actually is. The iOS Modern Dialog uses an rgba surface fill (0.78 alpha in light, 0.95 in dark — dark needs more opacity because bright stripes bleed through) and its DialogBody, DialogTitle, ContentPane, CommandArea sub-UIIDs are transparent so the rounded corners read cleanly. The same trick is applied to TabsContainer and the iOS MultiButton. The native theme is meant to be a starting point — you can layer your own palette on top without forking the theme. Above is the PaletteOverrideTheme capture: the base is iOS Modern, but the test layers a magenta palette on top at runtime via UIManager.addThemeProps(...). RaisedButton, FlatButton, the disabled tone, and the body-copy span all pick up the override in both light and dark — the override seam works at the resource-bundle layer, exactly the same mechanism a user theme uses to override the native theme on a real app. Three pieces, all live: Themes are bundled. The simulator jar-with-dependencies includes both modern themes alongside the four legacy themes (iPhoneTheme, iOS7Theme, androidTheme, android_holo_light) at the root of the jar. The simulator can pick any one of them at runtime without touching the skin repo. A new "Native Theme" menu. Right next to the Skins menu there is now a Native Theme menu with a radio group for the six themes plus "Auto" and "Use skin's embedded theme". Selecting one writes the simulatorNativeTheme Preference, flips the simulator-reload flag, and disposes the current window so the skin reloader kicks in with the new theme. You can sit on a single skin and flip through every native theme in seconds. Build hints know about it. The new nativeTheme, ios.themeMode, and and.themeMode build hints are registered with the simulator's Build Hints UI on launch — labels, types, value lists, descriptions, the lot. (The legacy keys cn1.nativeTheme and cn1.androidTheme are still honored for back-compat.) Set them in the Build Hints dialog, in codenameone_settings.properties, or via -D system properties; they flow through to the device build and the simulator both. The "Auto" choice in the Native Theme menu defers to those build hints — set ios.themeMode=modern in your project's settings and "Auto" previews iOS Modern; flip the same project to ios.themeMode=ios7 and "Auto" previews iOS 7. The explicit menu entries (iOS Modern, iOS 7, etc.) override the hints regardless. -Dcn1.forceSimulatorTheme is still honored as the highest-priority override; pick "Use skin's embedded theme" to bypass the framework theme entirely and get whatever the skin shipped with. The opt-in is the same on iOS and Android. The platform knobs follow a single naming pattern — ios.themeMode and and.themeMode — and accept modern / liquid / auto / ios7 / flat on iOS, modern / material / auto / hololight / legacy on Android. There is a single cross-platform shortcut, nativeTheme=modern, which the iOS builder consults when ios.themeMode is unset and which the Android port reads at runtime as a default for and.themeMode. The legacy aliases cn1.androidTheme and cn1.nativeTheme are still honored for back-compat, as is and.hololight=true. The default for an existing app stays on legacy on every platform. We do not flip a 15-year-old app's look without an opt-in. New apps generated from the initializr ship with nativeTheme=modern, ios.themeMode=modern, and and.themeMode=modern already set in codenameone_settings.properties, so a brand new project starts with the modern themes preselected. The Playground does the same, and Playground project downloads carry the same defaults into the generated codenameone_settings.properties. The HTML5 port has the runtime support for the modern themes but does not bundle them with user apps yet — that is one of the loose ends we want to close in the next round. The other piece of look-and-feel that we want to highlight is StickyHeaderContainer, which finally has a proper home in the framework. It is the iOS-contacts-list / sectioned-material-list component: scroll past a section boundary and the previous header is replaced by the next one. New this week, the swap is animated. A directional slide moves the outgoing header up on a forward scroll and down on a reverse scroll, or you can pick a cross-fade. Above is a six-frame sweep from the screenshot test — the user scrolls through sections A, B, C, D, E and the pinned header recolors to whichever section is currently active at the top of the viewport. The API is small. Build the container, register sections with addSection(header, content), configure the transition style and duration, and add it to a Form: StickyHeaderContainer sticky = new StickyHeaderContainer(); sticky.setTransitionStyle(StickyHeaderContainer.TRANSITION_SLIDE); sticky.setTransitionDurationMillis(250); for (char c = 'A'; c <= 'Z'; c++) { Label header = new Label("" + c, "StickyHeader"); Container items = new Container(BoxLayout.y()); for (int i = 0; i < 5; i++) { items.add(new Label(c + " entry " + i)); } sticky.addSection(header, items); } form.add(BorderLayout.CENTER, sticky); TRANSITION_SLIDE is the default. TRANSITION_FADE cross-fades the outgoing header on top of the incoming one. TRANSITION_NONE keeps the prior instantaneous swap if you want it. Issue #4807 for the original request. Every screenshot in this post is captured by a test that runs the app on a real iOS device, an Android emulator, and headless Chrome, then diffs each capture against a stored golden image. The diff is the test — if the rendered pixels drift, the run fails. For animations the test grabs a series of frames over a fixed-duration transition, then composites them into a single index image. That is how the dual-appearance shots end up as one side-by-side picture per test: …and how the sticky-header animation ends up as a six-frame strip stitched into a GIF: If you want to read the source, the suite lives at scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/. The theme work was the loudest thing this week, but plenty of other commits landed alongside it: SIMD large-allocation fallback. The SIMD path on iOS allocates its working buffers on the stack via alloca for speed. Past a certain buffer size the stack allocation simply fails — there is not enough stack to give, and the request crashes the process. The fix detects that case and falls back to a regular heap allocation when the request is too large to live on the stack. Small SIMD ops keep the fast alloca path; large ones no longer crash. Pluggable AnimationTime clock. Motion, Timeline, MorphAnimation, Image.animate, and Label tickers now all route through a new AnimationTime class that defaults to System.currentTimeMillis() but can be overridden. Tests can drive animations deterministically frame by frame; demos can run in slow motion or fast forward; Motion.slowMotion is no longer the only lever. POSIX character classes for non-ASCII letters. [[:alpha:]], [[:alnum:]], [[:lower:]], and [[:upper:]] silently failed to match anything outside the basic ASCII range — Greek, Cyrillic, CJK ideographs, accented letters, vulgar fractions, currency symbols. They now match the way you would expect, with five regression tests covering the failing cases from the issue. Fail-fast on JDK < 11. The simulator and "Run as desktop app" goals fork the JVM with --add-exports=java.desktop/com.apple.eawt=ALL-UNNAMED, which JDK 8 rejects with the unhelpful "Could not create the Java Virtual Machine". Now the Maven plugin checks the runtime JDK version on entry to cn1:run and cn1:debug and aborts with a friendly message naming the detected version, JAVA_HOME, and a pointer to Adoptium. JDK 11 through 25 is the supported runtime range for the simulator, JDK 8 stays the build-time requirement for the core framework, and JDK 8 is still fully supported at runtime for shipped desktop apps — only the simulator / "Run as desktop app" Maven goals require JDK 11+. Sheet scrolling swipe and animation. Sheet finally drags from the bottom with a real animation instead of snapping in. Issue #4825. Picker positioning. Picker got additional button-positioning options and a small batch of coverage tests. Playground polish. The Playground moved every Dialog.show(...) to InteractionDialog mode so user code calling Dialog.show does not blow away the editor chrome — it renders into the layered pane instead. Error messages got a substantial overhaul. The preview-resolution syntax expanded so the Playground can pick previews from a much wider set of expressions, with a new harness keeping it honest in CI. Deeper refreshTheme(). Form.refreshTheme() has been around forever — it re-resolves the styles on a single Form. The new thing this week is UIManager.getInstance().refreshTheme(), which snapshots the current theme props and theme constants, clears the resolved-style caches, and re-applies the lot. This is what lets the screenshot suite flip dark mode mid-suite and see fresh styles, and what lets a runtime palette override take effect immediately. Most apps will never need to call it directly — palettes typically don't change at runtime, and a Display.setDarkMode(...) call already triggers the right invalidation. It is there if you do change the palette and want the change to stick on the next paint without reloading the theme from disk. Last week's post was about Codename One feeling faster: corrected pixel densities, principled scroll physics, SIMD on iOS, accessibility text scaling. This week is the symbiotic other half — Codename One looking like it belongs on a 2026 phone. Both halves are the same project. There is not much point in shipping a SIMD-accelerated Base64 if the surrounding UI looks like a 2014 app, and there is not much point in shipping a glass-frosted Dialog if the scroll underneath it judders. Neither half is finished. They are both ongoing, and they both depend on community help — bug reports, RFEs, the patient back-and-forth on issue threads where somebody describes a layout problem on an iPhone you do not own. A specific thank you to the people who drove the issues that turned into this week's commits: Thomas (@ThomasH99) filed #4781 (the original "build a liquid glass example" RFE that started this whole effort), #4807 (sticky headers), #4838 (sideways tab swipe), #4841 (the POSIX regex fix), #4819 (picker buttons), and several others; Francesco Galgani (@jsfan3) filed #4825 (sheet swipe animation) and #4824 (light + dark theme by default in initializr); @ddyer0 caught #4811 (the EDT stack overflow) and #4767 (iPad restart Form size); Lucca Biagi (@LuccaPrado) filed #4817 (form creation in IntelliJ). Several of those are RFEs you would not file unless you actually use the framework day-to-day, and that is the kind of feedback that turns into shippable work. We are sitting at 496 open issues as of this post. That is slow but steady progress — the number is moving in the right direction week over week, and the issues that close tend to ship as features or fixes you can see, not as silent triage. If you have a problem, file it. If you have an RFE, file that too. The themes you saw above started as an RFE. You can try the new themes today by opening the Playground, by setting nativeTheme=modern (or ios.themeMode=modern / and.themeMode=modern for finer control) in your project's codenameone_settings.properties, or by picking them from the simulator's new Native Theme menu. New projects from the initializr already have them on. The shipping resources are bundled in the iOS and Android ports as of this week.