Swiftjective-C: Swift, iOS, SwiftUI, and Indie Development

Swiftjective-C is Jordan Morgan's independently published blog about building excellent software for Apple platforms. It includes practical Swift, SwiftUI, UIKit, accessibility, App Intents, architecture, design, and indie development articles, plus complete code samples and long-running notes from shipping iPhone, iPad, and Mac apps.

// Written by // // Read it in about 3 minutes // RE: Swift

This post is brought to you by Clerk. Add secure, native iOS authentication in minutes with Clerk’s pre-built SwiftUI components.

Now that we’ve seen Apple’s new flagship device, and have the simulator to boot (🥁!), I’ve been diving in on adapting Elite Hoops for the new form factor. As Cupertino & Friends™️ note in their tech talks, we’ve been here before. Over its ~20-year life, we’ve seen the iPhone grow taller, become shorter, lose a home button, gain some ears, and then some. Those of us who’ve been around likely already have developed a precocious ability to listen to Apple to handle these changes, and use Auto Layout/safe area/etc. etc. etc.

And so, after playing around with the device and coming up with a design plan, there’s one word I keep returning to: expressivity. This device allows you to express what you already offer in new and, in many cases, better ways. Much like when adapting an app for iPad, you don’t gate features by device, you let them grow and mature with the form factor.

Here’s a preview of what I’ve done. But first, you need to set your codebase up a little. Let’s start with a few things that may screw you if you’ve put these types of things off:

  • Using device idiom checks.
  • Relying on UIScreen.main — it’s going to be deprecated soon.
  • Not supporting landscape orientation.
  • Using .leading / .trailing toolbar placements for close buttons. Use ToolbarItem(placement: .cancellationAction).
  • Not relying on size classes.

All of those will torch you here, but luckily, there’s a built-in skill to help with all of this: app-resizability. It’ll help you address each issue listed above.

The App Resizability skill in Xcode helping prepare an app for iPhone Duo.

Now, let’s get to the design changes, pose by pose:

Closed

The closed pose will be easiest for most of us. It’s basically a chonky iPhone:

Elite Hoops running in iPhone Duo's closed pose.

Here, you’re mainly looking at bars: navigation bars, tab bars, toolbars — that kind of thing. That’s because of the nascent vertical bar setup, and the fact that the system will sometimes need to overflow items. The question you need to ask is: what’s most important, is there anything that should always show, and what can be over…flown (is that a word?).

Things that need to stick around? Use the .topBarPinnedTrailing placement. Do you want it to win out over other things when an overflow is needed? Reach for .visibilityPriority(.high). Or, overflow ahead of time with ToolbarOverflowMenu.

Opened

This is where the fun starts, and we get an opportunity to express existing features differently. Here, someone is viewing a public play. But — I’ve got more room to play with! So, let’s make use of it:

Elite Hoops adapting a public play view to iPhone Duo's opened pose.

Using the new ArrangementView, along with @Environment(\.toolbarVerticalEdge), there’s an opportunity for more data density. And, that’s what you’re seeing here: let’s show the play notes or its attachments — stuff that supports the primary view. Because now, there’s more room for it.

I think this is where the bulk of your work comes for iPhone Duo. Don’t simply aim for making your app work on it, make it freakin’ baller! And, I think this is one way you can do it. Another example:

Elite Hoops play designer with larger, more widely spaced tools on iPhone Duo.

The play designer in Elite Hoops is where the bulk of coaches spend their time. It has several tools, but needs to keep the focus on the court, and where players should be. With Duo, I’m able to make two critical improvements: space out those tools in the toolbox, and make them bigger.

Continuing the theme of data density, and the ability to spread it out — let’s look at the changes for when a coach is running practice:

Elite Hoops practice screen using iPhone Duo's extra space to emphasize drill information.

Here, I’m able to make the important things stand out a little more. How much time is left in this drill? What’s next? What are the drill’s steps? It’s all made a bit easier to parse on Duo.

The part that I am currently undecided on, however, is how liberally to deploy these arrangement view refactors. Do I always leave them in, and let iOS decide? That’s one part I’ve not yet worked out.

Rotated

For rotations, I’ve found that my existing size-class-based decisions (in terms of how to present things) held up remarkably well. For example, for the digital whiteboard, it now takes advantage of Duo’s real estate to expand and have a richer area for coaches to mark up with their Apple Pencils:

