FB19914338

I submitted the following feedback today. If you ever plan to change your business model from a paid up-front to freemium model, read this report and avoid a day of headache and stress.

Title: The sample code for a business model change was written by someone who’s never submitted an app to the App Store

Please describe the issue and what steps we can take to reproduce it:

The source code example using in Supporting business model changes by using the app transaction does not work if you’re using current Xcode and App Store conventions. Additionally, the sandbox environment uses the same outdated conventions.

And when you use that sample code, that you cannot test in the Xcode transaction simulator or in the TestFlight sandbox environment, it will fail spectacularly on launch day. You will be inundated with support requests from people who are expecting to see a payment for the previous version AND you’ll be in a state of panic because YOU HAVE NO IDEA WHAT THE HELL IS GOING ON. And did I mention that you can’t test this in production?

The sample code implies that the originalAppVersion is a string that’s separated by periods (“.”). The sandbox environment returns a value of “1.0” which reinforces this notion that it’s a value that separated by periods.

It is not.

If you’d read the Xcode documentation, you’d know that it automatically generates an app’s Info.plist. This has been the default setting for quite while – most developers have no idea this is a configurable option: they fill in the “Version” and “Build” number in the target’s General settings and are done with it.

For most, the build number will just be a single number that increments each time you submit to TestFlight (and eventually to the App Store).

When GENERATE_INFOPLIST_FILE is enabled, it sets the value of the CFBundleVersion key in the Info.plist file to the value of the build number (CURRENT_PROJECT_VERSION, or the “Build” in General settings). And that means your Info.plist is getting a CFBundleVersion without periods.

So what happens when you use this code?

let versionComponents = appTransaction.originalAppVersion.split(separator: ".")
let originalMajorVersion = versionComponents[0]

Well, if you’re an inexperienced Swift developer, your app is going to crash with an array index that’s out of bounds. Those of us who are more careful in our receipt processing code will skip over the originalMajorVersion because versionComponents is empty.

And that’s when the emails from customers start arriving.

Luckily, there is this nugget of information describing originalAppVersion:

The originalAppVersion remains constant and doesn’t change when the customer upgrades the app. The string value contains the original value of the CFBundleShortVersionString for apps running in macOS, and the original value of the CFBundleVersion for apps running on all other platforms.

So even though CFBundleVersion was originally intended as a major/minor/patch format, its current use is as a single integer that increments when you submit to TestFlight. So the code above is expecting “1.0” and is actually getting “83”.

(And why the hell is it different on macOS? You do realize that cross platform apps are a thing, right?)

Again, you have no way to test this theory other than going through App Review (with an expedited review if you’re lucky). And if you’re even luckier, you’ll have folks on Mastodon that will confirm that this sample code is a piece of shit. A few hours later you’ll breathe a sigh of relief when folks start telling you that things are working fine.

And then the next day, you’ll write this bug report and post it publicly because no one else should have endure the stress caused by this sloppy code.

Liquid Glass. Why?

Whether you love it or hate it, there is no shortage of opinion on Liquid Glass. I have thoughts about what it is, but today I want to focus on why it exists.

Apple’s public rationale for the new design language is that it offers a universal solution across platforms that takes advantage of recent hardware advances. Its touted benefits are a more lively experience that puts a greater focus on content.

The transition from iOS 6 to 7 was used as an example of how Apple has been successful with visual refreshes. But I don’t think that applies in this case.

In late 2012 and early 2013, there was a movement outside of Apple to simplify user interfaces: the highly textured designs introduced with the first iPhone had run their course. You can see this in our own work with Twitterrific 5 and other apps like Vesper. The launch of iOS 7 in 2013 was startling to some designers and developers, but not everyone. There was clearly a need, and the app ecosystem has benefitted from this change for over a decade.

I’m unaware of anyone outside of Apple who’s thinking “we really need to have more fluid glass in our designs”. Of particular note during the introduction is how much time they spend showing off glass blocks and talking about the physical effect itself. While not addressing the most important question: “why do we need this?”

And I’m pretty sure the answer is “we don’t”. The answer is “Apple does.”

I’ve spent the last few months updating Tot for iOS 26 while watching Sean do the same thing with Tapestry. One thing that’s clear from this work is that you never want a control or container that touches the edge of the screen.

