[SC]()

iOS. Apple. Indies. Plus Things.

2025 Indie Thoughts

// Written by Jordan Morgan // Jan 1st, 2025 // Read it in about 2 minutes // RE: The Indie Dev Diaries

This post is brought to you by Emerge Tools, the best way to build on mobile.

Another year down! I hope 2024 was a wonderful time for you and yours. As we look to 2025, I’ve decided to simplify my goals down to the essentials (a fair contrast to what I typically have done).

To that end, here’s what I’m shooting for:

A screenshot of Things3 showing yearly goals.

  1. Scores for NCAA: I had no plans to make this app, but it just sort of came up. There is no good way to follow Division 2 or 3 scores on the App Store, so it feels like a worthwhile problem to solve. I’m keeping this one lean, and it should hit the App Store this month. Feel free to try out the beta while you’re here!

  2. iOS 18 Book Update: It is still crazy to me that I spent almost three years writing over a 1,000 pages over iOS development — but I did! I’ll never do it again, but being on this side of it (i.e., it’s done) feels great. I still look forward to updating it annually. While its sales aren’t nearly what they once were, folks still buy it every week and I’m grateful for that.

  3. Month or Marketing for Elite Hoops: Inspired by my friend and successful indie Emmanuel, I’m going to dedicate an entire month to simply marketing Elite Hoops every single day. I love to build stuff, but I’ve learned more and more that marketing is what makes MRR grow. Over a year ago, before I shipped Elite Hoops, Emmanuel and I chatted for a bit over practical marketing tips and tricks. That’s what lead to me leaning into email marketing, and it’s been a critical part of my growth. I’m excited to see what this month could do for Elite Hoops.

  4. Keyframe Plays in Elite Hoops: This is a big one - and what Elite Hoops was initially supposed to do and be. Basically, today you can record your plays (I use ReplayKit to do this - which is actually meant for gaming) and it spits out a video. Helpful! Validated! But also not what I originally intended to do. A lot of coaches want a step-by-step play creator that animates as each “step” occurs. This is how all other basketball software works. So, I need to nail it. I could write an entire post over this, but I’ll keep it short: the fact that I kept Elite Hoops to an MVP and launched with what I thought was a “janky” way to share plays, and yet it is growing and doing great — is an entire lesson in of itself. I don’t even have my primary feature done yet! I’ll be well positioned once I do this, because I’ll be the only app that lets you just fire off a quick video recording and make those in-depth frame by frame sets.

  5. Ship Alyx: Hey! My next “pillar” app! Alyx is a caffeine tracker I’ve used for well over a year. I kinda bounce off and on with its development, and it’s been a UI playground for me. But, I use it daily, and it’s turned into such a fun and quirky app. I want the rest of the world to see it. I plan on shipping it later this year.

Alyx on iOS.

  1. Elite Hoops Roadmap: This one is actually #6, but Markdown formatting is very upset about the image above. So, we’ll go with it. Anyways, from there, I’ve got a whole host of things to do to make Elite Hoops into a grown up business. If I get here in 2025, that’ll be a win.

Aside from that, my other business-y business goal is to get to $5,122 in MRR. Of course, I’d love to go beyond that — but I’ve tried to make a reasonable goal for each year to reach my ultimate “I could almost go full-time indie” MRR milestone:

A screenshot of Things3 showing yearly MRR goals.

So that’s it! I’m ready for 2025, and I hope to run into some of you IRL. This year, I know I’ll be at Deep Dish Swift and WWDC 2025.

Until next time ✌️

···

The Big Feature

// Written by Jordan Morgan // Dec 15th, 2024 // Read it in about 1 minutes // RE: The Indie Dev Diaries

This post is brought to you by Emerge Tools, the best way to build on mobile.

In June of 2020, I wrote about my first major update to Spend Stack. It’s a fond indie memory! After meticulously preparing the update, press kit and everything else we all do, I woke up and…

  • Saw it shoot to #1 in the Finance category
  • It had press coverage
  • Multiple App Store features
  • And, App of the Day

the works, man. I remember thinking, “It doesn’t get any better than this!” Though, reflecting on it now, my moonshot goal has always been to grow an app which could be a single source of income for my family. With a stay at home wife, three kids, health insurance…it’s a tall order.

Spend Stack made a few thousand bucks over that month, I think, the most for me at the time. While I look back at it fondly, it never came close to being anything other than some nice side income.

