SwiftData Suprises

I've started working on a new app as a side project and because WWDC was just a few weeks ago decided to play with some of the new APIs introduced – namely SwiftData. Consider this class:

@Model
final class Company {
    var name: String

    @Relationship(inverse: \Person.company)
    var employees: [Employee] // `Employee` is also a class annotated with @Model
}

My surprises came when I started adding some tests around my new models. For somewhat contrived reasons, let's say that when a company gets created there is 1 employee.

func test_newCompany_hasEmployeeAttached() throws {
    let company = Company(name: "Great Co.")
    XCTAssertEqual(1, company.employees.count)
}

This test fails in 2 ways:

  1. I get a crash at the init on the first line because there is no configured container.
  2. I get a crash when accessing the array (which should have 1 auto-inserted value).

I fix the test by adding the container, and then inserting the company in to the container's context:

func test_newCompany_hasEmployeeAttached() throws {
    let context = // create the container

    let company = Company(name: "Great Co.")
    context.insert(company)

    XCTAssertEqual(1, company.employees.count)
}

What stands out to me is that it sure feels like SwiftData classes are your own classes. But they're not. They gain a conformance to PersistentModel and have all of their persisted properties rewritten with generated getters and setters. So yeah the class does not inherit from NSManagedObject but it's also not a class that is unencumbered from implicit behavior to be aware of.

I think this is teaching me that there are nuances to using SwiftData and macros in general.

self.employer = Adobe()

Just under 2 years ago, after being laid off from Lyft, I got to rejoin Zulily. During my time at Lyft I learned very much about the craft of app making. Especially at the lower levels. Build systems, tooling, app architecture. And while I was at Lyft I couldn't help but think over and over "if only I knew this when I was at Zulily...". So rejoining Zulily was a great opportunity to apply what I had learned from Lyft and help take Zulily's iOS app to the next level. Well, I've accomplished much of what I sought to do there.

Now there's a new journey ahead of me. Today is my last day at Zulily and on Monday I start a new one with Adobe.

A good friend of mine joined Adobe back in September and told me many times that I should apply. I took his advice and my first day is Monday. I'm super pumped that I'll be working a bunch in SwiftUI – I have much to learn! Everyone I spoke with during my interviews were fantastic. I asked everyone what they liked most about working at Adobe and they all said the people. That everyone is willing to lend a hand when it's needed. Egos are checked at the door and you work to help your coworkers ship something great. I'm so here for that kind of place.

I'll miss my colleagues at Zulily but I'm so pleased with how I've been able to help out that project over the past couple of years. I know I'm leaving it better than I found it, and that's not nothing.

Onwards to Adobe!

Powered by Tailwind

I've been able to take time off from my normal duties at Zulily and learn something new. That thing has been Tailwind CSS. I originally heard about it from Jordan Morgan's exploration of it from the perspective of an iOS developer and was intrigued. I'm not any CSS wizard by any stretch and Tailwind looked like a cool way to simplify what I've done before and maybe even make some things better.

The week went well over all! It took me what felt like a long time to get used to some of the core concepts like using all the utilities it gives me, but I'm happy with the outcome – even though the results don't look much different from the previous layout. Once I got my mind around using the configuration file, and then processing my input CSS into my final output things really started clicking. I could use my same fonts, and using the typography plugin I was able to easily just let the system lay out m text way better than I ever could by hand.

I'd be remiss too if I didn't mention the wonderful Tailwind UI templates that helped me along my way too. They are a really nice way to get started and piece together some super functional sites. I'm not using Vue or React so I had to do some adapting of the HTML templates (and if I need to, I'll have to write any JavaScript to interact with them by hand).

I also did a bit of refactoring of my leaf templates to simplify them a bit, wrote a couple of new command line utilities to help me make & publish new posts, and put in my first GitHub Action which will publish new posts as I commit them to the repository.

On the whole it's been a very productive week. My next task will be to upate my Taphouse using Tailwind as well – and I'll be making heavy use of those Tailwind UI components to get that done.

Tenets of iOS Phased Releases

