Once and Only Once (Part 1 : Apps )
(Started this before WWDC23 which introduced new widget stuff, but I'm ignoring that for now).
I had an idea for an app. Super basic and simple, the actual app was a dirt simple todo app where you add and remove strings (reminders) to a collection. What was interesting (to me) about the app was that I didn't much care about the data entry part of the app, what I wanted was a widget everywhere pointing to the same shared data source and showing me just one random thing from the list.
I'm quite a disorganised person and I find it works best if I glance down at my phone and randomly see "buy toothpaste" or "fix Hope's headphones" rather than a convoluted list of context aware things. Sometimes the random reminder is a time when I can do the job right then, so I do. If I can't do it right then, at least it keeps it floating around in the front of my memory.
So anyway, dirt simple app, could be horrible, just a way to add strings, nothing fancy. But widgets for the phone, phone lock screen, Mac, and watch complications.
So I figured SwiftUI was selling the "one Multiplatform app" and widgets were just views, so I figured I'd try an experiment to see how I'd get Xcode to work with a project that could make all those apps and widgets for the different platforms, and then reduce duplication wherever I found it and share code/configuration wherever I could. Through the course of this I won't be adding any custom code for my app, I just want to imagine Xcode had a single "Multiplatform App and Widgets Everywhere" template.
Disclaimer: No tests here, certainly not a production app, this is literally an experiment the sole aim of which is to produce an Xcode project that can build an app and a widget that runs on all of Apple's platforms (I'm writing this three days before the keynote so maybe there will be a headset by the time I publish) with the smallest number of target-specific files . Along the way I'll highlight things that seemed odd or counterintuitive. Or places where Xcode sees the platforms as separate or the same, regardless of how we might think of then. There may be perfectly sensible explanations but at the time they surprised me so I'll mention them. Lastly, I did this once already over a couple of days, hacking my way there. This is my attempt to revisit what I did a commit at a time and wrote down the steps.
It's all here in this repo which I'll add to as I add more commits and posts.
Step One : File->New->Project
Let's make a new project in Xcode. We'll pick a Multiplatform app and call it JustOneThing. Allowing the version control option gives us our first commit.
Step Two : Watch This
We'll add a new target, select a watchOS app, and call it JustOneThingWatch (Xcode gets upset if you call it the same JustOneThing even though that's what I want it to be called.
It's a WatchApp for our existing JustOneThing iOS app. (Multiplatform app really)
We can make our second commit here and we can now run on a phone or iPad, a Mac, or a watch. You can try the various simulators or your real devices is you have them.
Step Three : TV
Let's add tvOS support. Unlike the watch, this doesnt need a new target. We select the App target in the Project settings and on the general tab we can see "Supported Destinations" at the top. Let's select the little + and add AppleTV. Xcode says it is making changes to certain things to adapt to the new platform, but it doesn't actually do the thing we need and if you try to run it on the TV simulator it complains that the watch app is embedded and that's not allowed for a TV app, though it was ok with Mac support.
To fix that we make a change near the bottom of the general tab of the JustOneThing target settings to say that the watch app should only be embedded when the platform is iOS. Select the filter icon beside "Always Used", uncheck "Allow any platform" to enable the individual checkboxes, and ensure only iOS is selected.
At this point we make our third commit. We now have a single project with two targets. JustOneThing, a Multiplatform app which runs on Mac, iOS and tvOS, and JustOneThingWatch, a watch app which runs on watches. Both of these are set us as apple defaults and we haven't changed any code yet.
Notice one thing.
Aside from the entitlements file (the yellow checkbox-like icon) both targets are identical. They contains the same files, with exactly the same content. The only difference is the name of the app. This is only going to get worse when we start adding widgets, but lets make our third commit here - with an app for all four platforms building and working and showing us a globe and a message.
Step 4: Merge
OK Lets try and get rid of some of this duplication. Create a new group (with a folder) at the top level of the project. We'll call it AppShared.
We'll drag the JustOneThingApp, the ContentView and the Assets from the JustOneThing folder to the AppShared folder..
And then we can delete those same files from the JustOneThingWatch folder
We need to tell Xcode that those three files should be included in BOTH targets and we do that in the "target membership" section of the sidebar. Check the watch app target for the App, the ContentView and the Assets.
To make sure the Asset catalog is useful for all platforms, we'll need to add a single size watchOS app icon to the AppIcon imageset.
And the tvOS AppIcon and Top Shelf Image assets,
That all takes us to our next commit. The app should still build and run on all four platforms as before.
Step 5: Is this real?
Just to check we're really using the same file in multiple places, let's add a tiny bit of code to the content view to show something different on each platform. This isn't massively useful but it's a simple case to demonstrate how to use a single file across platforms which may be useful later.
At the bottom of the ContentView file we'll add little helper to let us distinguish iPad and iPhone.
and then inside the content view we can replace the "Hello World" line with this.
If we build and run you should see an appropriate message on each platform, all built from a single App and ContentView. We can make another commit here.
Step 6: Trust me
The next thing I want to do is remove the "Preview Content" folders because we're not using them at the moment and I really want to strip it down before I build it up. When I delete the PreviewContent from the watch app, there's actually nothing left and git will (essentially) delete the folder when I commit. I could drop to terminal and create a .gitignore or something in the folder, but I happen to know that when I eventually want to share data between iOS, watch and widgets I'm going to need an app group, so I'll need a separate entitlements file for the watch app. Right now the entitlements file is just a placeholder, but trust me.
Select the "Signing and Capabilities" tab of the watch target, and click "+Capability"
We're going to double click on "App Groups" to add the AppGroups capability to the watch app. Eventually we're going to watch to share data between the apps so it seems a safe bet we'll need them both in the same app group. This (as mentioned above) also usefully drops a file in the watch app folder so we don't lose it between commits.
If you used app groups before you'll likely see a few appear. Don't click any of them yet, we're fine with it being empty. Its created an entitlements file in the right folder and that's good enough for us for the moment.
Now switch to the main app target and follow the same steps to add app groups to that app. Again, no need to select anything yet, just enough to have the capability exist.
Interestingly, for the main app we see app groups appear twice. Once for iOS/tvOS (you can see I've used app groups for iOS before) and a separate entry from macOS. Single app, single entitlements file, separately configured.
We're now able to delete the Preview Content folders without losing the Watch App to git.
Delete the entries from Xcode and move to trash.
We now need to remove the references to them from the build settings.
In build settings for both targets the setting is "Development Assets" under "Deployment" but you can just search for "Preview" and they'll show up.
Just delete the "JustOneThing/Preview Content" value to leave the setting empty. (Both targets).
With that we have stripped away everything we don't need leaving just the stuff we do, better organised.
Each app target has an entitlements file, and the rest is in a single shared folder since the App, the Views and the Assets are all shared across all 4 platforms, so lets commit.
This post is so long, or maybe so image-heavy that Wordpress is actually struggling to let me edit it, so I'll stop here and start a second part.