History Repeats

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 developer’s 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!

Dynamic Type on the Web

This site now supports Dynamic Type on iOS and iPadOS. If you go to System Settings on your iPhone or iPad, and change the setting for Display & Brightness > Text Size, you’ll see the change reflected on this website.

This is a big win for accessibility: many folks make this adjustment on their device to match their abilities. Just because you can read a tiny font doesn’t mean that I can. It also is a win for consistency: my site’s font size matches the other text that a visitor sees on their device.

The best part is that this improvement can be realized with only a few lines of CSS:

html {
  font-size: 0.9em;
  font: -apple-system-body;
  font-family: "Avenir Next", "Helvetica Neue", sans-serif;
}

What’s going on here?

The font-size property sets the default text size for the page. All browsers recognize this setting and so do you.

The new addition is the font property with the -apple-system-body value. This font is the key to getting support for Dynamic Type. This feature has been in WebKit for almost a decade and is fully documented. This property overrides the font-size that was defined in the line above and our page now has a size that matches the system setting for body text.

One unfortunate side effect of the font value is that it also sets the page in the system font. I like San Francisco, but I don’t want it on my blog.

With a hint from Mastodon, it occurred to me that I could override the face with font-family. So I now have the best of both worlds: a size that makes my visitor happy and a font that makes me happy.

One other addition that I made to my CSS was a tweak for desktop browsers. There is no Dynamic Type setting on macOS (yet?!) and the default size was a bit small for my taste. A @media rule fixed that:

@media screen and (min-width: 801px) {
  body {
    font-size: 1.2rem;
  }
}

Now any browser window that’s wider than 800 points will get a slightly larger font.

You can, of course, use any of the other predefined font values, such as -apple-system-headline or -apple-system-footnote, but you’ll also need to override the family with each use.

But it’s likely that you’re already using em and rem sizes so that elements scale correctly in other contexts. By setting the base size in the html element, my rule for headers “just worked”:

.entry-header h1,
.entry-header h2 {
  font-size: 1.4em;
  ...
}

Another important point: if you’re using WKWebView or SFSafariViewController on an Apple platform, it will have the same capabilities as you’ve seen above. This means that you can have dynamic text in a SwiftUI view and a web page that matches exactly. This is why I needed to solve the problem in the first place.

Take a moment to look at your blog, product, or company style sheet and think about how this approach to accessibility can improve things. If you’re like me, in a couple of hours you’ll have a much better site.

App Store Subscriptions and Family Sharing

A toot by my friend Casey brought back some frustrating memories about expired subscriptions that haven’t expired (yes, really). This blog post will hopefully help you avoid having these same recollections.

It all begins when a customer contacts you with a screenshot that looks something like this:

Your code and the App Store don’t agree about when a subscription expired. The cause of this is Apple’s StoreKit sample code. It’s likely that you have some code similar to line 246 of Store.swift:

subscriptionGroupStatus = try? await subscriptions.first?.subscription?.status.first?.state

That code will work fine until you encounter a customer that has Family Sharing enabled, as most do. The issue is that the Product.SubscriptionInfo can contain multiple items, and the code above only checks the first one.

How can that happen? With Family Sharing, the people who are using the subscription act independently: one may subscribe for a year and then cancel. Then another could subscribe at a later date for only a month. You have to check all of the subscriptions, not just the first one. Something like this:

if let statuses = try? await subscriptions.first?.subscription?.status {
    let checkStatus = statuses.first { $0.state == .subscribed || $0.state == .inGracePeriod }
    ...
}

The documentation and sample code doesn’t say it, so I will: Apple’s StoreKit sample doesn’t support Family Sharing.

If you’re looking for code from Apple that does support Family Sharing, you can find it buried in one of the WWDC demo apps. Obviously.

What’s most frustrating about this situation is that you know it exists if you’ve read the documentation:

The array can have more than one subscription status if your subscription supports Family Sharing. Provide the customer with service for the subscription based on the highest level of service where the state is subscribed.

Which makes no sense until you’ve read the paragraphs above.