One of the more confusing aspects of releasing iOS apps on the App Store has to be phased releases. There is pretty sparse (and confusing at that) documentation which leaves much room for questions – which in turn lead to more questions still. My aim with this post is to help clarify some core tenets of this process and hopefully spare you and your team the confusion that has come to myself and my team.

Let's start by establishing some principles:

  1. Only one version of your app can be on the App Store at any given time.
  2. A phased release lets you slowly allow users to auto-update to a new version at a slow percentage.
  3. New users and manual updaters can always get your newest version from the App Store.

When you decide to phase a release (Apple Documentation), you're specifying the percentage of users who will be auto-updated to that release. The cadence is over 7 phases (1%, 2%, 5%, 10%, 20%, 50%, and 100%). Generally one phase equates to one day, but the release can be paused on a phase for up to 30 days. The happy path is making a release that gets phased without problem over 7 days and you're done.

Let's run through a scenario where things go on the not-so-happy path and see what happens.

Say we have an app and version 5.1 which is at 100%; new users and all updates will get this version. On Monday we release 5.2 using the phased release. This means that 1% of automatic updates will receive v5.2, but new downloads and manual updates can be performed to get 5.2 as well.

On Tuesday (phase 2, which is 2%) we identify an issue with 5.2 and decide to pause the rollout. This will stop the automatic updates from happening to 5.2 (but again, new users and manual updaters can get it). We identify a fix and submit it with version 5.2.1. That release also gets phased. So what happens now? We've had 5.1 available to everyone, 5.2 at a 2% automatic update, and now 5.2.1. Here's what happens:

  • 5.2 gets replaced on the App Store with 5.2.1: new users and manual updaters can only get to 5.2.1.
  • 5.2 also completes its phasing, but no additional users can get to it (see principle #1 above).
  • 5.2.1 begins a new phase – it does not replace the phasing of 5.2 that we had in place already – and it is at 1% at release.
  • There will be some subset of users on version 5.2 who will not be auto-updated to 5.2.1 for "some amount of time".
    • It is likely possible for a user to have been in the first 1% of 5.2, and also to be in the final 50% of 5.2.1 (the documentation is very unclear about this).

Phased releases can be a great tool to help control the rollout of new versions of your app. Before we had them we just had to make a release that went to everyone and had no control over that cadence at all. But there are some nuances that can easily trip up app developers and their teams alike. I hope this has been a helpful walkthrough in the life of a phased app release and brings clarity to questions that you may have encountered along the way.

Xcode Project Generation: A Primer

My first day at Lyft a few years ago delivered quite a shock: Our iOS repositories didn’t have Xcode projects checked in. Instead, we relied on manifest files that would define each of our targets and handed those to a tool which would create the projects for us. While this seemed really weird to me at first – and admittedly took some getting used to – I quickly game around to this being a great way to work. It shifted the source of truth in the Xcode project from the Xcode project file itself to these manifests as well as the arrangements of files on disk. And there was the added benefit of no more conflicts to resolve when merging project changes from the main branch.

At Zulily I have been able to bring the same philosophy (with a few twists, but more on that another time). Our project was started in 2012 and we still have much Objective-C laying around. Since ours is an established project I had to answer the question: Where do I start?

My aim in this post is to answer that question with a few helpful tips that I picked up along the way (with a shout out to the Tuist docs.

Extract Your Build Settings

The first step in de-emphasizing your project file is to remove as much from it as possible. The lowest-hanging fruit are build settings. Thankfully the wonderful BuildSettingsExtractor by James Dempsey is there to help you out. It will examine each target you have and create xcconfig files for each of your target configurations, as well as a shared file that each configuration file imports.

Once you have these files extracted you can attach each file to its correct target configuration using Xcode. I’d even suggest taking this one step further and having a script run to verify no build settings creep back in to your project file.

Clean Up Your Files

The manifest files I described earlier point to globs of files in your repository which will be come sources & resources for Xcode to assemble your targets in their final product form. If you have files strewn all about the repository then it will be difficult to pinpoint exactly what files are contained in any given target. It’s a good idea to determine how you want your target directories to be organized at the end of this project, and start implementing that now.

Here’s how I went about this cleanup at Zulily:

  1. The first thing I did was to run the excellent Synx tool on our repo. Our project file version was old enough that Xcode did not move files on disk when we moved them in the project, so things had gotten way out of whack. This tool ensured that we were working with files in their proper places from the start (for the most part).
  2. Using Xcode I created the directory structure for each target and started moving files & resources around. Most of our files & folders were a single level deep in their target’s hierarchy so I started by creating the top level Sources directory and moving our sources in there. I would then look back in the Finder and see what files were left over. 95% of the time I could just delete them straight away – if Xcode didn’t know about them then they weren’t affecting our app.
  3. Start building the project manifests in parallel to the file reorg. By building the manifests for each target (defining where to find the sources, resources, target build phases, etc) I could ensure that at the end of the day I would have a buildable project from our collective manifests.
  4. One of the big things I got from the Tuist docs was to order my operations by target dependency count. I started with targets that had 0 dependencies, got their files organized, and was able to have a project and a building target right away. This helped me gain momentum in the project.
  5. Once a target was organized, able to build using the generated project and still in my Xcode project, I made a PR changing just that target in our main branch. I wanted to keep all this churn as incremental as possible rather than dumping a PR that touched literally every file in our repo. So instead of one massive PR I had many smaller (but still big) PRs that landed in sequence before the one which generates our project file landed.

Evaluate Your Options

There are a couple of main tools that can be used to make Xcode project files (without also affecting things like your build system): XcodeGen and Tuist. I’ve now used both, and each has their merits and drawbacks. For Zulily’s project I went with Tuist. The main reason was because of its support for Test Plans (XcodeGen’s support hadn’t landed yet). For my personal projects I’m still using XcodeGen though and don’t have any real plans to make a change at this point. Here’s a rundown of some benefits & drawbacks that I’ve found for each:

XcodeGen

  • ↔️ Manifests are all defined in yml or json files.
  • ✅ Super fast project generation.
  • ✅ Templates greatly enhance flexibility & reuse of things like target definitions.
  • ✅ Lightweight app built in Swift with binary releases available for download. It’s super easy to integrate into your repository.
  • ❗️ Manifests only have so much flexibility because they aren’t defined in code.
  • ❗️ Errors can be hard to debug (yml is a good but somewhat picky format).

Tuist

  • ↔️ Manifests are all written in Swift.
  • ✅ It’s extremely opinionated but uses those opinions mostly helpfully.
  • ✅ You get a framework called ProjectDescriptionHelpers which lets you add whatever helpful code or additions you want to make your manifests as simple as possible.
  • ✅ There’s a fantastic community building up around it.
  • ✅ Can bundle itself and all of its dependencies with one command, making pinning to any given version (or commit hash) super simple.
  • ❗️ Generation is much slower than XcodeGen. This is because Tuist is attempting to do more for you, but sometimes I found that it gets in the way and I have to work around it when the Tuist way of doing things differs from my own.
  • ❗️ Errors can also still be hard to debug, because you can’t (currently) pause the debugger when running the manifest generator.

It’s hard for me to prescribe which tool is right for your particular environment. I’ve been an XcodeGen user for a few years now – that’s what Lyft used – and it’s been great. I’ve been diving more into its advanced features and putting more weight on my manifest files and it doesn’t bat an eye. My project generates incredibly fast. My personal projects will likely stay on XcodeGen for a long time.

I’ve also found Tuist to be really nice to work in, and in a future post I want to talk about my Tuist-centered approach to how I’m breaking the Zulily codebase into modules and what that API looks like that I’ve built out in Tuist. I’ve found that having running code, being able to define functions, and the extra things that Tuist gives us at Zulily delivers the power of something like bazel’s BUILD files without the hassle of switching build systems. It’s been the right call for Zulily.

Wrapping Up

Putting in some work at the beginning of your project can pay major dividends. By slimming down your existing project file, and getting your repository structure in place before (or while) building out the manifests to generate your project you are setting yourself up for success. I hope this has been a helpful primer to get you started down the road of generating your Xcode projects!

Working From My iPad

Since the beginning of the year, I’ve been MacBook-free. The M1 Macs that came out at the end of last year blew my socks off (I’m far from alone here, I know) and I knew that when the new MacBook Pros came out with the Apple Silicon updates inside that I would immediately want to get one. The 16” machine that I had been using for the year prior was really good, but I knew that its resale value would crater immediately when the new models came out.

So I did something fairly rash. I sold it, and all this year I’ve been doing any “real work” from my 11” iPad Pro.

Honestly the whole thing has gone pretty well. Better than I had expected it to. I did get an M1 Mac mini for my office desk (which is outside of my house) and I access it remotely using the wonderful Screens apps by Edovia. I’ve been working on a new cross-platform Mac & iOS app and the lion’s share of that work has happened from my iPad connected to that Mac mini.

None of this would have been possible without last year’s introduction of the Magic Keyboard for iPad Pro. Having a very good keyboard coupled with the fantastic pointer support on the iPad is a game changer. I really do love this accessory. The only thing that I wish was different about this setup would be the front camera. Why is it still on the “top” side, when we all know that these iPads are used in landscape nowadays. The camera should be on the top side when docked in the Magic Keyboard. Oh well.

This is definitely an experiment that I’m happy to have undertaken. I’m excited for new Apple Silicon-powered MacBook Pros (and I’m actually considering getting the much-rumored 14” model instead of another 16” after experiencing the 11” iPad) and this little iPad has been a great companion along the way of that transition.

Apple Silicon Macs: Who Goes First?

With Apple's announcement last month of a processor transition from Intel CPUs to "Apple silicon" CPUs (please don't let that be the real name for these things, Apple) we are entering a new phase for the Mac. While there are many things we know about how this transition is going to go down we don't actually know what Apple silicon Macs will look like or which Macs will come first this fall. So what I want to do here is speculate about which Mac(s) might get blessed with the new hardware later this year.

Let's start by looking at the existing lineup, shall we?

Desktops

  • Mac Pro: Just introduced last year, not often updated in general, and likely the model with the most outstanding questions about what an Apple silicon version might look like.
  • iMac: New models were announced last year but they've sported the same case design since late 2012 – and even that was a very mild redesign of the 2007 redesign, which itself was a very mild redesign that harkens back to 2004. There are rumors of new iMacs imminently and some evidence of them showing up in benchmarks.
  • iMac Pro: Introduced at the end of 2017 with nary an update since, the iMac Pro is an oddity in the current lineup. I hope it's not a one-hit wonder but an iMac update may fold it in to the more broad lineup.
  • Mac mini: It was "updated" this year but that was just a storage bump (having just purchased a new one, it lists itself as a 2018 model). Mac minis are not updated very frequently but it's due for a bump.

Laptops

  • MacBook Pro: MacBook Pro's have been revised fairly recently, with the 16" model coming at the end of last year, and 13" models coming just a couple of months ago. There are rumors of a 14" MacBook Pro to replace the 13" but I wouldn't say that these computers are screaming for an update (especially since the butterfly keyboard fiasco is now behind us).
  • Macbook Air: These may be the best MacBook Air's that Apple has ever shipped. They are now retina, with the good keyboard, and at a good price for a configuration suitable for most people. The Air was just updated a couple months ago alongside the MacBook Pro.

So, with that landscape in mind, which is the best candidate for Apple silicon? That's actually really complicated.

  • Apple could wait until this fall and revamp the iMac, bringing its case design forward to the modern age and giving it new life. It's worth noting that when the Intel transition came about the iMac was the first machine to get the new CPUs – a mere 3 months after its previous revision. If Apple brings out new Intel iMacs this summer don't be surprised if they revise them again in the Fall with Apple silicon chips.
  • One of the big gains from this new architecture is battery life, making the new chips perfect for the MacBook Air. I mean, who wouldn't want to see an Air with a legit 20 hours of battery life? That seems like a reasonable computer we can see after the transition.
  • Apple also in all likelihood wants to show off the power of their new Mac CPUs. The DTK that many developers are playing around with right now has the 2018-era A12z inside, and in emulation mode that is benchmarking comparable to 2015-era Macs. Once we start seeing apps built for Apple silicon and running on 2020-era CPUs I'm guessing the performance is going to be stunning. Even the MacBook Air I just talked about may get performance close to today's MacBook Pro. Apple surely wouldn't want its Pro laptops being outperformed by its consumer notebook.

So here's my prediction:

Apple is going to revise every Mac this fall with Apple silicon – save the Mac Pro. Yes, even the Mac mini will come along for the ride (it's the chassis for the DTK after all, so it seems simple enough for Apple to put a different board in it with production-ready CPUs on it). I think that Apple becoming their own Mac CPU vendor is going to lend itself to even more secrecy and it seems like they have a lot up their collective sleeves.

Will this happen? I don't know, and honestly I doubt it. But it's fun to speculate nonetheless. And it's also a really fun time to be a Mac user.

Back to the Zu

I'm officially off the market! As of this Monday, June 29, I'm rejoining the team at zulily as an iOS engineer. Things have changed a wee bit since I left and instead of having one mobile team to handle the app's duties they have started implementing vertical "squads". So with all that said, I'll be on the Discover Squad. What that means exactly, I'm not sure. I'll start to find out on Monday!

It's going to be fun going back to a place where I already have many friends – and I've gotten to meet some of the newer folks over the past couple years of Xcoders meetings. I'm excited to take some of the good things I learned at Lyft and bring them back to zulily.

I'm excited to start this new chapter. Onward!

On the Market

TL;DR: I got laid off last Wednesday and am on the job market for an iOS developer job. Here is my résumé 🙂

Last week started like any other work-from-pandemic kind of week, but then came Wednesday. Lyft laid off 17% of its workforce including me. It was pretty shocking – especially at first – but there was actually a calm that came over me pretty quickly about what was happening. Don't get me wrong: the situation itself was incredibly chaotic between sorting out logistics of what comes next, talking things over with my now-former team (who was entirely blindsided by this), and still having the regular day-to-day things happening around the house like Atticus's therapies. Life goes on.

In the days since the layoff I've come to realize how amazingly blessed I am by the community that Jesus has put around me. Many friends have said incredibly nice things about me in re-tweeting my original post about being laid off. My email inbox blew up from 3 messages on Wednesday morning to over 55 at its peak late Thursday. I've already had a few recruiter calls and have more lined up this week. I hope and pray that a new job will come along soon.

In the meantime I've got a short contract project to work on with a couple of good friends that I'm super excited about, a calendar that suddenly has become a lot more complicated, and a few things that I need to get listed for sale. Onward to my next adventure!

Thinking About Default iOS Apps

There's a report that came out this week from Bloomberg that says Apple is "considering" the idea of allowing users to pick third-party apps to be their default for things like email, music, and web browsing. This really does feel like a good thing for Apple to do, though I do laugh a bit at the "considering" word. If they are truly considering it then it's likely to miss iOS 14 which I imagine has been feature-locked for some time. The question is: what does setting a default app mean?

  • In Messages, tapping an http or https link would presumably take you to the browser you've set as default. This could be Safari, Chrome, or iCab.
  • Tapping a mailto link would open up your email app. There are plenty out there which could supplant the default Mail.app (which is what I use and personally find to be fine).
  • Asking Siri to play some music could launch Spotify without having to add "with Spotify" to the end of your query.

And there are many other possibilites here that I won't bother listing. As a user I think the time is definitely right for Apple to introduce a feature like this. But what about as a developer?

The big question that I have revolves around code that would normally put Safari in your app. The class to do this is SFSafariViewController and it literally puts a Safari view in your app. This allows for the system to use your content blockers and other installed extensions. It also locks the domain to the one provided by the developer when they brought it on screen.

If one of the benefits of setting your default browser to (let's say) Chrome so you have access to all the Chrome things all the time would it be jarring for a user to see Safari inside of their Twitter app when they tap on a link? Would Apple provide some kind of hook inside of SFSafariViewController to actually show the content of a third-party app like this? If I've purposefully gone and set Chrome as my default browser then I sure don't want Safari ever getting in my way.

There's also similar code around MFMailComposeViewController for sending emails in apps (I use that in Scorebook) as well as MFMessageComposeViewController for sending iMessage or SMS messages. Would they allow users to pick their email or message client and slide up those experiences as well?

If Apple does allow these third-party apps then it will be the result of a mountain of work touching who knows how many parts of iOS. I'm excited to see what we get in iOS 14 come WWDC this year. We should be hearing logistical details about the conference in just a few weeks. 🍿