A document-based app in Swift Playgrounds for iPad

Dec 28 2021 2:00 PM

It’s been just a couple of weeks since Apple introduced the new Swift Playgrounds 4 for iPad, which now enables full app creation and publishing directly from an iPad, but many people are already making some really interesting projects with the app.

Of course bringing app creation into a new platform that’s much more limited in general than macOS and Xcode means that Apple had to prioritize which app creation features to port over, and the things that they didn’t want to do “the Xcode way” just because that’s how it’s been done in the past.

This doesn’t mean that Swift Playgrounds can’t eventually become the “Xcode for iPad” that many people want, but it does mean that old-school developers like myself ¹ have to adapt to this new way of working if we want to take advantage of the new app. You may also choose to just not use Swift Playgrounds at all, and that’s fine.

I’ve been working on a little app that I’ve been wanting for myself for quite a while, and the release of the new Swift Playgrounds right before the holidays was the perfect excuse for me to dust off the iPad Pro and finally use it for something creative.

The experience so far has been really enjoyable. The performance of the code editor and autocompletion on my A12Z-powered iPad Pro is just fantastic, way better than Xcode on my M1 Macs, which has improved quite a bit, but is still not as good as the Playgrounds app on iPad. I’ve even learned some new iPadOS multitasking tricks that I didn’t know, now that I’m actually using it for a few hours at a time instead of picking it up once a week for a couple of minutes just to test something out.

After I had developed a good chunk of my app’s functionality and UI, I realized that it would make for a more streamlined workflow if the app was document-based, instead of a shoebox² type app. I don’t have lots of experience with making document-based apps for iOS, and even less experience with making document-based apps in SwiftUI, so I used Apple’s sample code as a guide.

One of the key aspects of creating a document-based app is declaring your app’s custom file type, or the standard file types that your app can handle. In order for LaunchServices to be able to know that your app handles a given file type, you have to declare support in your app’s Info.plist. Here’s what such a declaration might look like in Xcode’s Info.plist editor:

Declaring an app's document types in Xcode

That’s simple enough. However, I couldn’t find a way to declare a custom document type for my app in Swift Playgrounds. I thought that maybe Apple had moved that into the Capabilities editor, but didn’t find anything there.

Something I tried was to use a standard Uniform Type Identifier in code instead of declaring it in my app’s Info.plist. In my case, I was encoding my app’s document with a JSONEncoder anyway, so I just used the .json UTI in code, and it did work.

However, not everything worked. The main thing that wasn’t working as expected was the auto-save functionality you get when you create a document-based app in SwiftUI. My document was being saved, but in very unpredictable ways, and it would simply lose my edits quite frequently, especially if I opened the document, made a bunch of edits in quick succession, then closed it.

I was almost giving up when I remembered this excellent article by Aaron Sky where he unpacks the extensions that Apple has added to Swift Package Manager in order to support the creation of iOS apps. Here’s the extension that declares the .iOSApplication product type:

extension PackageDescription.Product {
  public static func iOSApplication(
      name: String,
      targets: [String],
      bundleIdentifier: String? = nil,
      teamIdentifier: String? = nil,
      displayVersion: String? = nil,
      bundleVersion: String? = nil,
      iconAssetName: String? = nil,
      accentColorAssetName: String? = nil,
      supportedDeviceFamilies: [PackageDescription.ProductSetting.IOSAppInfo.DeviceFamily],
      supportedInterfaceOrientations: [PackageDescription.ProductSetting.IOSAppInfo.InterfaceOrientation],
      capabilities: [PackageDescription.ProductSetting.IOSAppInfo.Capability] = [],
      additionalInfoPlistContentFilePath: String? = nil
    ) -> PackageDescription.Product
}

Notice the additionalInfoPlistContentFilePath in there? Sounds like exactly what I need.

The only problem is that it’s not exposed anywhere within the Swift Playgrounds app, so I had to use my Mac in order to edit the Package.swift file. It’s worth noting that the file itself has a warning at the top telling you that you shouldn’t edit it manually, so consider what I’m going to show here a temporary hack until Apple adds native support for including custom Info.plist content, or some other way to declare an app’s documents that doesn’t require messing with the Info.plist file directly.

So here’s what I had to do:

While you’re there, might as well add the ITSAppUsesNonExemptEncryption key with a value of NO so that you don’t have to go into App Store Connect in order to release every new internal TestFlight beta (assuming your app fits the criteria, of course).

Definitely not very straightforward, but the fact that this capability is there gives me hope that Apple plans on exposing this functionality in the future. I filed FB9824788 requesting this feature.

If you’d like to see an example, here’s a repo on my Github with a document-based Swift Playgrounds app that uses this technique.

Again, this is a hack, and it will probably break every now and then such as when you add new package dependencies to your app, so be careful.

Another big caveat that I’ve noticed is that, when uploading the app to App Store Connect through Swift Playgrounds on iPad, it will overwrite your Package.swift configuration, so the additional Info.plist data won’t be there. So if you decide to use this hack in your app and upload it to TestFlight, you’ll have to do that from Xcode on the Mac.

One more thing®: the built-in previews in Playgrounds stop working when your app has a DocumentGroup as its root scene. To workaround this and keep using previews, you can create a separate scene just for previews and use a regular WindowGroup that just displays whatever the root view of your document editor UI is.

Update: According to someone who works in developer tools at Apple, editing additionalInfoPlistContentFilePath is the right way to customize things such as supported document types, but support for that in the current version of Swift Playgrounds is still a work in progress, hence why it'll sometimes remove that property when the package manifest changes. I have filed FB9824864 for this specific issue.

Even though this is a risky hack, I'm using it in the app that I've been developing. Worst case scenario, I can just port it over to Xcode and publish it from there if this breaks.

I hope you found this post useful. If you've been working on a cool new app using Swift Playgrounds, let me know.

PS: This is probably my last post of the year, so Happy New Year!

¹ Older folks who know me might be laughing at me calling myself “old-school” given that I’m not even 30 years old yet. I get it, but keep in mind that old-school in tech is different from old-school in real-life. I learned the basics of programming in DOS with batch scripts and Pascal, and started to learn Mac development in Objective-C back when Interface Builder was still a separate app from Xcode, I think I can call that old-school

² This is the term that Apple used to employ in the Human Interface Guidelines when describing Mac apps that manage their files for you, apps such as iPhoto and iTunes, as opposed to document-based apps such as Pages, Numbers, or TextEdit