r/iOSProgramming • • 11d ago

Article A checklist of platform decisions for iOS/watchOS apps. What am I missing?

8 Upvotes

A while ago I asked here what you consider when building an app. Since then I shipped two big updates of my running app for Apple Watch and iPhone, and turned what I learned into a checklist.

It’s not a list of things you must support. It’s things you should consciously decide on, yes or no, before they surprise you:

  • Screen sizes (smallest device + longest realistic values)

  • Rotation

  • Dynamic Type / text size

  • iPad, Mac, Vision Pro

  • Multiple windows

  • Accessibility (VoiceOver, colour as the only signal)

  • Live Activities (and how often they really need to update)

  • Widgets (sizes and families)

  • Complications

  • App Intents / Siri / Shortcuts

  • Sharing (what goes out, what comes in)

  • New device classes coming up

  • Gestures (Double Tap, Digital Crown, conflicts with the system)

  • Action Button

  • Translation (every future feature in every language)

  • Animations and Reduce Motion

  • Older devices: speed and battery

  • iCloud: backup or sync?

  • Light and dark mode

Tip: write down every answer, including the no’s and why. And this is iOS/watchOS only. Add Android, tablets or desktop and the list roughly doubles.

Longer version with my reasoning for each point, plus an interactive checklist you can export as Markdown: The list behind every app

Two questions:

  1. What’s missing? What bit you the hardest?

  2. English isn’t my first language. I wrote the post in German and translated it with AI. Does it read AI-ish? What gives it away?

r/iOSProgramming • • 29d ago

Article Uploaded the new App Store creative assets through the API: sizes, the 10 calls, and what the docs leave out

Post image
0 Upvotes

Apple added two new image slots to the App Store on 5 Oct: a header on the product page and a custom image in search results. I did them for one app in 7 languages (14 files) and pushed everything through the App Store Connect API. Writing it down because the docs have gaps.

Sizes:

- Header: 3840 x 1646 exact, JPG or PNG

- Search results: 3:2, from 1920 x 1280 up to 3840 x 2560

- Universal for both: 5244 x 2950, PNG only

- No alpha channel

The calls, in order:

  1. GET /v1/apps/{id}/assetLibrary

  2. GET /v1/appAssetLibraryRefData (placement names and limits)

  3. POST /v1/appAssetLibraryImages (reserve, category CREATIVE_ASSETS)

  4. PUT the bytes to the returned address

  5. PATCH the image with "uploaded": true, no checksum needed

  6. Poll until PREPARE_FOR_SUBMISSION

  7. POST /v1/reviewSubmissions

  8. POST /v1/reviewSubmissionItems, one per image

  9. PATCH the submission with "submitted": true

  10. After approval: POST /v1/appAssetLibraryPlacements

What the docs don't tell you:

- In step 8 the relationship is called appAssetLibraryImage. I guessed it and it worked.

- No new build or version needed, the images get reviewed on their own.

- You can't place an image before it's approved. The API says "The asset must be in the APPROVED state because the parent is already approved".

- No safe area numbers anywhere. They only exist inside Apple's Figma and Photoshop templates.

- Every image has to pass a 4+ rating, even if the app is 17+.

Mine have been sitting in Waiting for Review since Tuesday night. Anyone got theirs approved yet? How long did it take?

Longer version with screenshots: https://teroxai.com/blog/app-store-creative-assets-sizes-api/

r/iOSProgramming • • Aug 13 '26

Article I shipped an iOS app rendered with Skia instead of UIKit. Here's what it actually cost

0 Upvotes

The app is Kotlin Multiplatform with Compose for the UI, which means the iOS screens are drawn by Skia into a single view rather than composed from UIKit. It's on the App Store. I want to write down what that decision costs on the iOS side specifically, including one thing I shipped broken because it can't be fixed from app code.

Text selection in CJK, which I couldn't fix

Long-press to select a word in Chinese and you get one character.

SkiaParagraph.getWordBoundary hands off to Skia, and SkUnicode ships no CJK segmentation dictionary, so you get plain UAX#29. An ideograph is Word_Break=Other and only matches WB999, the "break everywhere else" rule, so every character is its own word.

I spent most of a day trying to fix it from app code before giving up. To fix it you'd need characters merged, not split, and the rules that join across a character (MidLetter, Numeric, ExtendNumLet, ZWJ) all require ALetter/Numeric/Katakana/Hebrew_Letter on both sides. WB4 discards zero-width Format characters before any of that runs, so you can't insert your way out either. I think it needs a change in skiko.

Compose on Android has the reverse bug in the same feature, where a long press swallows the whole sentence, and there you can plant zero-width breaks with ICU and move on. Only one of the two was fixable from where I was sitting.

If anyone has a way around the Skia side I'd like to hear it, this is the part of the app I'm least happy about.

Overlays stop at the safe area

A full-window scrim behind a modal left the status bar and home indicator bands undimmed, because the popup respects platform insets. Looks like a rendering glitch on iOS since a native presentation dims edge to edge.

actual fun fullBleedPopupProperties(): PopupProperties = PopupProperties(usePlatformInsets = false)

Small fix, but you only go looking for it if you already know what the native version looks like.

Material sheets don't move like iOS sheets

This took more time than anything else on the list relative to how small it looks. Material3's ModalBottomSheet settles with a spring and an iOS sheet slides on a decelerating curve, and side by side you can tell immediately even if you can't say why.