Elite Hoops digital whiteboard expanded across iPhone Duo in the rotated pose.

Not much to say here, I’ve found that it essentially mirrors what your iPad implementation does today. As it should.

Folded

For folded situations, there wasn’t much to address. It leverages the arrangement view in most cases, and in some cases, it doesn’t, on purpose. I’m in love with how it looks, though. For example, here’s the play designer from earlier:

Elite Hoops play designer adapting to iPhone Duo's folded pose.

One thing I want to emphasize is that the system does the right thing nearly every time, as long as you didn’t reinvent the wheel. Things like the new reserved area and staying away from the fold in these folded situations — the system does all the work. Let it.

Revisiting old assumptions

Designing for iPhone Duo already pushed me to make some navigational improvements for Elite Hoops that I’ve wanted since launch: using a split view. I’ve put this off more times than I can count, but it fit in with all of the other work I was already doing:

Elite Hoops using a split view on iPhone Duo.

Plus, I had to revisit UI that looked great on modern iPhones but was no longer at home on Duo. For example, these bottom rounded corners look quite nice and are concentric with iPhones, but they look out of place on Duo in my opinion:

Elite Hoops UI with rounded bottom corners reconsidered for iPhone Duo.

You’ll quickly find places where you should’ve used a Label with an image and title. I cleaned up all such instances so that they show correctly in overflow menus with a more descriptive string supporting what the action will do.

Final thoughts

And that’s a wrap. If there’s one thing the iPhone Duo has been for me, it’s fun. I have to get one of these! It’s a business expense, after all. Sometimes there’s a serendipitous moment where what you want and what you need end up being the same thing, I guess. The Duo is an exciting little device to design for, and it’s not every day we get new form factors to bring about some fresh new ideas or new angles.

Until next time ✌️

···
// Written by // // Read it in about 3 minutes // RE: Swift

This post is brought to you by Clerk. Add secure, native iOS authentication in minutes with Clerk’s pre-built SwiftUI components.

It’s time for Apple to get into the… fold… with foldables. Anyways, on the day of the announcement, Cupertino and Friends™️ dropped six new videos over developing, designing, and crafting great apps for the nascent device:

  1. Design for iPhone Duo
  2. Prepare your app for iPhone Duo
  3. Raise the bar with iPhone Duo
  4. Strike a pose with adaptive layouts on iPhone Duo
  5. Leverage multiple displays and scenes on iPhone Duo
  6. Build a great camera experience for iPhone Duo

And there’s some ground to cover, because it seems like this thing can contort your app’s layout about 7,000 different ways:

Mail adapting to six iPhone Duo layouts, including closed, open, landscape, portrait, and partially folded poses.

I’ve only watched one, and the other five are next for me to marathon, but in the interim I had Astra check out each one because I’m excited and impatient. Here’s what it thinks the critical things to know are, with the obvious caveat that this is at the discretion of an agent.

let concatenatedThoughts = """

These takeaways come from Apple's English video captions. Some new iOS 27.1 API reference pages weren't available at writing; documentation links below point to available references, with timestamps for the new APIs.

"""

astra / session notes
AI-generated summary

The following section was generated by Astra from Apple's video captions. Follow the linked videos and documentation for the original guidance.

Build With iOS 27.1

Existing apps run, but rebuilding with the iOS 27.1 SDK enables edge-to-edge content and vertical bars. Use Xcode 27.1’s Device Hub to test opening, closing, rotating, and partially folding; its updated App Resizability skill also covers SwiftUI and Duo.

Size Classes Beat Device Assumptions

Duo remains an iPhone even when its inner display has regular width and height size classes. Use size classes and local view or scene geometry; avoid layout branches based on idiom, interface orientation, or UIScreen.main. The inner display doesn’t honor supported interface orientations for normally resizable apps.

Keep the Same App Inside and Out

Use the extra space for columns or richer navigation while preserving the same hierarchy and functionality across poses. NavigationSplitView and UISplitViewController adapt automatically. A tabletop layout can put media above controls, but opening or folding the phone shouldn’t be required to access a feature.

Safe Areas Are Asymmetric

Vertical controls can occupy either edge, especially in Split View. Handle each safe-area inset and layout margin independently: keep interactive content inside, let backgrounds extend behind bars, and test your app on both sides of the inner display.

Let Containers Move Your Bars

