<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[jsorge.net]]></title>
<description><![CDATA[Christian, husband, dad, developer, batman aficionado]]></description>
<link>https://jsorge.net</link>
<image><url>https://jsorge.net/favicon.png</url><title>jsorge.net</title><link>https://jsorge.net</link></image>
<generator>Maverick v0.7</generator>
<lastBuildDate>Sun, 03 Jun 2018 21:19:23 GMT</lastBuildDate>
<atom:link href="https://jsorge.net/rss" rel="self" type="application/rss+xml" />
<ttl>60</ttl><item>
<title><![CDATA[Arborist 1.1 is out]]></title>
<link>https://jsorge.net/2026/07/29/arborist-1.1</link>
<guid isPermaLink="false">LzIwMjYvMDcvMjkvYXJib3Jpc3QtMS4x</guid>
<pubDate>Wed, 29 Jul 2026 17:28:00 UTC</pubDate>
<description>Arborist 1.1 brings Grove, additional terminal support, iCloud web login, and more!</description>
<content:encoded><![CDATA[<p>What a whirlwind the last week has been since I released Arborist. I’ve gotten lots of positive comments, and it was amazing to see myself featured on <a href="https://mjtsai.com/blog/2026/07/20/licensing-a-mac-app-with-revenuecat/">Michael Tsai’s blog</a>, the <a href="https://www.revenuecat.com/blog/engineering/license-mac-app-merchant-of-record">RevenueCat Engineering blog</a>, and <a href="https://brettterpstra.com/2026/07/23/web-excursions-for-july-23rd-2026/">Brett Terpstra’s Web Excursions</a>. Thank you to everyone who has talked about Arborist and helped make the launch special!</p>
<p>Today I’m releasing <a href="https://taphouse.io/arborist/releases">Arborist 1.1</a> with support for Grove, a customizable terminal picker, big improvements to iCloud login for licensing, and more.</p>
<p>The first feature request I got for Arborist was support for <a href="https://grove.safia.sh">Grove</a>, which is a tool that I had never heard of but found that it complements Arborist quite nicely. The idea is that Grove can keep your main repo and all the feature branches you’re working on grouped together in a top-level directory, with an optional <code>.groverc</code> file that can define things like branch name prefixing and a set of commands to run when creating a new worktree. I think this would be especially helpful for me since I have a monorepo with <a href="https://mise.jdx.dev">mise</a> as my main task runner, and <a href="https://github.com/yonaskolb/XcodeGen">Xcodegen</a> as my project generator. I could see using Grove to have Arborist automatically run <code>mise trust</code> and <code>xcodegen</code> to get my new checkout up and running automatically. It’s been a really fun tool to learn about and I’m really happy with how its integration came together in Arborist.</p>
<p>Another feature request was for being able to customize the terminal application opened when resuming a Claude Code chat. Arborist 1.0 only supported the built-in Terminal.app, and 1.1 adds support for <a href="https://ghostty.org">Ghostty</a> and <a href="https://iterm2.com">iTerm2</a> as well. I looked into <a href="https://panic.com/prompt/">Prompt</a> support because I love Panic’s apps but it doesn’t appear that there is a way to open a new Prompt window to a location on the file system.</p>
<p>The other major feature that I’ve been able to ship is something I mentioned in my <a href="https://jsorge.net/2026/07/15/revenuecat-mac-licensing">licensing blog post</a> about iCloud. In 1.0, Arborist leaned on the Mac having a logged-in iCloud account in order to purchase a license. In 1.1 I’ve been able to add a web login to any iCloud ID (iCloud is still required to buy) so that you can use Arborist on a Mac that either has no logged in iCloud account or has an account other than the one you bought a license on. This has been helpful to me with my Adobe-issued work machine; once I logged in to my personal iCloud account in Arborist my license instantly activated, which is exactly what I was going for.</p>
<p>I’m very happy with how this release turned out, and there’s more in this release too – bug fixes and smaller tweaks – that you can read about in the full <a href="https://taphouse.io/arborist/releases">release notes</a>.</p>
]]></content:encoded>
</item><item>
<title><![CDATA[My New App: Arborist for the Mac]]></title>
<link>https://jsorge.net/2026/07/20/meet-arborist</link>
<guid isPermaLink="false">LzIwMjYvMDcvMjAvbWVldC1hcmJvcmlzdA==</guid>
<pubDate>Mon, 20 Jul 2026 13:33:21 UTC</pubDate>
<description>Arborist is my newest app, built for the Mac to be your command center for reusable workflows.</description>
<content:encoded><![CDATA[<p><img src="/_posts/2026-07-20-meet-arborist.textbundle/assets/Icon.png" /></p>
<p>TL;DR: My new app <a href="https://taphouse.io/arborist">Arborist</a> is now available for download and purchase!</p>
<p>Over the years, as my workflows have evolved, my tools have somewhat struggled to keep up. In any project I work in, I like having multiple checkouts — maybe I’m building a feature, fixing a bug, and doing a code review. Not necessarily at the same time (as there lies madness in the multitasking), but in different places on my drive so I don’t have to stash what I’m doing and restore it later. This workflow evolved from multiple clones to <a href="https://git-scm.com/docs/git-worktree">git worktrees</a>. Worktrees are great because they let me create a whole new checkout on my local machine without having to wait for a clone. I love how fast they are, and because they all share a common <code>.git</code> root I can easily move between branches and cherry-pick commits if I need to.</p>
<p>All of this came with a cost I wasn’t expecting as I started accumulating non-trivial shell commands to run in my repo: there’s the Python script at work which pulls binary dependencies, or the long fastlane incantation to make a build, and many others. These commands often require context which works well in a single-tree repo, but when I had multiple checkouts or worktrees I’d have to redo the setup for any location I was working in. I started to wonder if there’s some way to manage all this.</p>
<p>That idea has clanked around in my head for a while and finally I have an answer: <a href="https://taphouse.io/arborist">Arborist</a>. It’s my new Mac app which lets me take control of worktrees across my system and run any command I want. I’ve been building this app and using it every day for a while now; today it’s available for everyone running macOS 15 and above.</p>
<p><img src="/_posts/2026-07-20-meet-arborist.textbundle/assets/HomeScreen.png" /></p>
<p>I’m really happy with how Arborist has turned out. It’s chock full of features:</p>
<ul>
<li><p>Drag and drop your git repos and it automatically picks up all the associated worktrees and lists them right in the sidebar. The current branch is displayed right under the worktree name.</p>
</li>
<li><p>Rearrange and collapse sidebar items to get things organized just the way you want.</p>
</li>
<li><p>Create and delete worktrees with ease. <a href="https://www.git-tower.com">Tower</a> added worktree support a while back and while I have used that feature I haven’t loved it, because it feels like adding a whole new repository. I wanted to make worktree management seamless, and that’s just how it is in Arborist. Also, AI agents love to use worktrees but they name them <em>the same as the repo</em> so I’ve been stuck at times with 3 or 4 identically named locations and I don’t know why each of them is there. I can take the power back with Arborist.</p>
</li>
<li><p>The middle column is the superpower:</p>
<ul>
<li><p>The commands area stores any terminal commands that you want to run in your repo, and they’ll work across checkouts. I use this for both quick and longer tasks and it’s the killer feature of the whole app in my view. There’s a small console that pops open and shows the terminal output in real time. It’s <em>fantastic</em>!</p>
</li>
<li><p>Tools – at the top of the column – are the apps that you use no matter which repo you’re working in. I’m a big fan of <a href="https://nova.app">Panic’s Nova</a> for my non-Xcode projects, and I want to be able to open any worktree in Nova with a single click. Arborist lets me do just that.</p>
</li>
</ul>
</li>
<li><p>The shell environment that both commands and tools run in is fully customizable in Settings, and you can see the environment variables that Arborist uses to run commands and launch tools.</p>
</li>
</ul>
<p><img src="/_posts/2026-07-20-meet-arborist.textbundle/assets/TerminalSettings.png" /></p>
<ul>
<li><p>The inspector pane on the right side gives me details about the checkout I’ve got selected. The best part about it is the conversation recall for that worktree so I can see the sessions I’ve had with Claude or Codex.</p>
<ul>
<li><p>Clicking on a conversation launches you right back into that conversation. This proved really valuable to me when I couldn’t find a conversation in Codex but I could resume it in Arborist.</p>
</li>
<li><p>The sessions are discovered by reading the local files in their known locations, and no data about these conversations ever leaves your machine. Arborist is 100% privacy-focused.</p>
</li>
</ul>
</li>
</ul>
<p>I’ve worked really hard to make Arborist <a href="https://inessential.com/2020/03/19/proxyman">Mac-Assed</a> as well, with good keyboard support and a squircle-free alternate icon to boot (I freaking love that icon and use it myself on my machine). I’ve loved the Mac platform since the early ’90s and I want to make a Mac app that Mac people will love to use.</p>
<p>In my last post, I <a href="https://jsorge.net/2026/07/15/revenuecat-mac-licensing">wrote about</a> using RevenueCat for my licensing. I’m really happy with how that turned out, and I think the ease of tying a license to an iCloud account will make for incredibly smooth transfers to new machines.</p>
<p>Arborist comes with a 14-day free trial, and after that costs $39 for a full-feature license to v1 and all the updates in the v1 family. If and when a v2 comes, there will be a discount available to v1 owners (I don’t know when a v2 will ship, that’s all way in the future at this point).</p>
<p>I hope you give <a href="https://taphouse.io/arborist">Arborist</a> a try and that it helps you take control of your workflows.</p>
]]></content:encoded>
</item><item>
<title><![CDATA[Licensing a Mac App with RevenueCat]]></title>
<link>https://jsorge.net/2026/07/15/revenuecat-mac-licensing</link>
<guid isPermaLink="false">LzIwMjYvMDcvMTUvcmV2ZW51ZWNhdC1tYWMtbGljZW5zaW5n</guid>
<pubDate>Wed, 15 Jul 2026 14:55:43 UTC</pubDate>
<description>Going boldly where no Mac app has gone before!</description>
<content:encoded><![CDATA[<p>I’ve been hard at work at a new Mac app called <a href="https://taphouse.io/arborist">Arborist</a> lately, and one of the key decisions has been how to monetize it. Arborist is not an app that can be sandboxed, so putting it in the Mac App Store was never a real consideration (it runs commands entered by the user at arbitrary locations, which means sandboxing is right out). I had thought that I would make it a subscription app at first, but after a lot of thought I settled on a one-time version 1 purchase for $39 USD (just like the Software Days of Yore).</p>
<p>Then came the question: how do I do licensing on my own?</p>
<p>I know there are options out there like <a href="https://www.paddle.com">Paddle</a>, <a href="https://fastspring.com">FastSpring</a>, and <a href="https://www.lemonsqueezy.com">Lemon Squeezy</a> that have worked for many apps like mine over the years. But I also wanted something simple for my users. Not to mention Apple Pay was a must-have. I’ve experienced friction more times than I can count when an app doesn’t support Apple Pay, and every time it happens I am ever so slightly less happy with that app because of it. Customers are also more familiar with the smooth in-app purchase experiences provided by the App Store, and if I could achieve something that easy I wanted to do just that.</p>
<p>I also happen to be in a couple of Slack workspaces with the wonderful and friendly <a href="https://davedelong.com">Dave DeLong</a>. Dave started working at <a href="https://www.revenuecat.com">RevenueCat</a> recently and he also has a passion for good Mac apps. He mentioned in a discussion about Mac app licensing that he would love to work with someone using RevenueCat’s web SDK to power that experience – and my gears started turning.</p>
<p>After collaborating with Dave and some trial and error I was able to land on a somewhat novel – and simple – licensing implementation:</p>
<ul>
<li><p>RevenueCat serves as the source of truth for whether or not a customer is licensed.</p>
</li>
<li><p>I create RevenueCat customer IDs from local iCloud identifiers (more specifically, calling <code>userRecordID()</code> on my <code>CKContainer</code> in CloudKit).</p>
</li>
<li><p>When the user makes a purchase, they earn an entitlement that I look for in Arborist to grant them a license.</p>
</li>
</ul>
<p>The big bullet of note there is the second one. I didn’t want to have to spin up a licensing server to send out codes (much less have to store them), and I didn’t want to have an email-based system. I wished for something simple like StoreKit but without using the App Store as a backend. I had to implement some kind of identifier that I could tie easily to a user and I wanted that to be as transparent to the user as possible. Hence, <code>userRecordID()</code> made complete sense. This also allowed for “license sharing” across all of a user’s devices. So as long as a customer is logged in to the same iCloud account on a device that they made a purchase on, they’ll be licensed for Arborist. I like the simplicity there. One other great thing about this is that I don’t have to collect any personally identifying information about my customers directly. Stripe handles the payment process, and the iCloud’s identifiers give nothing away.</p>
<p>The eagle-eyed among you may notice that my <em>entire licensing strategy</em> hinges on the customer being logged in to iCloud. That’s true for my 1.0. I don’t think this is too big of a leap to make for a power-user app like Arborist. But I am working on a way to sign in to iCloud using the web so that folks who don’t have iCloud set up on their machines can still buy a copy. That should land not super long after 1.0, but it does have some more moving parts to it and so I deferred it to post-launch.</p>
<h2>Setting up the backend</h2>
<p>Getting these details in place was not immediately obvious to me, and I actually went down one path first before partly reverting and going another direction. At first I set up <a href="https://www.revenuecat.com/billing">RevenueCat Billing</a>, but that ended up being the route I didn’t want to take. I had never heard the term <a href="https://stripe.com/resources/more/merchant-of-record">merchant of record (MoR)</a> before and when I first did I didn’t know it was something to care about (spoiler alert: it very much is). RevenueCat’s product does not take care of being the MoR for me and thus would leave me on the hook for things like taxes and refunds. Being a solo developer embarking on my first direct-sale app this became a dealbreaker.</p>
<p>Thankfully Stripe has a service called <a href="https://stripe.com/managed-payments">Managed Payments</a> which does handle this for me and even better is that RevenueCat plugs right into that system out of the box. So after doing some research it seemed the better way for me to go.</p>
<p>On the RevenueCat side, I will say that I am consistent in forgetting how to set up their chain of values. I have used RevenueCat for <a href="https://taphouse.io/baseplate">Baseplate</a> and <a href="https://taphouse.io/decoder">Decoder</a> previously but getting set up for Arborist definitely tripped me up.</p>
<ol>
<li><p>In RevenueCat, create the entitlement that Arborist will look for to ensure the user is licensed for v1.x.</p>
</li>
<li><p>Add the product in my Stripe dashboard (ensuring it meets the criteria for managed payments).</p>
</li>
<li><p>Back in RevenueCat, I had to set up Stripe as a <a href="https://www.revenuecat.com/docs/projects/connect-a-store">web provider</a>. There’s a box to check to <a href="https://www.revenuecat.com/docs/web/integrations/stripe/stripe-managed-payments">use Managed Payments when available</a> that I had to check as well.</p>
</li>
<li><p>Create the RevenueCat product by importing it from my Stripe web provider.</p>
</li>
<li><p>Add a new offering which has the product inside with a lifetime duration. This will give me effectively a one-time purchase (there is a subscription “end date” that comes along in the SDK but it’s for 200 years from now so if a customer needs a refund in 200 years I probably won’t be able to help them).</p>
</li>
<li><p>That offering then gets a Web Purchase Link. This URL is what I then use to direct users to purchase in the app. It lets me specify things like the callback URL for when a purchase is complete.</p>
</li>
</ol>
<p>I’m listing these steps as much for your information as I am for Future Me in the hopes that we can all do this a little smoother the next time 😀.</p>
<p>So the whole flow goes from Settings -> License -> Buy -> Safari -> Complete purchase -> Back to Arborist -> Refresh from RevenueCat -> Done. My hope early in the process was to avoid the Safari hopping, but doing so meant that I could not offer Apple Pay. This is a <a href="https://bugs.webkit.org/show_bug.cgi?id=282078">known WebKit bug</a> from 2 years ago about Apple Pay not working in <code>WKWebView</code>. It’s a real bummer because keeping the flow in the app was so smooth when I had it working that way, but I figured the friction of going to Safari to do Apple Pay and then hopping back was much less than having to input all your payment details in the app.</p>
<h3>An Aside on Taxes</h3>
<p>One thing that has scared the daylights out of me in all this is sales taxes. I’ve never had to collect them before, let alone distribute them back to a government. Throw in dealing with non-US countries and that’s even more terrifying. I was originally looking at <a href="https://stripe.com/tax">Stripe Tax</a> to take care of things for me but something felt strange as I was exploring that route. I couldn’t ever get it working in the sandbox environment and when I did some more research I was still going to have to file on my own. Thankfully I found Managed Payments and from everything I can tell, making Stripe my MoR puts all the burden of these collections and distributions on Stripe. So I’m happy to give them a few extra percent of each sale to handle that for me.</p>
<h2>Hooking up the app</h2>
<p>The last piece to complete the puzzle is hooking all of this up in the app. It’s important to note that while the RevenueCat SDK is great, it’s also geared towards apps which use StoreKit. For my needs I’m not using StoreKit, so fetching things like my Offering can’t be done with their library. This means that I have to hard-code the URLs I need to hit (and I have both production and sandbox URLs in there for testing). So when a user clicks the buy button, this happens in my licensing view model:</p>
<pre><code class="language-swift">func startCheckout() async {
    guard isStartingCheckout == false else { return }

    isStartingCheckout = true
    actionMessage = nil
    defer { isStartingCheckout = false }

    do {
        let checkoutURL = try await licenseManager.checkoutURL()
        guard NSWorkspace.shared.open(checkoutURL) else {
            throw LicenseCheckoutOpenError.failedToOpenBrowser
        }
        actionMessage = "Checkout opened in your browser."
    } catch {
        displayState = LicenseDisplayState.resolved(
            from: nil,
            previous: displayState,
            error: error
        )
    }
}
</code></pre>
<p>There’s some state to handle to show the user that the process has started, we assemble the checkout URL (which looks something like <code>https://pay.rev.cat/{web_link_id}/{user_id}</code>) and that gets kicked out to the system in their browser of choice with the <code>NSWorkspace</code> API call.</p>
<p>When a purchase is successful on the web, RevenueCat’s backend will associate the user ID that I passed in my link above with the entitlement in step 1 above and call back Arborist with the registered deep link I put in the web purchase link. The callback handler then runs a re-fetch of the customer and validates that the entitlement to unlock the license exists:</p>
<pre><code class="language-swift">func handleDeepLinkPurchase() async throws {
    let customerInfo = try await Purchases.shared.customerInfo(fetchPolicy: .fetchCurrent)

    guard
        let entitlement = customerInfo.entitlements[entitlementID],
        entitlement.isActive
    else {
        // Handle a missing entitlement; this means the
        // transaction did not succeed
        return
    }

	// Once we get here we have a validated customer who has
	// made a purchase and the app can be unlocked.
}
</code></pre>
<p>Don’t miss the first line of that method: <code>Purchases.shared.customerInfo(fetchPolicy: .fetchCurrent)</code>. The fetch policy here is critical because without specifying <code>.fetchCurrent</code> the SDK will return cached data, and if a customer has made a purchase that won’t be reflected instantly like they’ll expect. Instructing the SDK to bust out of its cached data is the thing that will make this process feel seamless to my customers (and yours!).</p>
<p>There’s also code that looks a lot like this which runs on app launch to validate a user against their iCloud ID. If this is their first launch of Arborist and they have bought a license using the same iCloud account on a different Mac then Arborist will be unlocked automatically (there’s no “Restore purchases” button or workflow to even worry about!).</p>
<h2>Wrapping Up</h2>
<p>I think this process has gone about as smoothly as I could have hoped, given that I have never sold a Mac app directly before. I’ve also never seen anyone attempt to use iCloud as a customer identifier. So there’s still some nerves as I approach July 20, when I’m going to be launching Arborist. I know I’ve done my best to get here and if everything goes sideways on launch day, it’s still just the first day of my journey selling this app.</p>
<p>I want to give a lot of thanks to RevenueCat’s team, particularly Dave DeLong, Ed Shelley, and Delia Behr for being great partners in helping me get set up technically as well as on the content side with this blog post. I’ve long been a fan of RevenueCat’s and for someone running an operation of my size I’ve yet to have to pay them a nickel.</p>
<p>I’ll follow up post-launch with a retrospective for how things have gone and how this licensing plan has worked out. If it’s as transparent as I hope it is I think this will work out nicely.</p>
]]></content:encoded>
</item><item>
<title><![CDATA[The Trials and Tribulations of a Blog Engine Update]]></title>
<link>https://jsorge.net/2026/01/11/maverick-updates-amidst-grief</link>
<guid isPermaLink="false">LzIwMjYvMDEvMTEvbWF2ZXJpY2stdXBkYXRlcy1hbWlkc3QtZ3JpZWY=</guid>
<pubDate>Mon, 12 Jan 2026 05:01:39 UTC</pubDate>
<description>I decided to update my blog engine over the weekend (which may not have been a wise choice) and it did not go well.</description>
<content:encoded><![CDATA[<p>This weekend has been quite the ordeal. In an effort to distract myself from having to say a <a href="https://mastodon.social/@jsorge/115874577725112641">permanent goodbye to our beloved cat Molly</a> I decided to dust off the code to Maverick – my blog engine – and update all the things: from Swift 5.3 -> 6.2 and the newest versions of Vapor and its dependencies. It’s been a few years since I’ve worked on Maverick so I figured there was some work to be done.</p>
<p>Boy, was I right. And boy, did it get more complicated than I thought it would.</p>
<p>Honestly, in the whirlwind of emotions I don’t know if I remember all the steps I went though. It did lead to <a href="https://forums.swift.org/t/how-to-troubleshoot-upgrading-docker-and-swift-deployments/84062">my first Swift Forums post</a> because I was getting crashes I couldn’t make heads or tails of. The first part of it had to deal with Swift’s Docker images (or libraries that I link to in newer versions of Swift, it’s hard for me to tell) using “CPU flags” – a term I’m not familiar with – which my Digital Ocean server doesn’t have.</p>
<p>I was able to solve that by statically linking the Swift standard library in my executable. At least I think that’s what solved it. Then came thread creation crashes, and then Docker permission crashes. What I ended up doing was this:</p>
<ul>
<li><p>Created a new server, and took the opportunity to automate that process. The automation part wasn’t strictly needed, but it’s one of those things that had been in my head a while so I took the chance to do it.</p>
</li>
<li><p>Simplified my SSL approach. If I was going to create a new server to check if it built there, I would need to update my infrastructure because it built all the SSL pieces for me. I didn’t have a way for it <em>not</em> to do that. Instead I went with Cloudflare and removed SSL from the game entirely. This simplified a lot of my spin up process and made it identical whether in production or locally on my Mac. This was a big win.</p>
</li>
<li><p>Because It’s Always DNS, I had to point my nameservers at Cloudflare (and Cloudflare at my new server too). My old server was too old – 7 years at this point – so it was time to make the switch there too.</p>
</li>
<li><p>Once I deployed the new server I got some Swift runtime crashes in my code (a first for this project) and I had to fix that by updating permissions of some mounts that get made in my app’s container. The problem was Maverick couldn’t read the site’s configuration and crashed. Once the permissions were set, my site was happy again.</p>
</li>
</ul>
<p>There are probably steps along the way that I’m missing, but the gist is that my blog is back after a couple of days down. The next time I update the engine I’ll be dang sure to do it on a test server first. Live and learn.</p>
]]></content:encoded>
</item><item>
<title><![CDATA[From Zero to App in 18 hours]]></title>
<link>https://jsorge.net/2026/01/04/from-zero-to-app-in-18-hours</link>
<guid isPermaLink="false">LzIwMjYvMDEvMDQvZnJvbS16ZXJvLXRvLWFwcC1pbi0xOC1ob3Vycw==</guid>
<pubDate>Sun, 04 Jan 2026 22:03:41 UTC</pubDate>
<description>I made a new app in a very short amount of time, and had a lot of fun doing it.</description>
<content:encoded><![CDATA[<p><strong>Updated to include the prompt that I used</strong></p>
<p>Over the years I’ve always been very impressed when I hear stories of developers building and shipping an app very quickly. But for me that experience had never come together – getting the idea (which I could execute quickly), actually making the app to a standard that I’m happy with as a simple 1.0, updating all the marketing materials, and the 28 other steps that all go in to making a release. It’s a lot of work!</p>
<p>However, on Monday last week things came together. And I’d say it was a highlight of my Christmas break from work.</p>
<p>TL;DR: I’ve got a new Mac app – Decoder. It will use your Mac’s camera to scan any kind of code, barcode, QR code, Starbucks gift card, etc.. Check it out at https://taphouse.io/decoder.</p>
<h3>Backstory</h3>
<p>My wife Emily had gotten a bill in the mail that had a QR code on it so that you can pay by phone. However (and I’ve had this feeling often as well) entering the data associated with the bill is easier done on the Mac. The problem is that the Mac has no built-in QR code scanner, unlike the iPhone. So I went searching on the App Store for an app that could help her utilize her webcam to scan the code. There are a couple out there but neither seemed all that great.</p>
<p>I’ve been working with AI coding tools more (Claude Code and Codex are my current weapons of choice) and so I created a new git worktree in my monorepo and opened Claude code to it and gave a simple prompt:</p>
<blockquote>
<p>I’d like to make a new app for the Mac which turns on the camera and lets the user scan a QR or barcode. Let’s call the app <code>Decoder</code>. Take a look at the pattern of my other apps and make a new one in the Apps directory with an Xcodegen definition file. The app should also follow common Mac patterns and fit in well with the rest of the ecosystem.</p>
<p>Once we have the basic shell up and running we can talk about other features.</p>
</blockquote>
<p>The result took just a couple of minutes and out popped an app that interacted with the camera and scanned the code. I showed Emily the app and she confused it with one that I had sent her way (she had already paid the bill, I was just having fun). Her surprised look when I said that I just made this was a lot of fun to see.</p>
<h3>Claude’s Initial Output</h3>
<p>I have had a bit of experience with Claude over the past year, and I’ve built up a decently extensive set of rules in my <code>.claude</code> folder. But I have to say that the initial output from my meager prompt above was <em>very good</em>. It made an XcodeGen yml manifest similar to my other apps, and I was able to easily generate the project the first time. My monorepo is anything but conventional but it works very well for me. Claude plugged in to it and added the new app structure seamlessly.</p>
<p>The SwiftUI code it produced also was really good. It produced easy to read and reason about views, and the backing models were all in the modern <code>@Observable</code> pattern. I didn’t think it got too clever about anything. As a first examination and running of the output I’m very impressed.</p>
<h3>Polishing for Distribution</h3>
<p>I figured that it could be fun to see what it might take to get this app on the App Store. There were some missing niceties like history preservation and a nicer UI for when the camera is off. For history between sessions I leaned on the <a href="https://github.com/pointfreeco/swift-sharing">swift-sharing package</a> from Point Free. It added the package to my XcodeGen manifest and integrated it perfectly (I was able to point to my example implementations in Baseplate as a pattern).</p>
<p>Next came the app icon. I’ve used Bakery in the past to make quick icons using SF Symbols, but that’s disallowed in the App Store. Claude can’t create images, but it can create SVGs. Since Decoder is basically a full-window code scanner I came up with the concept of a Mac window with a QR code inside and Claude generated the code to make an SVG. This process actually took quite a few iterations but I got to a place I was happy enough with. The biggest creative change I had to make was putting the macOS window stoplights in the center of the title bar because otherwise Tahoe’s roundrect clipping would slice the red one right off. It took a few iterations to get the sizing correct for Icon Composer so that the SVG would fill the icon but we got there. I’m actually pretty happy with how the icon turned out.</p>
<p><img src="/_posts/2026-01-04-from-zero-to-app-in-18-hours.textbundle/assets/decoder-icon.png" /></p>
<h3>Business Time</h3>
<p>While I wanted Decoder to be free, I also knew that having a way to get a few dollars for it would be nice. I’ve used RevenueCat with Baseplate already and I’ve seen other apps implement a tip jar feature so I asked Claude to add a tip jar, with a primary action button in the toolbar. Claude spun for a little bit and did a very nice job of producing a view with buttons that can act as individual “tip” buying buttons, hooked up to RevenueCat (after having added RevenueCat’s SDK to my XcodeGen manifest).</p>
<p>The next step was to add the identifiers for my in-app purchases to App Store Connect and hook them up to the RevenueCat backend. I did these steps by hand because I’m not sure if there’s a way to do them using automation.</p>
<h3>Web Updates</h3>
<p>The next piece of the puzzle to ship Decoder to the App Store is a website. I have a decent if not overdone process for the taphouse.io website – it’s a <a href="https://vapor.codes">Vapor</a> site that could absolutely be static but is not. I use <a href="https://docs.vapor.codes/leaf/getting-started/">Leaf</a> for the templates and have one shared template for both Baseplate and Scorebook. I didn’t want to go overboard with screenshots so I decided to go with a single, simple one for now.</p>
<p>I have a separate repository for my web services (there’s the website and a small API I run), and I added that working directory to my session with Claude Code. From there I gave it the prompting to look at how my other app pages are set up and to create a page for Decoder. It went to work and reused the template I had for the other apps and made something… okay. But after some iteration I was able to get a new template spun up that Decoder could use. It also added the app to my dropdown listing and updated the home page.</p>
<h3>App Review</h3>
<p>This project began at about 5pm PST on December 29. By 11am on December 30 I had the app in a good state, tested the in-app purchase flow, and updated my website. I asked Claude to give me the marketing spiel, looked it over, and submitted it for review.</p>
<blockquote>
<p><strong>A Brief In-App Purchase Aside</strong></p>
<p>The workflow for adding in-app purchases to an app is strange and easily confusing. I say this having gone through it with Baseplate at the end of 2024, and at Adobe having gone through it with <a href="https://apps.apple.com/us/app/adobe-premiere-video-editor/id6742757464">Premiere’s</a> launch in September of last year. In-app purchases (consumable, non-consumable, or subscriptions) need to be approved by Apple before they can be sold in an app and the process for getting them approved gets bundled up with an app’s submission.</p>
<p>After I submitted Decoder for app review I found a couple of bugs that I wanted to take care of. So I did that, made a new build, and developer-rejected the existing build that I had submitted before. This took my app (which also had the 3 tip jar in-app purchases submitted as well) back to the “Prepare for Submission” state. However when I went back to submit the updated build I couldn’t find the 3 tip jar items to add back and looking at their product pages in App Store Connect showed them still waiting for review.</p>
<p>It seems to me that the process of approving an in-app purchase has much less transparency than a regular app. There’s no way to see their review status in the same way as an app, and the fact that bundling the submission with an app but rejecting the app doesn’t affect the in-app purchases is unnerving at best.</p>
</blockquote>
<p>It took until January 4 to get the app in to App Review, but once it did the review was just 30 minutes. The in-app purchases for the tip jar got approved (even though they were no longer part of the submission) and Decoder is now for sale!</p>
<p>This project was a lot of fun and seeing how well the tooling like Claude Code can help me put together an app and its marketing material – and in so short an order – was really eye opening. I have started working on another, much more complicated, app that I hope to have progress updates on in the coming months. It will take a lot more time than Decoder did but it also does quite a bit more. I’ve also dusted off an app that I started a few years ago and have been working on it too (I’m writing this post using it in fact) and may be able to polish it up for release.</p>
<p>I’m definitely feeling an energy for building apps of my own that I haven’t felt for a while and I’m excited to see how I can move these projects forward in 2026.</p>
]]></content:encoded>
</item><item>
<title><![CDATA[Meet Baseplate]]></title>
<link>https://jsorge.net/2024/12/20/introducing-baseplate</link>
<guid isPermaLink="false">LzIwMjQvMTIvMjAvaW50cm9kdWNpbmctYmFzZXBsYXRl</guid>
<pubDate>Fri, 20 Dec 2024 18:01:45 UTC</pubDate>
<description>I have a new app: Baseplate, which helps you organize your LEGO collection!</description>
<content:encoded><![CDATA[<p>I love indie software. It’s one of the things that I love about Apple’s platforms. And being an indie is something that I’ve wanted to do for a long time (no this isn’t a post where I say I’m quitting my job). So when a really cool opportunity crossed my path to acquire an exsiting app I decided to take the leap.</p>
<p>Meet Baseplate! It’s a fun app for LEGO collectors to manage their collections. Over the past few years I’ve gotten pretty heavy into collecting and building and I’ve had a great time getting to know Baseplate. I hope you love it too. Check it out <a href="https://apps.apple.com/us/app/brick-catalog-baseplate/id6737250348">on the App Store</a>!</p>
<p><img src="/_posts/2024-12-20-introducing-baseplate.textbundle/assets/baseplate.jpeg" /></p>
<div align="center">[Go get Baseplate on the App Store](https://apps.apple.com/us/app/brick-catalog-baseplate/id6737250348)</div>
<p>Baseplate was originally developed by <a href="https://x.com/JPEGuin">Shihab Mehboob</a> and released earlier this year. I’m super impressed at how he was able to build the app so quickly – and for all of Apple’s platforms (save tvOS). It’s been a lot of fun to dig in to the app and learn from a developer who did the thing that I’ve constantly struggled with: shipping the app!</p>
<p>The whole process of taking the app over has reinvigorated me for my own apps. I have put Scorebook back on the store and am working on updates to it too. I was able to rebuild my <a href="https://taphouse.io">Taphouse website</a> using Tailwind CSS as well as the newer version of Vapor. I’ve sorted out some of the things that slow me down such as build and deployment processes and have attempted to take the friction out of building apps as much as I can.</p>
<p>I’m also going to try and market Baseplate better than before. I created a <a href="https://indieapps.space/@baseplate">Mastodon profile</a> for it and will be working on features to let users post about their collection super easily.</p>
<p>So go give the app a whirl. I hope you like it!</p>
]]></content:encoded>
</item><item>
<title><![CDATA[SwiftData Suprises]]></title>
<link>https://jsorge.net/2023/06/30/swiftdata-surprises</link>
<guid isPermaLink="false">LzIwMjMvMDYvMzAvc3dpZnRkYXRhLXN1cnByaXNlcw==</guid>
<pubDate>Sat, 01 Jul 2023 05:11:53 UTC</pubDate>
<description>I'm playing with SwiftData and got suprised by a couple of things.</description>
<content:encoded><![CDATA[<p>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:</p>
<pre><code class="language-swift">@Model
final class Company {
    var name: String

    @Relationship(inverse: \Person.company)
    var employees: [Employee] // `Employee` is also a class annotated with @Model
}
</code></pre>
<p>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.</p>
<pre><code class="language-swift">func test_newCompany_hasEmployeeAttached() throws {
    let company = Company(name: "Great Co.")
    XCTAssertEqual(1, company.employees.count)
}
</code></pre>
<p>This test fails in 2 ways:</p>
<ol>
<li><p>I get a crash at the init on the first line because there is no configured container.</p>
</li>
<li><p>I get a crash when accessing the array (which should have 1 auto-inserted value).</p>
</li>
</ol>
<p>I fix the test by adding the container, and then inserting the <code>company</code> in to the container’s context:</p>
<pre><code class="language-swift">func test_newCompany_hasEmployeeAttached() throws {
    let context = // create the container

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

    XCTAssertEqual(1, company.employees.count)
}
</code></pre>
<p>What stands out to me is that it sure <em>feels</em> like SwiftData classes are your own classes. But they’re not. They gain a conformance to <code>PersistentModel</code> and have all of their persisted properties rewritten with generated getters and setters. So yeah the class does not inherit from <code>NSManagedObject</code> but it’s also not a class that is unencumbered from implicit behavior to be aware of.</p>
<p>I think this is teaching me that there are nuances to using SwiftData and macros in general.</p>
]]></content:encoded>
</item><item>
<title><![CDATA[self.employer = Adobe()]]></title>
<link>https://jsorge.net/2022/03/04/adobe</link>
<guid isPermaLink="false">LzIwMjIvMDMvMDQvYWRvYmU=</guid>
<pubDate>Fri, 04 Mar 2022 16:31:51 UTC</pubDate>
<description>I got a new job as an iOS engineer at Adobe!</description>
<content:encoded><![CDATA[<p>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.</p>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<p>Onwards to Adobe!</p>
]]></content:encoded>
</item><item>
<title><![CDATA[Powered by Tailwind]]></title>
<link>https://jsorge.net/2021/09/03/powered-by-tailwind</link>
<guid isPermaLink="false">LzIwMjEvMDkvMDMvcG93ZXJlZC1ieS10YWlsd2luZA==</guid>
<pubDate>Fri, 03 Sep 2021 21:34:40 UTC</pubDate>
<description>I got to play with Tailwind CSS this week, and updated my layout to use it.</description>
<content:encoded><![CDATA[<p>I’ve been able to take time off from my normal duties at Zulily and learn something new. That thing has been <a href="https://tailwindcss.com">Tailwind CSS</a>. I originally heard about it from <a href="https://www.swiftjectivec.com/tailwindcss-from-a-ios-dev/">Jordan Morgan’s exploration of it</a> 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.</p>
<p>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 <a href="https://github.com/jsorge/jsorge.net/blob/main/styles/styles.source.css">input CSS</a> into my final output things really started clicking. I could use my same fonts, and using the <a href="https://github.com/tailwindlabs/tailwindcss-typography">typography plugin</a> I was able to easily just let the system lay out m text way better than I ever could by hand.</p>
<p>I’d be remiss too if I didn’t mention the wonderful <a href="https://tailwindui.com">Tailwind UI</a> 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).</p>
<p>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 <a href="https://github.com/jsorge/jsorge.net/blob/main/.github/workflows/push-on-main.yml">GitHub Action</a> which will publish new posts as I commit them to the repository.</p>
<p>On the whole it’s been a very productive week. My next task will be to upate my <a href="https://taphouse.io">Taphouse</a> using Tailwind as well – and I’ll be making heavy use of those Tailwind UI components to get that done.</p>
]]></content:encoded>
</item><item>
<title><![CDATA[Tenets of iOS Phased Releases]]></title>
<link>https://jsorge.net/2021/06/16/tenets-of-ios-phased-releases</link>
<guid isPermaLink="false">LzIwMjEvMDYvMTYvdGVuZXRzLW9mLWlvcy1waGFzZWQtcmVsZWFzZXM=</guid>
<pubDate>Wed, 16 Jun 2021 17:23:35 UTC</pubDate>
<description>Things I've learned about phased releases on the iOS App Store.</description>
<content:encoded><![CDATA[<p>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.</p>
<p>Let’s start by establishing some principles:</p>
<ol>
<li><p>Only one version of your app can be on the App Store at any given time.</p>
</li>
<li><p>A phased release lets you slowly allow users to <strong>auto-update</strong> to a new version at a slow percentage.</p>
</li>
<li><p>New users and manual updaters can <em>always</em> get your newest version from the App Store.</p>
</li>
</ol>
<p>When you decide to phase a release (<a href="https://help.apple.com/app-store-connect/#/dev3d65fcee1">Apple Documentation</a>), 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.</p>
<p>Let’s run through a scenario where things go on the not-so-happy path and see what happens.</p>
<p>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.</p>
<p>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:</p>
<ul>
<li><p>5.2 gets replaced on the App Store with 5.2.1: new users and manual updaters can only get to 5.2.1.</p>
</li>
<li><p>5.2 also completes its phasing, but no additional users can get to it (see principle #1 above).</p>
</li>
<li><p>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.</p>
</li>
<li><p>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”.</p>
<ul>
<li><p>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 <em>very unclear</em> about this).</p>
</li>
</ul>
</li>
</ul>
<p>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.</p>
]]></content:encoded>
</item><item>
<title><![CDATA[Xcode Project Generation: A Primer]]></title>
<link>https://jsorge.net/2021/03/15/xcode-project-generation-a-primer</link>
<guid isPermaLink="false">LzIwMjEvMDMvMTUveGNvZGUtcHJvamVjdC1nZW5lcmF0aW9uLWEtcHJpbWVy</guid>
<pubDate>Mon, 15 Mar 2021 16:05:37 UTC</pubDate>
<description>Xcode project generation is pretty great – here's how you can get started!</description>
<content:encoded><![CDATA[<p>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.</p>
<p>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?</p>
<p>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 <a href="https://tuist.io/docs/usage/get-started/">Tuist docs</a>.</p>
<h3>Extract Your Build Settings</h3>
<p>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 <a href="https://buildsettingextractor.com">BuildSettingsExtractor</a> 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.</p>
<p>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 <a href="https://github.com/olofhellman/VerifyNoBS">creep back in</a> to your project file.</p>
<h3>Clean Up Your Files</h3>
<p>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 <em>now</em>.</p>
<p>Here’s how I went about this cleanup at Zulily:</p>
<ol start="0">
<li><p>The first thing I did was to run the excellent <a href="https://github.com/venmo/synx">Synx</a> 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).</p>
</li>
<li><p><strong>Using Xcode</strong> 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 <code>Sources</code> 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.</p>
</li>
<li><p>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.</p>
</li>
<li><p>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.</p>
</li>
<li><p>Once a target was organized, able to build using the generated project <em>and still in my Xcode project</em>, 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.</p>
</li>
</ol>
<h3>Evaluate Your Options</h3>
<p>There are a couple of main tools that can be used to make Xcode project files (without also affecting things like your build system): <a href="https://github.com/yonaskolb/XcodeGen">XcodeGen</a> and <a href="https://tuist.io">Tuist</a>. 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:</p>
<p><strong>XcodeGen</strong></p>
<ul>
<li><p>↔️ Manifests are all defined in yml or json files.</p>
</li>
<li><p>✅ Super fast project generation.</p>
</li>
<li><p>✅ Templates greatly enhance flexibility & reuse of things like target definitions.</p>
</li>
<li><p>✅ Lightweight app built in Swift with binary releases available for download. It’s super easy to integrate into your repository.</p>
</li>
<li><p>❗️ Manifests only have so much flexibility because they aren’t defined in code.</p>
</li>
<li><p>❗️ Errors can be hard to debug (yml is a good but somewhat picky format).</p>
</li>
</ul>
<p><strong>Tuist</strong></p>
<ul>
<li><p>↔️ Manifests are all written in Swift.</p>
</li>
<li><p>✅ It’s extremely opinionated but uses those opinions mostly helpfully.</p>
</li>
<li><p>✅ 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.</p>
</li>
<li><p>✅ There’s a fantastic community building up around it.</p>
</li>
<li><p>✅ Can bundle itself and all of its dependencies with one command, making pinning to any given version (or commit hash) super simple.</p>
</li>
<li><p>❗️ 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.</p>
</li>
<li><p>❗️ Errors can also still be hard to debug, because you can’t (currently) pause the debugger when running the manifest generator.</p>
</li>
</ul>
<p>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.</p>
<p>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.</p>
<h3>Wrapping Up</h3>
<p>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!</p>
]]></content:encoded>
</item><item>
<title><![CDATA[Working From My iPad]]></title>
<link>https://jsorge.net/2021/03/12/working-from-my-ipad</link>
<guid isPermaLink="false">LzIwMjEvMDMvMTIvd29ya2luZy1mcm9tLW15LWlwYWQ=</guid>
<pubDate>Sat, 13 Mar 2021 05:29:55 UTC</pubDate>
<description>I've been working from the iPad Pro for the last couple of months. It's going kind of okay!</description>
<content:encoded><![CDATA[<p>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.</p>
<p>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.</p>
<p>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 <a href="https://edovia.com">Screens apps</a> 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.</p>
<p>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 <em>fantastic</em> 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.</p>
<p>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.</p>
]]></content:encoded>
</item><item>
<title><![CDATA[Apple Silicon Macs: Who Goes First?]]></title>
<link>https://jsorge.net/2020/07/17/apple-silicon-macs:-who-goes-first?</link>
<guid isPermaLink="false">LzIwMjAvMDcvMTcvYXBwbGUtc2lsaWNvbi1tYWNzOi13aG8tZ29lcy1maXJzdD8=</guid>
<pubDate>Fri, 17 Jul 2020 21:36:25 UTC</pubDate>
<description>Let's speculate about which Macs will get Apple silicon first.</description>
<content:encoded><![CDATA[<p>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.</p>
<p>Let’s start by looking at the existing lineup, shall we?</p>
<p><strong>Desktops</strong></p>
<ul>
<li><p>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.</p>
</li>
<li><p>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 <a href="https://www.macrumors.com/2020/06/23/kuo-24-inch-imac-new-design-4q20/">rumors</a> of new iMacs imminently and <a href="https://www.macrumors.com/2020/07/01/unreleased-imac-comet-lake-geekbench/">some evidence</a> of them showing up in benchmarks.</p>
</li>
<li><p>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.</p>
</li>
<li><p>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.</p>
</li>
</ul>
<p><strong>Laptops</strong></p>
<ul>
<li><p>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).</p>
</li>
<li><p>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.</p>
</li>
</ul>
<p>So, with that landscape in mind, which is the best candidate for Apple silicon? That’s actually really complicated.</p>
<ul>
<li><p>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.</p>
</li>
<li><p>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.</p>
</li>
<li><p>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.</p>
</li>
</ul>
<p>So here’s my prediction:</p>
<p>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.</p>
<p>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.</p>
]]></content:encoded>
</item><item>
<title><![CDATA[Back to the Zu]]></title>
<link>https://jsorge.net/2020/06/26/back-to-the-zu</link>
<guid isPermaLink="false">LzIwMjAvMDYvMjYvYmFjay10by10aGUtenU=</guid>
<pubDate>Fri, 26 Jun 2020 22:46:46 UTC</pubDate>
<description>I got a job!</description>
<content:encoded><![CDATA[<p>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!</p>
<p>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.</p>
<p>I’m excited to start this new chapter. Onward!</p>
]]></content:encoded>
</item><item>
<title><![CDATA[On the Market]]></title>
<link>https://jsorge.net/2020/05/04/on-the-market</link>
<guid isPermaLink="false">LzIwMjAvMDUvMDQvb24tdGhlLW1hcmtldA==</guid>
<pubDate>Mon, 04 May 2020 18:16:19 UTC</pubDate>
<description>I got laid off last week, which puts me square in the middle of the job market.</description>
<content:encoded><![CDATA[<p>TL;DR: I got laid off last Wednesday and am on the job market for an iOS developer job. <a href="https://github.com/jsorge/my-resume">Here is my résumé</a> 🙂</p>
<p>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 <em>still</em> having the regular day-to-day things happening around the house like Atticus’s therapies. Life goes on.</p>
<p>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.</p>
<p>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!</p>
]]></content:encoded>
</item><item>
<title><![CDATA[Thinking About Default iOS Apps]]></title>
<link>https://jsorge.net/2020/02/28/thinking-about-default-ios-apps</link>
<guid isPermaLink="false">LzIwMjAvMDIvMjgvdGhpbmtpbmctYWJvdXQtZGVmYXVsdC1pb3MtYXBwcw==</guid>
<pubDate>Sat, 29 Feb 2020 04:32:06 UTC</pubDate>
<description>Apple's rumored to be allowing default iOS apps in a future version of iOS. I've got some questions about that.</description>
<content:encoded><![CDATA[<p>There’s a report that came out <a href="https://www.bloomberg.com/news/articles/2020-02-20/apple-weighs-loosening-restrictions-on-rival-iphone-music-apps">this week from Bloomberg</a> 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?</p>
<ul>
<li><p>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.</p>
</li>
<li><p>Tapping a <code>mailto</code> 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).</p>
</li>
<li><p>Asking Siri to play some music could launch Spotify without having to add “with Spotify” to the end of your query.</p>
</li>
</ul>
<p>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?</p>
<p>The big question that I have revolves around code that would normally put Safari in your app. The class to do this is <code>SFSafariViewController</code> 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.</p>
<p>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 <code>SFSafariViewController</code> 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 <em>ever</em> getting in my way.</p>
<p>There’s also similar code around <code>MFMailComposeViewController</code> for sending emails in apps (I use that in Scorebook) as well as <code>MFMessageComposeViewController</code> for sending iMessage or SMS messages. Would they allow users to pick their email or message client and slide up those experiences as well?</p>
<p>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. 🍿</p>
]]></content:encoded>
</item><item>
<title><![CDATA[Apps I Love: Kaleidoscope]]></title>
<link>https://jsorge.net/2020/02/22/apps-i-love:-kaleidoscope</link>
<guid isPermaLink="false">LzIwMjAvMDIvMjIvYXBwcy1pLWxvdmU6LWthbGVpZG9zY29wZQ==</guid>
<pubDate>Sun, 23 Feb 2020 04:21:38 UTC</pubDate>
<description>Kaleidoscope is my diff tool of choice on macOS.</description>
<content:encoded><![CDATA[<p>Several years ago I had a series of posts here called “Apps I Love”. Many of the apps I loved back then I still do, but right now I want to highlight a special part of my tool bag. It’s my diff tool of choice: <a href="https://www.kaleidoscopeapp.com">Kaleidoscope</a>. I freaking love this thing, and it’s <a href="https://www.kaleidoscopeapp.com/release-notes">just received its first update in a while to v2.3</a>.</p>
<p>If you’ve never seen a diff tool in action (or even heard the term), basically what it does is can take 2 things and tell you the difference between them. Kaleidoscope can diff text files, folder contents in the Finder, git branches, and even images. I use it most frequently in my development workflow when resolving conflicts during code merges but have definitely used its folder support before too. It plugs nicely in to my git app of choice <a href="https://www.git-tower.com">Tower</a> (which I should probably write about too).</p>
<p>I do have a funny story to share about Kaleidoscope in closing. Version 2.0 came out 7 years ago, in early 2013. At that point I was barely writing any code and definitely not professionally. I heard about v2’s release somehow (probably from one of the tech-related Twitter accounts I was following) and Black Pixel was doing some Twitter contests for free copies. I’m a sucker for stuff like that even if I have no idea how to use what I’m winning. I ended up winning a copy and being lost as to what it did. Fast forward a year or so and I’m learning development and see in Tower that it has a way to trigger Kaleidoscope as the diff tool! That was my gateway to a bigger world 🙂</p>
<p>I really hope this new version is a harbinger of things to come and that some day they’ll release a version that I need to pay for. So go check out Kaleidoscope and come to love it as much as I do!</p>
]]></content:encoded>
</item><item>
<title><![CDATA[🔗 iOS Performance Tips]]></title>
<link>https://jsorge.net/2020/02/18/ios-performance-tips</link>
<guid isPermaLink="false">LzIwMjAvMDIvMTgvaW9zLXBlcmZvcm1hbmNlLXRpcHM=</guid>
<pubDate>Tue, 18 Feb 2020 15:55:09 UTC</pubDate>
<description>This is a good collection of tips to increase performance of your iOS app.</description>
<content:encoded><![CDATA[<p><a href="https://www.fadel.io/blog/posts/ios-performance-tips-you-probably-didnt-know/">iOS Performance tips you probably didn’t know (from an ex-Apple engineer)</a></p>
<blockquote>
<p>A common anti-pattern is leaving UITableView/UICollectionView cell labels populated with their text content when these cells enter the reuse queue. It is highly likely that once the cells are recycled, the labels’ text value will be different, so storing them is wasteful.</p>
</blockquote>
<p>I could have pulled a few more gems from this super helpful post by Rony Fadel, but this first one struck me off the bat. I don’t know how many times I have kept text in a label when I perhaps didn’t need to. I <em>also</em> didn’t know that the right place to nil-out text in reusable views (<code>UI{Table|Collection}ViewCell</code>) is not in their <code>prepareForReuse()</code> method but in the delegate’s <code>didEndDisplaying</code> method instead.</p>
<p>In larger projects with many cell class types this may take some thinking on how to get the message across to a cell type of “time to reset any labels you have”. I know if I were still at zulily this would be something that I’d work to get in place as soon as possible, as I did a lot of really hacky things to collection views in my time there 😀</p>
]]></content:encoded>
</item><item>
<title><![CDATA[The Time I Stumped the Apple Geniuses]]></title>
<link>https://jsorge.net/2020/02/14/the-time-i-stumped-the-apple-geniuses</link>
<guid isPermaLink="false">LzIwMjAvMDIvMTQvdGhlLXRpbWUtaS1zdHVtcGVkLXRoZS1hcHBsZS1nZW5pdXNlcw==</guid>
<pubDate>Fri, 14 Feb 2020 17:11:23 UTC</pubDate>
<description>AKA how did Apple sync the iMac G3 desktop color with its case?</description>
<content:encoded><![CDATA[<p>When the Apple stores first opened, the Genius Bar was a much different experience than it is today. For one it actually was a bar you could go to at the back of the store. But they also wanted to answer the really tough questions. And if the Genius you were talking with didn’t know the answer, there was a Red Phone to dial home and talk to someone in the next level up in support. It really was a fun experience.</p>
<p>Prior to the Seattle area getting its first Apple store my family and I went on a vacation to Las Vegas and we went to the Fashion Show Mall so I could see their store. And I had just the question to stump their Geniuses:</p>
<p><img src="/_posts/2020-02-14-the-time-i-stumped-the-apple-geniuses.textbundle/assets/imacs-galore.jpg" /></p>
<p>I worked at an Apple Authorized Service Center and had been doing service on the iMacs with slot loading optical drives (like the ones pictured above). Whenever I would need to erase a hard drive and restore the operating system I noticed that the desktop wallpaper color matched the color of the case. So a Ruby iMac would get a Ruby colored desktop, and same with Sage green, Indigo blue, and so on. How did they pull this off?</p>
<p>I do also have some inside information to help rule out some possibilities:</p>
<ul>
<li><p>The main logic boards did not know anything about the shell color. There was 1 part per model of iMac and they were not ordered by color of case. These boards hold <em>everything</em>: CPU, graphics, memory, all the ports, all of the guts.</p>
</li>
<li><p>Same story for the other big board: the analog/video board. It was not ordered by color either.</p>
</li>
<li><p>The only other real parts inside were the optical and hard drives.</p>
</li>
<li><p>Except for the speakers.</p>
</li>
</ul>
<p>And that last part is what I think happens. The speakers on these iMacs were colored to match the back case (and you could see them through the translucent base). I <em>think</em> that there was some chip in the speakers that would indicate to the system what color it was and then the system would pull the appropriate wallpaper.</p>
<p>If you or someone you know may happen to know how this worked I would love to know the answer!</p>
<p>PS: Special thanks to the Xcoders group last night for encouraging me to post this after I told the story. It was a ton of fun to speculate together. Community is awesome 🙂</p>
<p><strong>Update 2/15</strong></p>
<p>After a lot of fun discussion and speculation I think <a href="https://twitter.com/iKenndac">Daniel Kennett</a> has the most plausible answer: the color was encoded into the serial number!</p>
<blockquote class="twitter-tweet" data-theme="dark"><p lang="en" dir="ltr">VQK? That’s a pink 2nd Gen iPod nano! I spent many years reverse-engineering all things iPod for an app of mine back in the day. <a href="https://t.co/Xxg3Yjxnes">pic.twitter.com/Xxg3Yjxnes</a></p>&mdash; Daniel Kennett (@iKenndac) <a href="https://twitter.com/iKenndac/status/1228422312229294086?ref_src=twsrc%5Etfw">February 14, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<p>If my memory serves correctly, when we would replace a logic board we would have precisely one opportunity to reset the serial number via a firmware reset tool. I don’t recall what would happen if we failed to update the serial number (and that sure would be a fascinating experiment to run!).</p>
]]></content:encoded>
</item><item>

<link>https://jsorge.net/2019/12/16/animating-constraints</link>
<guid isPermaLink="false">LzIwMTkvMTIvMTYvYW5pbWF0aW5nLWNvbnN0cmFpbnRz</guid>
<pubDate>Tue, 17 Dec 2019 05:49:56 UTC</pubDate>
<description></description>
<content:encoded><![CDATA[<p>Note to self: when animating <code>NSLayoutConstraint</code> properties, remember to call <code>layoutIfNeeded()</code> before the animation block <em>and inside</em> the animation block. Otherwise the animation won’t work.</p>
<p>Hopefully this saves future me some time.</p>
]]></content:encoded>
</item>
</channel>
</rss>