Fast forward to now, and things look different. Elite Hoops has had zero press coverage and no App Store features and yet…it’s made that same amount over the last three or four days.

I don’t mean to puff out my chest (I know several devs making much, much more — I know several devs making much, much less). What I do mean to say is, I think I’ve arrived at a place where my experience is outpacing my previous “indie” desires. Instead of chasing a new API, I spend a lot of time these days poring through feedback, wondering about my app’s ability to grow, and doubling down on the things that are working.

Which is where the aforementioned “big feature” launch came from. After a bunch of research, talking to coaches, and looking at my own data — I realized I was in a wonderful position to deliver a compelling practice planner experience. I wrote about it recently. I don’t want to rehash too much other than to say, it’s gone great! It’s only been over a week, but all metrics are up. Conversions. Trial starts. All of it.

Usually, I preach that growth is found outside of Xcode. But, maybe, just maybe, there’s that one killer feature that could drive up all metrics for you. The trick is to know what that feature is, because historically — we tend to think all of them are.

Postscript

Originally I wanted to do an in-depth, shareable, knowledgeable-laden postmortem that anyone could dissect at their leisure to learn from. Apply it to their own work. Share all I could. Though, the year is coming to a close, and, well — I’m pretty tired. I’m ready to sit on a couch and play Call of Duty (Nuketown 24/7 playlist is live!) once my Christmas break kicks in.

What I do hope you get from this, though, is that if you are in a spot where things are stale, you’re in a rut, not really making any money, downloads are down — you name it, I want you to know that I’ve been there for several years. And while I’m not at my goal of Elite Hoops being a sole source of income, I’m encouraged that it’s growing. It took so many years, but for the first time it feels possible, like I could actually do this.

And sometimes, all it takes is about ten years of it not working. Then one day, it just kinda does.

With that, I also want to say I’m truly thankful for all of you who have stuck around and read this silly website. I used to think of myself as the new kid on the block, writing for whoever would care to read. Now, I suppose I’ve been around a bit. I still enjoy all of this just as much as I did when I picked up this lil guy.

Have a wonderful rest of 2024, and I’ll see you all next year.

Until next time ✌️

···

Eight Fun SwiftUI Details in my Practice Planner

// Written by Jordan Morgan // Nov 22nd, 2024 // Read it in about 7 minutes // RE: The Indie Dev Diaries

This post is brought to you by Emerge Tools, the best way to build on mobile.

For the last few months, I’ve been cracking away at a massive feature for Elite Hoops, a practice planner. At midnight a few days ago, I stealth shipped it to the App Store. While I have a mountain of marketing to go through still (social media assets, blog posts, email, you know the dance) I figured if it’s already approved, might as well ship it.

The feature turned out great, and while I try to pour love into everything I make, sometimes there’s just that thing where you step back and say, “Huh. I really kinda love this.” And that’s how this went for me.

From all of these scattered thoughts…

Notes in Free Form for my practice planner.

…I arrived here:

Elite Hoops practice planner running on iOS.

Here are eight little SwiftUI nuggets from the feature that I think turned out to be something worth sharing.

Photo slider
Directly inspired by Photos’ implementation. The clipping of the text was tricky to figure out at first, but animating the pill using .matchedGeometryEffect made the rest trivial:

Elite Hoops practice planner running on iOS.

Gradient header
Initially, the practice details screen was a little stale. Since each drill category has a color associated to it, I decided to incorporate a subtle MeshGradient at the top. The colors being derived from each category added a little pop (the extended font width also works great here, emphasizing the competitive nature of the feature):

Elite Hoops practice planner running on iOS.

Story pills
When running a practice from your phone, you cycle through drills which each have a time associated to them. I ended up using an interface inspired by workout apps paired with Instagram’s now famous “story pills” at the top, which fill up as the drill progresses. Here, I manually skipped the drill to demonstrate how it works:

Elite Hoops practice planner running on iOS.

Shortcuts sheet
Creating a practice has a near identical structure as Shortcuts, where actions are drills and the practice itself is an aggregation of them. As such, the retractable drawer made sense to replicate, and save for a few differences (primarily, the sheet doesn’t dismiss when you add a drill) it works much the same way:

Elite Hoops practice planner running on iOS.

Printing
From a technical aspect, printing was a difficult problem to solve. It involved a mix of ImageRenderer, .pdf generation and measuring the height of each element to support pagination. It was time well spent, as I quickly discovered a vocal minority of coaches who had no interest in running a practice from their phone1 due to a variety of reasons - they want it printed out:

Elite Hoops practice planner running on iOS.

Interpolation countdown
When starting a practice, I quickly noticed it was too jarring to have it begin immediately after the view was presented. To ease coaches in, or let them cancel it, I added a quick countdown experience. An increasingly firm haptic matches each digit to convey a sense of urgency:

Elite Hoops practice planner running on iOS.

Blur out for finished practice
Finishing a practice is an accomplishment, akin to finishing a workout or a run. As such, I wanted a quick interaction that created that sort of “atta boy” vibe. I ended up reusing much of the countdown view’s code above to make a little wrap up view:

Elite Hoops practice planner running on iOS.

Symbol animation for buttons
Finally, incorporating symbol animations feels so incredibly right to me. There are a few of them which occur in the buttons near the bottom of a practice:

Elite Hoops practice planner running on iOS.

Wrapping Up

The practice planner is something I’m proud to put my name on. It’s one of the first truly innovative things I’ve made — as planning basketball practices has traditionally been somewhat of a dross affair. It hasn’t really changed much or rethought in terms of, “With all we have available to us today, how could we make all of this easier, better and help coaches more?”

Perhaps more importantly, we live in an age where inspiration strikes all the time across social media, with talented trainers constantly posting incredibly useful drills. There hasn’t been an ideal place to save them all, and information becomes scattered. The drill library aims to solve that, too. Like lego building blocks, over time — coaches will build up a drill library, and use those pieces to form their ideal practices.

Feel free to download Elite Hoops today, even if you aren’t intent on using it, if even to simply play around with the feature. Most of it isn’t paywalled, so it’s no issue to poke around. Finally, huge thanks to the developers and designers at Apple who created first-class user experiences in Photos (an app I’ve long championed) and Shortcuts. Rebuilding similar flows from both of them not only increased my appreciation for their solutions (as I hit similar user experience dilemmas, and realized how their approaches made so much sense), but they also helped me chip away at the whole feature quite quickly.

Until next time ✌️

  1. In fact, they downright despised it. They felt as though it conveyed a sense of distraction to their players, as if they weren’t focused on them or the practice. 

···

Elite Hoops Year One: 12 Bite-Sized Lessons

// Written by Jordan Morgan // Oct 8th, 2024 // Read it in about 6 minutes // RE: The Indie Dev Diaries

This post is brought to you by Emerge Tools, the best way to build on mobile.

Elite Hoops has hit the one year mark! I had a few buddies ask me some questions around getting those first paying customers, the kinds of marketing I’ve tried and other similar things.

In short, indies talkin’ shop. So, here is a point-by-point brain dump of what I think the twelve most helpful things I’ve learned or tried are. I’ll try to keep each one around a few sentences to keep it to the point. But, you know, IChat(.aLot).

In you just want the TL;DR on the #numbies, here’s that:

  • 860 paying customers
  • $3,000 MRR

Cool, on to the good stuff.

3 Tips on Getting the First 100 Paying Users

On getting to that 100 paying user mark, which was my first goal.

1: Ask for Emails in Exchange for ✨Value✨
Setup an email capture component. A form, use an API, whatever. This is a fantastic way to let people know of large updates, push sales and reactivate folks. I use Mailerlite, and it’s okay, but adding an option to join the mailing list during onboarding to learn about plays, free stuff and more means that a little over 30% of people join it.

let concatenatedThoughts = """

Also, use the right text content types on your sign up fields so iOS will offer to autofill their email.

"""

2: Scaling with Paid Ads
I spend $30 in Meta ads every single day1. It took about three months, but I found a winning creative and audience after experimenting. I think a lot of indies either say paid acquisition is “lighting money on fire” or they take too much pride in touting “organic growth” at the expense of, well — more growth. You don’t have to spend a ton of money, you can get meaningful results with a little money.

Here is an example of the ad I run most. I make all of the creatives in Sketch, too:

Sketch for paid ad creatives

3: Side-Side Project Marketing
I love what I call “side-side project” marketing. Give a useful tool away for free. Get backlinks to it. Spread awareness. That’s exactly what I did with my Youth Basketball Practice Planner, a little web app that creates practice plans that you can customize and share.

This means that:

  1. People use a good tool, who are my target customers, for free.
  2. They like it, and they share it because they have a reason too (i.e. “Check out this practice I came up with for this week”)
  3. Now, they download Elite Hoops to get more.