Use bars managed by navigation and tab containers; standalone UIKit bar instances don’t participate in the new vertical layout. Supply both titles and symbols so the system can choose a representation. Custom controls need an explicit vertical opt-in and must fit the bar’s fixed width; meaningful text can stay horizontal.

Decide What Overflows First

Toolbars compress before tabs by default; task-focused screens can change that preference. Give frequent actions and status-bearing items higher visibility priority, and consolidate custom overflow actions into ToolbarOverflowMenu or UIKit’s additionalOverflowItems so users get one system overflow menu.

Move Controls Away From the Fold

System sheets, alerts, and menus already avoid the fold. For custom layouts, query reservedRegion on GeometryProxy or UIView: division regions describe the fold, while occlusion regions describe obstructions such as the active inner camera. Reposition important controls; continuous scrolling articles, feeds, and lists don’t need displacement.

Arrangements Handle Two-View Layouts

ArrangementView and UIArrangementViewController adapt two views around available space and the fold. Choose split when both views must remain unobscured, or overlay for a foreground/background relationship. Arrangements provide layout, not navigation: avoid placing navigation containers inside them or embedding arrangements inside lists and scroll views.

Hinge Angles Drive Effects

onHingeChange and UIHingeInteraction expose discrete hinge states and continuous angles for interactions like a musical pitch bend. Handle a missing hinge on other devices and reset effects when appropriate. Use arrangements and reserved regions for layout decisions.

Multitasking Can Change Both Dimensions

Side-by-side apps reduce width; pinned Picture in Picture can reduce height. Duo also supports multiple instances of your app, but new windows can only be created on the inner display. Handle scene-request errors; UIWindowScene.ActivationAction automatically hides when opening another window is unavailable.

Camera Apps Can Use Both Displays

CameraCaptureAccessory can show a subject-facing preview or teleprompter on the outer display while the main camera UI occupies the inner display. It requires a full-screen app on the inner display and an active camera session. Register it with the camera view and observe availability changes.

Choose Automatic Camera Switching or Full Capabilities

Front-camera discovery using wide or ultrawide types returns a virtual front camera that switches between displays automatically, limited to 1080p/60fps and no depth. Access the physical cameras individually for depth or the outer camera’s higher limits—up to 4K and 120fps—but manage switching yourself.

Camera Position Isn’t Camera Direction

A camera marked .front can face away from the person using your app. AVKit’s AVCaptureDeviceDirectionCoordinator reports direction relative to a view; use one per display view. Its callback runs on the main actor: pass the sendable AVCaptureDeviceDescriptor to your camera actor before reconfiguring capture, then update preview mirroring and UI.

Revisit Preview Framing and Rotation

Use preview videoGravity to choose fit versus fill, and dynamic aspect ratios to exploit the square front sensors. Adopt AVCaptureDevice.RotationCoordinator for upright previews and captures across display changes; after adopting it, disable camera-sensor-orientation compensation to improve performance.

Until next time ✌️

···
// Written by // // Read it in about 1 minutes // RE: The Indie Dev Diaries

This post is brought to you by Clerk. Add secure, native iOS authentication in minutes with Clerk’s pre-built SwiftUI components.

Ah, Spend Stack. I’m pleased to announce that everyone’s favorite finance app/running total list thingy/do-it-all-calculator-with-1000-use-cases-but-not-one-clearly-defined is coming back from the dead…again! The previous owners, who had let me open source it, have given me back full IP rights. She’s back home:

Spend Stack restored to Jordan's App Store Connect account.

If you need a refresher, Spend Stack was my first indie darling. It was well received among press and had a few fans over at Apple, but I never could quite figure out what exactly it was (spoiler, I still haven’t) or how to monetize it. It was originally sold around the covid-era, with a blog post from me covering the details here:

And, so it is, nearly six years later, I guess Spend Stack has a new home, again. Which is back home! For now, I’m not going to change much about it, here’s my plan:

  1. This thing is old. Most of it is in Objective-C. I have Codex on a loop modernizing it now. I’ll get it buiding in a Swift-first rewrite.
  2. Then, I’ll integrate modern APIs such as GRDB and CKSyncEngine.
  3. I’ll make the app free (it launched as a paid up front app), with an optional $9.99/yr sub for cloud sync.
  4. And once it’s stable, I’ll open it up for TestFlight.

