[SC]()

iOS. Apple. Indies. Plus Things.

An Ode to Swift Enums: The View Models That Could

// Written by Jordan Morgan // Feb 24th, 2025 // Read it in about 6 minutes // RE: Swift

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

Swift enums are the swiss army knife of the iOS developer’s tool belt. Adept at solving many problems in several novel ways, its utility extends far beyond what we might’ve come to expect from an enumeration (which rings louder for those of who started with Objective-C). As such, a lovely way to embrace your fellow Swift enum is a lightweight view model.

Consider:

  • A baskeball practice planner app (hey, I know of one!)
  • Which shows a list of practices
  • …and each with a practice type: team practice, workout or skills training.

That’s an ideal time to flex enum’s raw might:

enum PracticeType {
	case teamPractice, workout, skillsTraining
}

Text Me

A fine start, though you’ll likely need to show the actual type as a String in your interface. Being the epitome of flexibility, there are two obvious routes fit to the task when using an enum:

Option 1: Use a String as its rawValue:

enum PracticeType: String {
	case teamPractice, workout, skillsTraining
}

Here, PracticeType has a raw value type of String:

let team = PracticeType.teamPractice
// teamPractice
print(team)

Serviceable in many cases. We’ll come back to this. For now, there’s also…

Option 2: Adopt CustomStringConvertible:

enum PracticeType: CustomStringConvertible {
    case teamPractice, workout, skillsTraining
    
    var description: String {
        switch self {
        case .teamPractice:
            "Team Practice"
        case .workout:
            "Workout"
        case .skillsTraining:
            "Skill Training"
        }
    }
}

CustomStringConvertible is a lightweight protocol which allows you to describe the adopting type a String via its description property:

let team = PracticeType.teamPractice
// Team Practice
print(team.description)

let concatenatedThoughts = """

However, any Swift type can be represented as a `String`, it's a nicety built into the language. Using the `String(describing:)` initializer will still yield a `String` of the passed in type. In this case, if the type adopts this protocol, then Swift defers to the `description` implementation.

"""

If you want to be an academic, you could make the argument (perhaps, too easily) that this is a misuse of the protocol. Here’s Apple:

Accessing a type’s description property directly or using CustomStringConvertible as a generic constraint is discouraged.

To wit, in their example — they interpolate a String to represent a type described in a more holistic sense, rather than a single case of an enum:

struct Point {
    let x: Int, y: Int
}

extension Point: CustomStringConvertible {
    var description: String {
        return "(\(x), \(y))"
    }
}

Which is appropriate for you is a matter for you to decide, but what I can tell you is I’ve adorned enums the world over with CustomStringConvertible and I’ve been no worse off.

Identity Crisis

Moving along, now — what if we need to show such a type in a Picker or ForEach? In these situations, the concept of identity is crucial. How do we individualize each type — shouldn’t enums inherently be described in such a fashion by design? In SwiftUI, we solve this problem (in part) via Identifiable conformance. And in enums? It’s quite trivial:

enum PracticeType: CustomStringConvertible, Identifiable {
	case teamPractice, workout, skillsTraining
    
    var description: String {
	    switch self {
	    case .teamPractice:
	        "Team Practice"
	    case .workout:
	        "Workout"
	    case .skillsTraining:
	        "Skill Training"
	    }
    }

	var id: Self { self }
}

Now, you’re free to show it in several different SwiftUI views — thanks to each type representing its own unique value in a way SwiftUI understands:

@State private var practiceType: PracticeType? = nil

Picker("", selection: $practiceType) {
	// Your picker representation
}

This view, here — a Picker, naturally leads us to another issue — how do we show all of the cases? Certainly, something like this is tiresome…

Picker("", selection: $practiceType) {
	Text(PracticeType.teamPractice)
		.tag(PracticeType.teamPractice)
	Text(PracticeType.workout)
		.tag(PracticeType.workout)
	Text(PracticeType.skillsTraining)
		.tag(PracticeType.skillsTraining)
}

…and indeed, unnecessary thanks to more of what Swift enums offer us.

Iteration

Moving on, we now arrive at CaseIterable, yet another protocol the Swift compiler can handle for us. This protocol allows us to represent our type as a collection, accessed via its allCases property. Though common in enums without an associated types, it can be in enums with associated types all the same. In our scenario, though — all that’s required is to simply declare conformance:

enum PracticeType: CustomStringConvertible, Identifiable, CaseIterable {
	// No other changes required from us
}

Now, the ergonomics of utilizing a Picker with our enum becomes much more tolerable (and scalable):

Picker("", selection: $practiceType) {
	ForEach(PracticeType.allCases) { practice in 
		Text(practice.description)
			.tag(practice)
	}
}

For the Love of the Game

Enums are a quintessential fit in the SwiftUI ecosystem, lending itself well to many of its design choices. Beyond using them in View types, I find myself having maybe a little too much fun with them. I could write a book (technically, because I’m a glutton for punishment, I wrote five) over the flexibility of the Swift enum. I can think of no better way to wrap this post up other than showing them off like a show pony.

For example, could I interest you in a random practice type?

let randomPractice: PracticeType = PracticeType.allCases.randomElement() ?? .teamPractice

Create extensions for further use cases:
Or, as your app grows and feature emerge, I also find myself sticking several quality of life extensions on my enums:

extension PracticeType {
	var subTitle: String {
		switch self {
	    case .teamPractice:
	        "Practices suited towards a team environment with five or more players."
	    case .workout:
	        "An individual workout."
	    case .skillsTraining:
	        "Skills-based training sessions, where drills are the primary activity.""
	}
}

Don’t get me started on the flexibility of associated values:
Associated values make what was already a powerful little construct stray into OP territory. Consider loading states:

enum PracticeState {
    case notStarted
    case inProgress(TimeInterval) 
    case completed(totalDuration: Int)
}

struct PracticeView: View {
    @State private var state: PracticeState = .notStarted
    @State private var elapsedTime: TimeInterval = 0
    private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    
    var body: some View {
        VStack(spacing: 20) {
            switch state {
            case .notStarted:
                Button("Start Practice") {
                    state = .inProgress(0)
                }
                
            case .inProgress(let timeElapsed):
                Text("Time Elapsed: \(Int(timeElapsed))s")
                
                Button("End Practice") {
                    state = .completed(totalDuration: Int(timeElapsed))
                }
                .padding(.top)
                
            case .completed(let totalDuration):
                Text("Practice Completed")
                Text("Total Duration: \(totalDuration) seconds")
                
                Button("Restart") {
                    state = .notStarted
                }
                .padding(.top)
            }
        }
        .onReceive(timer) { _ in
            if case .inProgress(let timeElapsed) = state {
                state = .inProgress(timeElapsed + 1)
            }
        }
        .padding()
    }
}

Mocks and Dependency Injection:
Continuing on with associated values, they naturally slot into SwiftUI previews and mock services:

enum Environment {
    case production(Service)
    case staging(Service)
    case mock

    var analyticsService: AnalyticsService {
        switch self {
        case .production(let service), .staging(let service):
            return service
        case .mock:
            return Service.mocked()
        }
    }
}

let env: Environment = .production(ProdAnalytics())
let analyticsSerivce = env.analyticsService

Stay vanilla:
All this talk of creative use cases — it’s possible I’ve overlooked using enums just as they were originally envisioned. Of course, they are practical for that, too. But because Swift treats them as more of first class citizen and not a singular value to switch on, they are suited to many different tasks:

enum FeatureFlags {
    case onboardingV2
    case usePracticePlanner
    case environmentValue(value: String)

    var isEnabled: Bool {
        switch self {
        case .onboardingV2:
            return true
        case .usePracticePlanner:
            return false
        case .environmentValue(let val):
            return ProcessInfo.processInfo.environment[val] == "true"
        }
    }
}

// Later on...

if FeatureFlags.onboardingV2.isEnabled {
    NewOnboardingView()
} else {
    OnboardingView()
}

Wrapping Up

In practice, many of my enums become tiny little view models. Just because they can do so much — should they? Personally, I have never been one to get lost in the weeds on questions like that in my career. I simply enjoy the flexibility that the Swift language provides, and I deploy these tiny little powerhouses to make life easier and my apps ship faster.

Enums represent the Ne plus ultra of Swift’s type system — what was historically a tiny little construct relegated to simple multiple case representations has instead evolved into a durable, flexible type capable of handling just about any situation that both Swift and SwiftUI could demand of it. As such, the evolution has been something along the lines of using a lightweight NSObject , which became a lightweight Struct, and then become a lightweight Enum. What a time to be alive 🍻.

Until next time ✌️

···

Introducing Scores for NCAA Sports

// Written by Jordan Morgan // Jan 28th, 2025 // Read it in about 4 minutes // RE: The Indie Dev Diaries

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

All I wanted was Apple Sports for Division II and III sports.

It didn’t seem to exist, so I made it. Introducing Scores for NCAA, an extremely fast and easy way to view stats and scores for your favorite men’s and women’s NCAA teams — across Division I, II and III:

A screenshot of Scores for NCAA on iOS.

While it works great for Division I, where it really shines is for folks who follow Division II and III teams. Want to know how the Division 2 overtime thriller between the Huntsville Chargers and Valdosta St. Blazers went last night? Covered:

A screenshot of Scores for NCAA on iOS showing a D2 boxscore.

Or, how about your favorite D3 women’s hockey team? Who hit the game winner between the Trine Thunder against Lebanon Valley? It was Payton Hans (great shot, Payton!):

A screenshot of Scores for NCAA on iOS showing a D2 boxscore.

Personally, I live for an upset alert. I love when a D2, D3 or NAIA school beats a D1 team. Here, you can easily check when smaller schools are playing Division I universities (unfortunately, there was no trace of an upset on this one):

A screenshot of Scores for NCAA on iOS showing a D2 boxscore.

Finally, the app is very fast. Look at how quickly you can page between scores here:

A screenshot of Scores for NCAA on iOS showing a D2 boxscore.

So that’s Scores for NCAA. It has some other nifty features, but you can download it for free and check them out.

But wait Jordan, how does this compare to Apple Sports?!

Great question, and I’ll just copy and paste what I have in my press kit:

Apple Sports is wonderful and I use it daily, but it has a few things missing I really wanted:

  • The biggest one? It only covers Division 1 sports. It does not cover Division 2 or 3, which Scores for NCAA does. In fact, as far as I am aware, it is the only app on the App Store which does.
  • It also only shows scores/games for yesterday, today and “upcoming” — Scores for NCAA can you show games from any date. Three years ago. Today. Tomorrow. Whatever.
  • It doesn’t have light and dark mode, which is a small nitpick but still bothers me.
  • And it’s only on iPhone. Scores for NCAA will eventually be on iPad and Mac.

Go check it out

There is so much more to add. Obvious stuff, too. But I had to cut it here for version 1, otherwise I would’ve kept at it and the current winter sporting seasons would’ve come and gone. I think there’s enough value here today for college sports fans to enjoy it.

All of this is just twenty bucks a year, while viewing all of today’s action being free. Today, it shows the major winter NCAA sports (basketball and hockey), but I’ll certainly be adding other sports as they arrive in season. And yes, football being the primary one - I just couldn’t get it in for the initial release.

Until next time ✌️

···

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. 

···