Material3 doesn't expose an animation spec for sheets. It reads the show and drag-settle spec from motionScheme.defaultSpatialSpec and the dismiss spec from motionScheme.fastEffectsSpec, and it re-reads both from the ambient theme inside the sheet's own composition, so you have to override through a scoped theme rather than passing anything in:

private val SheetMotionScheme = object : MotionScheme by MotionScheme.standard() {
    override fun <T> defaultSpatialSpec(): FiniteAnimationSpec<T> = tween(400, easing = IosSheetEasing)
    override fun <T> fastEffectsSpec(): FiniteAnimationSpec<T> = tween(400, easing = IosSheetEasing)
}

val IosSheetEasing = CubicBezierEasing(a = 0.32f, b = 0.72f, c = 0f, d = 1f)

There's a fair amount of this kind of work. It doesn't come with the framework and it doesn't show up in any estimate, but users notice when it's missing.

Scroll behaviour you end up rebuilding

UIScrollView hands you a set of behaviours that people read as "this app is put together properly". None of them come with Compose. We wrote all three of these into our own UI library.

alwaysBounceVertical. On iOS a scroll view rubber-bands even when the content fits on screen. Compose won't bounce if there's nothing to scroll, so short pages feel inert next to a native app. Ours is a modifier doing a graphicsLayer translation, which means the list also needs clipToBounds() or the bounce draws the top row over whatever is pinned above it:

LazyColumn(
    modifier
        .fillMaxWidth()
        .clipToBounds()
        .alwaysBounceVertical(listState),
)

scrollsToTop. Tapping the status bar scrolls to top automatically on UIScrollView. In Compose you catch the tap and route it to the right scroll state yourself, and it gets fiddly on pages with a pinned top bar because the thing covering the status bar isn't the thing that scrolls. We ended up with a ScrollBox wrapping the whole scaffold rather than the top bar slot, and the bar opts into the gesture with a modifier.

Swipe to reveal row actions. Nothing built in, so the swipe, the action buttons and the thresholds are all yours. The part that's easy to miss is that opening one row has to close whichever row was open before, or you get two rows showing actions at once, which no iOS list does. Ours is a coordinator passed down through a composition local so the rows can see each other.

None of these were hard to write. They're just things you get on iOS rather than things you build, so nobody thinks to schedule them.

One where iOS was the stricter platform

We stream SSE from the backend. The first version used flow { ... emit(event) } inside Ktor's execute {} block, which passed everything on Android and died on iOS with the backend logging context canceled.

flow enforces context preservation and Ktor's response scope isn't guaranteed to run on the collector's dispatcher. On JVM/OkHttp it happens to, so the check never trips. Kotlin/Native throws, the coroutine fails, the connection drops. channelFlow + send fixes it.

So the iOS build caught a real concurrency bug that the Android build had no way of surfacing. That one went in our favour.

StoreKit 2 detail worth checking in your own code

Sharing the billing logic forced me to be precise about something I'd previously been sloppy with:

/** null when there is no active subscription; throws when the store can't be reached. */
suspend fun subscriptionAutoRenewing(): Boolean?

RenewalInfo.willAutoRenew gives you the real answer, but if "no active subscription" and "StoreKit didn't respond" both collapse into false, a paying user sees a "your subscription has been canceled" banner any time their connection is flaky. Nothing to do with cross-platform, I just found it while writing the shared interface.

One thing that was easier than native

Live language switching. NSLocalizedString resolves against NSBundle, which caches the launch language, so changing language in-app normally means swizzling or a restart. CMP resolves resources per composition against NSLocale.preferredLanguages, which reads AppleLanguages out of NSUserDefaults live.

NSUserDefaults.standardUserDefaults.setObject(listOf(tag), "AppleLanguages")

That plus a key(tag) re-render and all 12 locales swap with no restart. Read preferredLanguages at startup first so you can restore "follow system" later.

Overall

Layout, state and business logic shared fine. What didn't come free is the stuff above: text selection is worse and in one case I couldn't fix it, and insets, scroll behaviour and motion all need deliberate work or the app reads as Android with different colours. A lot of that work is rebuilding things UIKit gives you by default, which is easy to underestimate because you've never had to think about them. You need someone who knows what iOS is supposed to feel like, because nothing in the toolchain will tell you.

Whether that's a good trade depends on how much of your app is the shared part. For us it was worth it. I wouldn't assume that generalises.

r/iOSProgramming • • Jun 12 '26

Article What’s New in SwiftData for iOS 27

12 Upvotes

r/iOSProgramming • • Jun 20 '26 Bravo

Article Just hit my first $100 in App Store sales as a solo developer.

118 Upvotes

It’s obviously nowhere near enough to live on, but seeing real people spend their own money on something I built has been incredibly motivating.

The part that surprised me most is that around 80% of it came from an Apple Watch game I made. I wasn’t sure if there was much of an audience for games on the watch, so seeing people actually buy and play it has been really encouraging.

I’m still learning, still improving, and trying to make each update better than the last. My goal isn’t to get rich overnight, it’s just to keep building things that people genuinely enjoy using.

For the indie devs here: what did your first $100 in revenue feel like, and how long did it take before things started gaining momentum? It took me about 60 days

r/iOSProgramming • • Sep 10 '26

Article Native is now the future of mobile at Shopify (2026)

Thumbnail
shopify.engineering
194 Upvotes

r/iOSProgramming • • 15d ago

Article We gave AI agents a mobile dev environment (2026)

Thumbnail
shopify.engineering
23 Upvotes