It’s like when safe area insets appeared in iOS 11: it wasn’t clear why you needed them until the iPhone X came along with a notch and a home indicator. And then it changed everything.

There has also been an emphasis on “concentricity”. It’s an impossible thing to achieve and an easy target for ridicule. But it’s another case where Apple wants to take control of the UI elements that intersect with the physical hardware.

All of this makes me think that Apple is close to introducing devices where the screen disappears seamlessly into the physical edge. Something where flexible OLED blurs the distinction between pixels and bezel. A new “wraparound” screen with safe area insets on the vertical edges of the device, just like we saw with the horizontal edges on iPhone X.

The user interface work of the past few months will all make a lot more sense, and developers who haven’t been paying attention will have their “holy shit” moment.

I can see this new physical design being very successful with touch-oriented devices: it will feel natural with a phone, tablet, or watch. Hardware and software becoming one in classic Apple fashion.

I’m having a much harder time seeing how Liquid Glass will benefit other platforms like the Mac or Apple TV (where Apple doesn’t even make the screen). Forcing tactility where it’s not needed or wanted feels like a misstep.

Other challenges, like infusing your own branding into an app with clear buttons will be easier to reason about once the reality of the hardware drops. Until then, stay away from the edges and wait for Apple to reveal the real reason for Liquid Glass.

iPadOS Windows

The first thing I installed after the WWDC25 Keynote was the beta for iPadOS. There was only one reason: it had the windows we have all wanted for so long.

And generally, windows on iPad work exactly how we want them to.

But there’s a problem, and I suspect that the root cause is that Apple engineers are thinking more about how the iPhone has worked for the past 18 years rather than how the Mac has worked for the past 41 years.

From the very beginning, iOS has had a notion of an app being in the foreground or background. When you saw an app on screen it was active and when it was gone it was inactive. The operating system let you know when that state changed and developers used it for all kinds of things:

  • Saving the state of the app. For example, if you are in the middle of editing some text that is only in memory, the data gets written to local storage because there is no guarantee that your app stays in memory. No one likes losing several paragraphs of typing because they switched to Safari and loaded a heavyweight page.
  • Syncing state between devices. Apps like Tapestry and Ivory do this with the reading position so you can switch between devices seamlessly.
  • Exchanging data with a server. To make apps more responsive, developers often queue up work to be performed after you leave the app. This work can be done at a lower priority in the background.

It was simple system that let you do what you needed to do, when you needed to do it. Now with windows on iPadOS, that’s gotten a lot harder.

That’s because apps stay active even when their windows do not.

If you’re using iPadOS 26 and noticing that the saving/syncing/exchange of data is not happening, there’s a stupid trick you need to do to get things working:

Tap on the home screen to hide the windows (they slide off to the sides of the display). That makes all the apps on screen inactive and triggers the work that they need to do.

Of course, that’s a completely unintuitive action, hard to remember, and generally a pain in the butt. Especially when you’re on an iPad Pro with a lot of screen real estate and have several apps working together nicely.

(Note that this “hide to sync” issue is also a problem when you’re running iOS/iPadOS apps on your Mac: you have to hide a window to make it inactive. It breaks Tapestry.)

There is an API in iPadOS to track the state of each window: it has an “active appearance” that tells you when the window has focus. Unfortunately, on older versions of the OS, nothing is reported for this state, so backward compatibility is a problem. Also, on iOS 26, the active/inactive window state changes unpredictably while an app is off screen: my suspicion there is that the OS is taking screenshots to use in the App Switcher.

It’s taken several years for folks at Apple to arrive at a solution that the Mac had long ago, and I think it’s time for them to re-evaluate the notion of what makes an app “active”.

On macOS, two kinds of things can be active: an application itself or one of its windows. An app is active if it’s been launched and has at least one active window. A window is active if it’s frontmost on screen.

There is a clear distinction between the overall activity and the content-specific activity. If there is some global data that needs attention, you work with it when the app becomes active. If a window has some document data, you know to update it when the active state changes.

iOS and iPadOS need that same clear distinction.


For any Apple folks reading, take a look at FB18443571. It shows how I worked my way through this problem and came to the conclusion above. I’m happy to talk more about this problem and any potential solutions.