Actually, I was wrong. The most frustrating thing about this situation is that it’s essentially untestable. You can’t reproduce the problem, even after a customer lets you know they’re having issues and you’ve read this blog post. That’s because there is:

  • No way to test this in Xcode (even if it’s turned on in .storekit configuration).
  • No way to test this in TestFlight (fake purchases don’t use Family Sharing).
  • No reasonable way to test this in production (red flags will be raised with refunding and changing purchases repeatedly while testing with real Apple IDs).

The StoreKit test harness in Xcode has been a godsend, but in this case it’s just not up to the task. And the result is lots of frustrated developers who are testing code in production on a customer’s device.

Apple folks: you can learn more in FB13212468. It’s been closed as “Investigation complete” — maybe you should ask Casey if he agrees with that resolution.

Tapestry: What About?

On Mastodon, Alex Chaffee points out some of Tapestry’s shortcomings. These are all valid concerns and I’ll deal them individually here (rather than with a long toot thread).

iOS-only

Building for iOS first is a strategic choice. There is a lot of work to do here, and many new concepts that need to be designed and developed, so we’re picking our battles carefully.

Personally, I’d prefer to do a macOS client first, but iOS is a choice that makes the product available to as many folks as possible.

We also get asked a lot about supporting Android and that’s something, like macOS, that we will look at after we have a strong footing with this new concept.

But what about the web?

Another platform we get asked about frequently: can Tapestry be a web app? We covered this in a FAQ, but I’d like to add a little more detail here.

The only way a web app could be done is if there’s a server to marshal requests to the various services. Since we want the privacy and independence of a device-only design, that option is off the table.

If you’re working in a single browser window as a web app, you put all of your code into a single secure JavaScript context. That’s not a problem, but any code you pull in will need things to communicate with all of the services, and in most cases that will leak private authentication data like OAuth/JWT keys and tokens. All it takes is one malicious plug-in to make your life hell.

The root of the problem is that Tapestry’s fundamental design is different than a web browser. The easiest way to think about: it’s an app that asks a bunch of browser tabs if they have anything new to show. Each tab is secure and none of them know what the others are doing. The results that each tab provides are aggregated and displayed to the user.

This design is wholly different from the web we’ve known, but is just as flexible and adaptable. Building a new thing for the community of the open web is what excites me most about this project. Giving folks tools to be creative is what the web has always been about.

Closed source

Right now, the core of Tapestry is closed source. We have put some components up on GitHub and are also fully documenting an open API to that proprietary core. Teaching is a part of that openness.

Again, we are in the very early days of this project and have no idea where things will head. It’s like if you had asked me in 2006 if a shithead would be running the show in 2024.

I think it’s important to think about how Netscape Navigator was a proprietary product until Mozilla happened. Establishing a new idea is a first step; letting it flourish is another.

All I can say at this point is that I’m fully committed to letting this idea flourish.

Scraping

There is no such thing as a proper web scraper for authenticated content. It’s a cat-and-mouse game where the scrapee wins in the end. We have every reason to believe that Facebook or Instagram will go to great lengths to protect “their” data.

On the other hand, if you want to get standardized information from a page, such as OpenGraph, that’s already something we’re doing.

Posting

The prototype I built (named Muxer) was able to post to Mastodon and Micro.blog. But it’s a harder problem than you first think.

The issue is that each service has a different set of capabilities. On Bluesky, you can’t post video. On Mastodon, you can post polls. With RSS, you can’t post at all. Size limits and file formats differ wildly (including between Mastodon instances).

You quickly end up in a situation where a user interface gets confusing. For example, you have a video ready to post on Mastodon and decide that you’d also like to send it to Bluesky. As soon as you do that, the post button gets disabled and it’s hard to explain to a user why that happened.

It’s also not clear in my mind if cross-posting between services is a good thing or not. Once you have an app that can display information from many different sources, it quickly gets annoying to see duplicates.

As we learn more about this product, and what people want from it, we’ll have a better idea of how to handle posting.

And more…

I’m always happy to talk about Tapestry and explain the thought processes behind it. If you have questions or concerns not covered here, please feel free to reach out on Mastodon.

And, of course, everyone at the Iconfactory would love your support for Project Tapestry.