Image 1
Image 2
Image 3

3 Thoughts on Being a Big Boi App

Nuggets on how to treat an app like the business you may want to grow it to eventually.

4: Use Mixpanel
Analytics and tracking can be done “the righteous indie way” easily. You don’t need personal information - a random identifier that shows you how people use your app is critical, and you can do it all for free with Mixpanel.

This is how I know that half of my users are youth basketball coaches, which is why I built the tool above:

Mixpanel Dashboard

And the code for it is trivial:

struct Telemetry {
    static func initializeClient() {
        Mixpanel.initialize(token: "YOUR_TOKEN", trackAutomaticEvents: true)
    }
    
    static func createdDemoCourt() {
        guard !Device.current.isSimulator else { return }
        Mixpanel.mainInstance().track(event: "Demo Court Created")
    }

    // etc etc....
}

5: Use Plausible
Along the same lines, Plausible is wonderful for privacy-based web analytics. Once I double-down on some SEO plays, this is how I will know if what I’m trying is working.

6: Use Supabase for In-App Feedback
Slot in Supabase for easy feature requests. Again, they have a very nice Swift package (an indie started it, and they hired him on full time) and it takes minutes to get going. I wrote a post on how to get this up and running here.

I can’t tell you how valuable the stuff in here is:

Supabase dashboard with feedback

3 Indie Mindset Vitamin Shots

On ways to motivate your mindset in the indie world.

7: Think Small, Achievable Chunks
If I thought to myself, “I want to get to over 800 paying customers in a year” I probably would’ve stressed out. But, by breaking down that goal to mini ones, it was much more doable, “Get 100”, “Okay, maybe we can crack 200 in a few months”, etc.

8: Bake In Marketing
Indies don’t market enough. And, we can’t really learn much from the “winners” who had something go viral for whatever reason because that’s unlikely to happen to you or me. So, spend a whole day of your indie work on marketing.

I have this reminder weekly, #ItAintMuchButItsSomething:

Marketing tasks.

The above is the bare minimum, but while I’m in build mode (more on that below) it’s still going to help spread the word.

9: …And Figure Out If You’re in Build Mode or Marketing Mode
Are you building core functionality that’s going to get you to the next step? If you’re not, then you should really spend a lot more effort on marketing.

All of these stories going around about the Gen Z folks making millions on the App Store? They market so well. You can build an amazing thing that nobody knows about, and at the end of the day it will still be an amazing thing that nobody knows about.

3 Overall Lessons

Stuff I’ve picked up along the way.

10: Wow, Copywriting Matters
This is an area I’ve tried to grow and learn more about over the last year. And yeah, chatGPT really shines here for me. Consider selling the original iPod:

“100 GB of storage.” or “1,000s of songs in your pocket.”

One is a technical fact, the other tells a story. I’m prone to spitting out technical facts and not telling a basketball coach why something is great. So, often I’ll do a prompt this like: “I’m working on X feature, it does Y. My attempt at marketing copy was Z. Assume you are an expert in marketing in the basketball and tech space, how would you write about this feature?”

11: Staying Focused is the Hardest Part for Me
You’ll just see story after story after story about how some app, tool or gold rush is happening — and how much money is in it. And, while there is definitely value in jumping on the moment, I find that for me it’s super difficult to stay focused on Elite Hoops.

My MRR is peanuts to a lot of people, and a quite a bit to others. There’s always a spectrum, so I’ve opted to not really chase other opportunities and stay focused on growing Elite Hoops.

12: Have a Roadmap!
Seriously, it sounds obvious but I used to be bad at this! Now, I have a clear picture of where I want to go, and how I will get there. I know I’m in the aforementioned “build” mode because of the feedback I’ve received from paying users. Once I have that in, I’ll shift more towards marketing mode (but, again, don’t stop doing marketing ever!).

Wrapping Up

And that’s it! I hope this time next year, Elite Hoops has continued to grow and I’ll have more (hopefully helpful) things to share. Let’s build, baby!

Until next time ✌️

  1. And, I don’t even use their SDK. You can run install ads without it, you just won’t get accurate results in terms of being able to say ‘This install was definitely from Facebook or Instagram’, but I have enough data to have a good idea of what I get from it. 

···

Using PreviewModifier for Quick Xcode Previews

// Written by Jordan Morgan // Sep 30th, 2024 // Read it in about 2 minutes // RE: SwiftUI