History Repeats

Updated June 16th, 2025: I’ve never been happier to be wrong about something as I was on the eve of WWDC25. History did repeat, but in a good way.

As with the iPhone SDK, Apple decided to open up its language models for all developers to use. This lets third party developers leverage all the features available on each platform: we are on equal footing.

Additionally, App Intents have become much more useful, thanks to their adoption in Spotlight. The use cases for human–oriented activities are much more concrete.

While I still have concerns about the business aspects of being a developer in today’s ecosystem, the technical situation is exactly where it needs to be. Well done, Apple!


I’ve been developing on Apple products for a long time: typing PEEK and POKE code from magazines into an Apple ][, figuring out how QuickDraw worked using the Inside Macintosh pre-prints, having my mind blown by Mac OS X and every new thing prefixed with “NS”, and then jailbreaking the first iPhone so I could write an app that eventually won an Apple Design Award.

It’s been an exciting adventure. Until now.

The engineering behind Apple products continues to be amazing: Swift and SwiftUI have made it easier than ever to create products. The App Store continues to be the easiest and most vibrant marketplace to sell those products (in spite of the company’s attempt to screw that up.) Fricken’ amazing hardware, too.

So what’s wrong?

The problems we’re solving and the apps we’re writing haven’t changed in years. After almost two decades of iOS, everything is iterative. And while maturity is a good thing, it’s not the thing that gets developers excited.

We’re at the point where a big change is putting a new coat of paint on our creations. Sure, it looks nice, and customers will love it. But it’s a lot of work and none of it sparks our imaginations.

But what is exciting these days?

Large Language Models: a huge body of statistical data that can be leveraged to solve problems that have heretofore been intractable. It’s the most exciting technology in decades because it lets our imaginations run wild and create new things.

And that’s a problem for developers in Apple’s ecosystem. Because while the company has done a significant amount of research with these models, and includes one on every iPhone, iPad, and Mac, the core capabilities of the mechanism are out of reach.

It’s like if Apple’s products didn’t provide direct access to the camera. There would be no Instagram, no Zoom, no Halide, just the Camera app. Developers don’t get a shutter button: they can only access photos that have already been taken. Apple knows what’s best for customers, of course.

Developers have been in this situation before: at the introduction of the iPhone. We all saw a wildly innovative piece of hardware that immediately gave thousands of developers a revolutionary idea for a piece of software.

Maybe it was emulating a glass of beer, turning the device into a musical instrument, a game that could only be played by touch, or a way to connect millions of people using photos and filters.

Then Apple told us we couldn’t write native apps and had to make web pages instead. There was no way for developers to do the same things Apple was doing. This was, indeed, a shit sandwich.

Eventually, the company came to its senses and opened up the platform, dropped the ridiculous non-disclosure agreements, and allowed developers to do what they wanted. That led to a period of innovation like I’ve never seen: developers had something revolutionary and magic happened.

Now history is repeating itself. We have a new shit sandwich that’s called Apple Intelligence.

Instead of building our own ideas on top of an LLM, we’re supposed to provide the internal details of our apps to Apple so they can do it on our behalf.

Providing those details is a lot of busy work for developers and not nearly as much fun as the coat of new paint: at least with visuals you can see and feel the results of your efforts. And from a business point-of-view, managing their internal details is why customers pay us. If Apple starts doing that on our behalf, what perceived value do we provide?

The internal details, called App Intents, are abstract and not something where you can immediately see the results of your efforts. It’s a “trust us Siri will be great at this” situation. Given the company’s track record in this area, there are few developers who think this will be successful. Worse, the improvements will be tied to lengthy release cycles: other companies drop language models with the frequency of new Emoji, not WWDC keynotes.

(I would not be surprised to learn that this whole situation is based on a fever dream of charging monthly service fees to use Siri and Apple Intelligence. These folks are seriously underestimating the reputational damage that Siri has incurred in the past decade.)

Some developers are working around this problem by providing their own models. This is unsatisfactory because it’s a waste of device resources: the downloads duplicate a tremendous amount of memory and storage. In many cases products are relying on cloud-based LLMs and losing all the privacy and security benefits of on-device processing.

All of this feels like Safari and mobile web apps in 2008: valiant attempts that everyone knows are wrong. Doing the best you can with a shit sandwich.

There are so many transformative ideas forming in developers’ minds right now that will never see the light of day. In our case, Tapestry has megabytes of textual information that describes a person’s interests and social connections. There’s no way for us to explore mining that data in a way that benefits the customer and respects their privacy.

(The developers who are making the greatest strides in this area are all doing it on the Mac. Ideas like Sky can thrive in a more open environment. Those of us in the jailbreak scene all saw how iOS borrowed heavily from its desktop sibling. Time will tell if that can happen again, but I suspect it will not given the locked down nature of mobile.)

So where does this lack of developer creativity lead?

It feels like developers are now part of the supply chain and being optimized accordingly. We are expected to refine and improve Apple’s ideas year-over-year. Our own needs and desires aren’t even secondary (where customers sit) or tertiary (our normal place in the hierarchy). We are just expected to deliver the products when Apple needs them.

I fear that this will lead to history repeating itself again, in a much more drastic way.

I remember how Microsoft’s response to the mobile revolution was to protect their existing desktop products. That looks a lot like Apple with its iOS franchise now. Instead of setting developers free, letting us experiment, and reaping the benefits, accountants and lawyers are fighting to keep us in line. We are all tired of the bullshit and many will happily move onto something better when it comes along.

Apple has been the lucky recipient of developer attention for a long time and they act like it will last forever.

It won’t.

Making Software Fun

We’ve recently released a new product. There’s no shortage of marketing or technical information about that.

What I want to talk about today is the fun we had making it.

Tapestry was a challenge on many fronts, but I’ve found that if you add a bit of humor and mischief to development, it helps get past the day-to-day frustrations you encounter. It’s hard to be pissed off when you’re laughing.

The spinner

It all started with a fidget spinner. As we were getting our first beta release ready, Ged wanted a badge at the bottom of the timeline that said // BETA //. The initial release was functional, but there were a lot of rough edges that we knew needed smoothing. So a label there was.

On a Sunday afternoon I decided to have a little fun. A couple of hours later, our new badge recognized touches and had a very springy animation. And I didn’t tell anyone, not even my wife. That secrecy was hard, but the success of the gag depended on it.

But as soon as the people downloaded that first beta, we started getting comments like “I love the spinner!”. And no one on the company Slack had any idea what was going on until I said “tap the beta badge”.

Showing your first release to other folks is always full of surprises, even when it’s self-inflicted!

The spinner also ended up being used to test our error reporting mechanism. If you tapped it too often, which many people did, there was a message that you needed to ZAP the PRAM.

Yep, still having fun.

The disco

One of our beta testers, Joline Celebrion, is a huge fan of our iconography. More than once, she asked on our Patreon Discord about the arrival of alternate app icons.

A couple of weeks before they were ready, I added this bit of code to settings under the “App Icon” category:

I knew she’d immediately see the new category and open it excitedly, only to see a message that they were imminent. Teasing is only fun when you follow through, so in the next week’s build there was this footer below a large selection of icons:

And when she launched the app:

But we had to deal with that #warning and remove the message in the released product. And I knew it would immediately generate a bug report.

Good developers are proactive, especially when it comes to about boxes. And about boxes are branded with an icon. And on the factory floor, there is no shortage of icons. So I had my workaround: Joline was getting a disco.

The first step was to take all the icons and cycle through them to get a nice colorful flashing effect. That went out in a beta release and I hinted about it on Discord. Joline and everyone else loved it.

But that was just an amuse-bouche. I couldn’t close the bug report unless it had her name in it. I’d also been meaning to learn about the new TextRenderer modifier and protocol: I had my excuse to spend time learning and having fun.

Another important piece of the puzzle was knowing it was her tapping the icon. Luckily Kickstarter backers register their reward in the app so we had enough information to display everyone’s first name in the about box. I got to close a bug report and all our Kickstarter backers got a fun little bonus: that’s a win-win!

But it’s still Joline’s Icon Disco. She just lets everyone else visit and pretend otherwise :-)

And if you think these are the only Easter eggs, well, let’s just say that the best part of making software fun is watching folks discover the weird things we come up with!

Like if you find yourself tapping twice on the product website’s wordmark. Repeatedly!