It warms my heart that, years and years later, I’ll still get a random, out-of-the-blue Spend Stack message from someone. I don’t know where it goes from here, but I can bring it back as it once was without too much hassle in the age of agents. And, if we’ve become internet friends post-Spend Stack era, here’s most of what I wrote about it if you’d like to catch up:

Here’s to new, old beginnings!

Until next time ✌️

···
// Written by // // Read it in about 1 minute // RE: The Indie Dev Diaries

This post is brought to you by Clerk. Add secure, native iOS authentication in minutes with Clerk’s pre-built SwiftUI components.

For a long time, the homies and I have joked about starting a podcast.

Spoiler: we will never actually start a podcast. But, it would be fun though.

The idea of a podcast is tantilizing. I love talking shop, writing about indie life, or chatting with anyone about the ongoings of our insane vocation. And while I’ve written on this blog for years now, I do love talkin’.

So, introducing…

Indie Walks

Short form video essays/journals/episode/talk show??? from me over indie stuff. So, MRR, web to app, how to manage time as a parent with side projects, that kind of stuff. You can watch the first episode wherever is best for you:

  1. X
  2. Threads
  3. Instagram
  4. Episode Directory

And, as linked above, I’ll keep that evergreen page up to date here with each new episode. I hope you’ll follow along, and feel free to shoot over any questions or suggestions for topics.

Until next time ✌️

···
// Written by // // Read it in about 12 minutes // RE: AI

This post is brought to you by Clerk. Add secure, native iOS authentication in minutes with Clerk’s pre-built SwiftUI components.

Skills are a paramount puzzle piece to my agent usage. The rub? They are too easily fragmented:

  1. You could be using different agents. Claude, unlike other agents, looks at its own spot for skills. Everything else uses an open standard, ~/.agents/skills.
  2. Then, there’s different machines you could be running. Symlinking is great, but run npx skills update on your MacBook at home and then your MacBook on the go is now stale.
  3. Each harness seems to want to funnel you into different paths for installing skills.

Is there an open standard to solve? Surely, if there is, it would have to be cloud based. For example, at Superwall, we simply have GitHub repo for proprietary, shared skills. So, I’ve adopted that same pattern for two things:

  1. The skills I’ve personally made, and just as important…
  2. Skills I’ve installed via Vercel’s skills.sh.

The whole process hinges on three skills: push-my-skills, pull-my-skills, and sync-npx-skills. So, uhhh, naturally - I made a Windows XP recreation to show you how the process works. You got two computers, each needing to be current on 3rd party skills from skills.sh, and my own personal ones. Click on any of the skill buttons below to see how they operate:

Skills Sync Center

My Network Places Choose a job below to watch it travel.
At home

MacBook A

My source filespersonal-skills/
Agent copiesClaude · Codex · more
npx lockWhat is installed here
The handoff

GitHub

Skills I madeDreamingInBinary/skills
Third-party manifestDesired state + baselines
On the go

MacBook B

My source filespersonal-skills/
Agent copiesClaude · Codex · more
npx lockWhat is installed here
THE WHOLE IDEA

Two kinds of skills. One cloud handoff.

Your skills no longer depend on whichever Mac or agent you happened to use last.

  1. 1

    Skills you authorPush the real files up; pull them down elsewhere.

  2. 2

    Skills you installPublish the list; reconcile each Mac against it.

  3. Installed folders are copiesGitHub is the durable source of truth.

Ready. Pick a job to run.

Here’s the technical breakdown.

For the skills that I’ve made… …one GitHub repo is the source of truth; so everything inside ~/.agents/skills is merely an installed copy. On one Mac, push-my-skills refreshes those copies, commits my changes and pushes them to GitHub in one shot. On another, pull-my-skills pulls that repo and reinstalls the same skills for every agent. Now, my personal skills are source controlled and current across machines.

But, that’s just half of it. Third-party skills actually use a shared manifest instead…

…so sync-npx-skills compares that manifest against the Mac’s last sync and its current npx lock file, determining whether a difference was made here, or on another machine. Local additions and removals are published; which means remote changes are applied so each Mac eventually converges on the same set of skills without manually copying folders around.

At this point, I have to ask: Did I solve the problem? Absolutely. Did I engineer myself into a small pyrrhic victory along the way? Also absolutely maybe yes! My hope is that an open standard arrives tomorrow, and makes all of this gloriously obsolete. Until then, at least both Macs agree on what skills they have.

Until next time ✌️

···