This post is brought to you by Emerge Tools, the best way to build on mobile.

Ah, Xcode Previews. The absolute best, worst thing Apple has ever shipped for developers. When it works, which for me is around 90% of the time, it’s absolutely essential to my workflow.

Other times? It crashes, spins, indexes and loads for an eternity — and I end up sitting there and waiting for it to work again out of nothing else but pure, unadulterated spite. And, as I launch my fusillade of negative thoughts and words towards Xcode, I couldn’t help but wonder if the problem was me.

Foolishly, I was passing in an expensive object around my previews, a holdover of a //TODO: Don't do this I had while prototyping stuff. In the process, though, I came to love having actual data from the server that wasn’t mocked. This was the good stuff, along with the real-world connectivity loading times.

In short, I wanted my cake. And I wanted to eat it, too.

Recently, I found a tip that’s helped out with the long loads like this, PreviewModifier (docs). It helps make complex, heavy-load type of objects more performant to use across previews by basically doing all the expensive work once.

Sharing is Caring

Consider the purpose of Observable - it’s literally to reuse and share the same chunk of data, managers, data access objects, networking layers — you name it — across your codebase:

@Observable
class DAO {
    var accountData: AccountData = .empty 
    init() {
        veryExpensiveSetup()
    }

    func veryExpensiveSetup() { 
        // Hit API, or DB, or both...
        accountData = fetchedData
    }
}


@main
struct TheBestApp: App {
    // Expensive work happens here
    @State private var dataAccess = DAO()


    var body: some Scene {
        WindowGroup {
            MyViewHierarchy()
                // But at least the hit happens only once
                .environment(dataAccess) 
        }
    }
}

And so it goes, dataAccess can do the time-consuming stuff with an upfront cost, and let the rest of the app use it without incurring the same hit:

struct MyViewHierarchy: View {
    @Environment(DAO.self) private var dataAccess


    var body: some View {
        Form {
            // Everything has access to dataAccess
            Text(dataAccess.accountData.name)
            UserAccountView()
            NotificationsView()
            LegalView()
        }
    }
}

This is SwiftUI 101 stuff. So, given the wins we have here — why don’t we extend the same logic to Xcode Previews? Instead of doing something like this:

#Preview {
    MyViewHierarchy()
        // Snapple! We're running `init()` every
        // time this thing reloads!
        .environment(DAO())
}

…or this1:

extension DAO {
    var mock: DAO { // Setup mocked DAO... }
}

extension AccountData {
    var mock: AcccountData { // Setup mocked account data... }
}

…maybe you want the real, bonafide data that comes with actually hitting your API, or setting up your data layer. But, without doing that over and over.

Caching Preview Objects

It turns out, you can. And I had no idea this was possible until a few days ago. It’s eerily similar to how ViewModifier is handled. Here’s what it looks like:

// Adopt `PreviewModifier`
struct CachedPreviewData: PreviewModifier {

    static func makeSharedContext() async throws -> DAO {
        let dao = DAO()
        await dao.makeSomeOtherNetworkCalls()
        await dao.noSeriouslyGoCrazyWithIt()

        // Because we're only doing this once...
        return dao
    }


    // And now it's reused
    func body(content: Content, context: DAO) -> some View {
        content
            .environment(context)
    }
}


// Add the modifier to the preview.
#Preview(traits: .modifier(CachedPreviewData())) {
    // Now this, or any other preview using the same setup
    // Has access to `DAO`, and it only ran `init()` once
    MyViewHierarchy()
}

When you do this, Xcode Previews will cache the expensive object, and then refer to it over and over. Now, you’re free to have real data, in real previews, without it being a real %!@*&%$ pain in the @$!.

And that’s it! Print out some logs, and you’ll see that crazy work only happens once. Plus, you can get as creative as you want here - again, it works just like ViewModifier does. So, pass in some flags or data in the initializer, make a few of them, house core logic in only one — it all works:

struct CachedPreviewData: PreviewModifier {
    // Add in a flag to access different stuff
    enum UserRole: Int { case user, admin, temp }
    var role: UserRole 

    // Same code as we had before that uses that enum
}

#Preview(traits: .modifier(CachedPreviewData(role: .admin))) {
    // Admin role will be used
    MyViewHierarchy()
}

Until next time ✌️

  1. Which, by the way, is not a bad approach at all. I use this method all the time. But there are situations where you need to hit the actual server for a variety of testing reasons. 

···