[SC]()

iOS. Apple. Indies. Plus Things.

The Prelaunch, Launch

// Written by Jordan Morgan // Sep 21st, 2023 // Read it in about 1 minutes // RE: The Indie Dev Diaries

Ahhh, that warm, fuzzy and panicking feeling that only a launch can give you. It feels good to be back, baby šŸ˜Ž. Since annoucning Elite Hoops, I’ve been hard at work at what I’ve been terming a ā€œPre Launch, Launchā€. I thought I’d share what exactly that is, and why I’m doing it.

First off, during this sabbatical time, I’ve been spending the first few weeks getting the book series done. That’s paid dividends, as I’ve done nearly a year’s worth of planned work in three weeks. During that time, though, I wanted to at least announce Elite Hoops, and get the ball rolling.

Since I knew I wouldn’t have much time (I plan to get the proper launch in a few weeks), but I did have a few weeks - I came up with a gameplan:

  1. Get a website up.
  2. Capture emails.

Literally, that’s it. If I could at least gather 100, cold-call emails from coaches who were interested - I figured that would be a win. That would be my pre launch, launch.

So, I got to it and did my annual front end development task.

I love overthinking websites, and the typical result is that I spend about a month on them. I made the Elite Hoops one in two days, a massive achievement for me. I could go on about the art of time boxing yourself, but that’s all it was. I knew it wouldn’t be a long, expansive page. It would simply have a hero tagline, and a box to sign up for a mailing list. If they scrolled down, it would list some features.

That’s almost exactly how it went down, all wrapped up in a jiffy thanks to Tailwind CSS and Netlify:

Elite Hoops marketing banner with several iPhones running the app.

And herein lies my not-so-secret sauce to achieve my pre launch, launch goals. A pretty good website with a purpose (email sign ups) paird up with a pretty good Instagram reel that links to it.

I made a somewhat compelling teaser video of the app, about 12 seconds long (and yes, I bought another Rotato license for like the third time). But, I avoided having to buy ScreenFlow once more (i.e., I lost email access to my old Dreaming In Binary accounts with the licenses) because I found an unexpected alternative.

With iPad apps on the mac, I figured there must be at least one I could use. And I found one: CapCut on macOS. I don’t think it’s the iPad running on Mac dance, perhaps it’s Catalyst? Or native? Who cares.

Like, it’s great!? I cut up the rest of the video in there, and it was so easy to add titles, transitions and even music:

A screenshot of CapCut on macOS

Yes, there are some ads and upsells - but hot dang, it does everything I used ScreenFlow for. Not only that, it does it just as good or better! Once I had that, I plopped it on Instagram, boosted the post for about $80 bucks…and, it seems to have worked:

  • Over 200 coaches have joined the mailing list.
  • The reel has over 18,000 views.
  • Several emails from coaches asking for more details.
  • And of those, several colleges, High Schools and travel clubs. My target market ā¤ļø.

So, that’s my pre launch, launch. So far, so good. But of course, there’s always more to do:

A screenshot of Things 3, listing out tasks to complete a pre launch of Elite Hoops.

Until next time āœŒļø.

Ā·Ā·Ā·

Introducing Elite Hoops

// Written by Jordan Morgan // Sep 15th, 2023 // Read it in about 2 minutes // RE: The Indie Dev Diaries

I’ve often said that every app has a story. And 1,531 days after I announced my last app, I’m here to tell you another story, and start on a new adventure.

I’m happy to formally introduce the world to Elite Hoops, launching soon:

Elite Hoops marketing banner with several iPhones running the app.

Quick Links

The Backstory

My oldest son lives and breathes basketball. He is also a visual learner. So, when he had trouble learning a full court press - I scratched out how it worked on a piece of paper. He loved that, but he wanted to review it again tomorrow.

And the next day, and the next day after that. So on.

I wanted a way for him to review those plays anytime. And, as it turns out, coaches want an easy way to share their own plays, explain defensive concepts and get them to their players and coaching staff.

On iOS, there isn’t a phenominal way to do this. And there definitely isn’t a great way to do this with audio commentary attached, either. Of course - you know where this is going.

So on September 12th, 2022, I cracked open a new Xcode project with no intentions of starting a new LLC, app or business venture at all. But, after my son instantly clicked with the app, started explaining plays back to me and watching the videos we made - I thought I might have something.

Luckily, I have no shortage of basketball coaches and parents in my life. The response was the same each time I showed it to them, ā€œWhen can I use this?ā€ The app had become a swiss army knife for the basketball world. A way to record and share plays, help players visually move around their avatar on a court to explain things, a modern, technology-first whiteboard. And, clichĆ© though it may be, that is just the start.

So this past year, Elite Hoops has been a nights and weekends project for me, squeezing out any work I could in-between the book series and this website. It’s at an M.V.P. point for me, so check it out! If you are a basketball coach, basketball parent or know any - send them to the Elite Hoops homepage. I’ll skip going over all of its features here, I think the website does a good enough job of it. I will say, though, this thing shines on iPad - it’s the best way to use it.

Elite Hoops marketing banner with an iPhone and iPad running the app.

The Indie Stuff

This app has been an entirely different beast than my last app, Spend Stack. I’ve been much more…brutal?…with this one. Spend Stack was a boutique app for me. I wanted it to be the pinnacle of what third party apps should stand for on the App Store. I wanted it to be crafted.

With Elite Hoops, I am trying to make a business. And so, I’ve punted several things that are near and dear to me. They’ll land in future updates. I live for implementing App Intents, widgets, quick actions - you name it. But here, I’ve had to be a bit more honest with myself.

Spend Stack took five years to launch. And that was before I was writing a five book series. I’ve managed to launch this in a year, and I’m proud of that - I think I took all of the right lessons, for me personally, from Spend Stack.

The point isn’t that I won’t be putting an emphasis on crafting a well built experience that pushes the platform. The point is that I’m launching a to-the-point M.V.P., knowing those platform features will make it in. Just not at launch.

Further, this is also my first SwiftUI-based app. What a journey that has been - to wit, there were only a few things I couldn’t do, a few rough edges I couldn’t quite punch out using the framework. It’s come a long ways, so color me impressed. Plus, it is what it is - there is no way I could move this fast in UIKit.

Final Thoughts

I can’t wait to officially join back the indie ranks! Things are coming together, and I’m pumped. As many who’ve read this website for years know (and thanks for reading, by the way!) - my professional scratches must be itched in several ways. I want to write, I want to ship apps and I want to help.

Now, finally, I’m in a spot to do all of those things. The book series is nearly done, I’m able to still write blogs posts and now I’m shippin’ something! Here’s hoping app review treats me well šŸ»!

Until next time āœŒļø.

Ā·Ā·Ā·

Generated Asset Catalog Symbols in Objective-C

// Written by Jordan Morgan // Sep 14th, 2023 // Read it in about 2 minutes // RE: Objective-C

Sweet harmony, if String-based APIs aren’t the worst? Especially for those us working in Objective-C codebases and using our friend, NSAttributedString, often used to stylize labels in interfaces:

- (NSAttributedString *)configureBrandString {
    NSDictionary *attrs = @{NSForegroundColorAttributeName: [UIColor blueColor]};
    NSAttributedString *brandString = [[NSAttributedString alloc] initWithString:@"Hey there!"
                                                                      attributes:attrs];
    
    return brandString;
}

But wait - [UIColor blueColor] is so boring. Your designer wants to use that fancy brand color, so go for it:

- (NSAttributedString *)configureBrandString {
    NSDictionary *attrs = @{NSForegroundColorAttributeName: [UIColor colorNamed:@"BrandPrmary"]};
    NSAttributedString *brandString = [[NSAttributedString alloc] initWithString:@"Hey there!"
                                                                      attributes:attrs];
    
    return brandString;
}

And crash. Can you even spot it?

We misspelled ā€œBrandPrimaryā€ as ā€œBrandPrmaryā€, and since Objective-C doesn’t appreciate nil values in a dictionary, we get a nice, lovely runtime crash. Yay for Strings! Apple knew this, and finally asset catalogs can generate your color and image names for you in Xcode 15. No more nil values!

let concatenatedThoughts = """

In fact, this problem of String based asset catalog items bothered me so much, I made a CLI tool to generate symbols for them years ago.

"""

In Swift, it just works. Take this asset catalog, with a color of ā€œBrandPrimaryā€ and an image called ā€œBrandLogoā€:

An Asset Catalog in Xcode with a color and image.

…over in Swift - we can’t screw up using them:

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Image(.brandLogo)
            Text("Generated Assets")
                .foregroundStyle(.brandPrimary)
        }
        .padding()
    }
}

So much so that, if I were to delete either of those from the asset catalog, the project wouldn’t build anymore! The static properties off of either ColorResource or ImageResource are removed, so I’d be using code that no longer exists (i.e., .foregroundStyle(.brandPrimary)).

And so it is, Objective-C can reap mostly the same benefits!

Just import ā€œGeneratedAssetSymbols.hā€ and you’ll gain access to NSString constants representing each symbol:

#import "User.h"
#import "GeneratedAssetSymbols.h"
#import <UIKit/UIKit.h>

@implementation User

- (NSAttributedString *)configureBrandString {
    NSDictionary *attrs = @{NSForegroundColorAttributeName: [UIColor colorNamed:ACColorNameBrandPrimary]};
    NSAttributedString *brandString = [[NSAttributedString alloc] initWithString:@"Hey there!"
                                                                      attributes:attrs];
    
    return brandString;
}

Notice the constant ACColorNameBrandPrimary for the color. It even comes with some lightweight documentation that its Swift counterpart enjoys:

The ā€œBrandPrimaryā€ asset catalog color resource.

It keeps up with changes, too. Rename the asset, delete it - whatever, it regenerates the same as Swift code does.

I freaking love this.

Why? Not because I do cartwheels for Objective-C additions, but because I still maintain a lot of Objective-C code. And in its Wild West World, I battle nil values all the time. Now, my code is a lot safer.

Before we close, here a few notes I caught from the release notes over generated asset symbols:

  1. You can disable generated asset symbols via the ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS over in build settings. Just set it to NO. Maybe this is useful if you get some symbol clashes or something similar.
  2. You can pick and choose which frameworks you want to support with the ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOL_FRAMEWORKS build setting. By default, you get all of the technologies your app uses, but if you only want AppKit, UIKit, or similar - you can do that.
  3. Asset generation, for Swift at least, generates not only constants on ImageResource and ColorResource - but also extensions for those, too. So, you could do Color.brandPrimary too.

What a quality of life win. Hats off to Cupertino & Friendsā„¢ļø.

Until next time āœŒļø.

Ā·Ā·Ā·

Vending Non-Mutating Data in SwiftUI

// Written by Jordan Morgan // Aug 7th, 2023 // Read it in about 2 minutes // RE: SwiftUI

It’s well noted that imperative interface frameworks tend to have their bugs manifested from mutating data. It’s all too common to display stale or incorrect interface values which don’t accurately reflect their backing view model. That’s just one scenario where you can get into trouble, but we could go on with the rough edges of imperative user interface frameworks.

Today, SwiftUI has swept in and practically eliminated that entire class of error. I’ll avoid writing another treacly blog post proclaiming declarative interface advantages though. To wit - I’m here to do somewhat of the opposite. I recently hit a situation I hadn’t yet encountered in my years of SwiftUI.

Data, but the kind that doesn’t mutate at all.

How do you do this? Is there an idiomatic SwiftUI API for this? The answer wasn’t immediately obvious to me.

Consider any composing experience, the likes of which you’d see in virtually any app where you create a post, edit some details, append notes or anything else in-between:

Three composing experiences on iOS.

I had a similar case recently - our composing experience for Buffer. Our nascent SwiftUI version is contained in a Swift package, used in extensions and our main app target. Today, it takes in ā€œpopulationā€ data in our current Objective-C and UIKit based code. It has metadata like user information or domain-specific identifiers, whether or not you’re editing a post or creating a new one. That kind of stuff.

What’s interesting here is that none of that changes.

The composer opens, uses that metadata for a lot of things - but it never changes it.

So, What’s a SwiftUI Version of That?

Usually, in a SwiftUI world - a simple top level state object is at play here:

struct MyView: View {
	@StateObject private var metadata: Metadata = .init()

	var body: some View {
		ViewsAndMoreViews()
			.environmentObject(metadata)
	}
}

There are so many SwiftUI tools available to manage mutating data from bindings, state and the more modern Observation framework and its macro.

That’s all well and good, but we’re coming in hot from an Objective-C call site - so this doesn’t really mesh. After quite a bit of back and forth with some other friends - we chatted about dependency injection methods, using other SwiftUI semantics but bridging them and some more thoughtful patterns.

I do want to point out that, true to this post’s title - you could use any of those to handle non-mutating data in SwiftUI. But remember where we are: Objective-C and UIKit land, needing to present a SwiftUI view wrapped in a hosting controller.

I like where we landed considering this, it’s technically a mix of dependency injection, just with SwiftUI flavorings: Create a custom environment key, populate it when creating the SwiftUI view, and then pass it back wrapped in a hosting controller.

Here’s how it works. The Objective-C code calls into a Swift shim that sets up a metadata object from anything the Objective-C caller feeds it. Then, it takes care of creating the view and the hosting controller. We present it and we’re done, the immutable data is available all over.

What follows is a simplified example.

The Approach

First, we’ll need the environment key. This little mechanism lets us reach into this data anywhere in our SwiftUI views. That’s perfect for us, none of this data mutates. It allows us to play nice with previews by setting up lightweight defaults, we don’t need to pass it in each view’s initializer - the list goes on:

private struct ComposeMetadataKey: EnvironmentKey {
    static var defaultValue: ComposingMetadata { .empty }
}

public extension EnvironmentValues {
    var composingMetadata: ComposingMetadata {
        get { self[ComposeMetadataKey.self] }
        set { self[ComposeMetadataKey.self] = newValue }
    }
}

Now, we can peek into it anywhere we need that metadata:

public struct ChannelListView: View {
    @Environment(\.composingMetadata) private var metadata: ComposingMetadata
    
    public init() {}
    
    public var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(spacing:4) {
            	// Access the metadata
                ForEach(metadata.channels) { channel in
                    ChannelGroupListItemView(channel: channel)
                }
            }
            .padding()
        }
    }
}

Now, back in Objective-C - we use a shim to easily call into SwiftUI and get the metadata assigned:

func createComposerViewController(with info: Info) -> UIViewController {    
    let metadata = SwiftUIComposer.ComposingMetadata(with: info)
    
    let composeView = SwiftUIComposer.ComposerView()
        .environment(\.composingMetadata, metadata)
    let host = UIHostingController(rootView: composeView)
    
    return host
}

And then, over back in dino land:

SwiftUIComposerShim *shim = [SwiftUIComposerShim new];
UIViewController *composingVC = [shim createComposerViewControllerWith: composingInfo];

[viewController presentViewController:composingVC 
		        animated:YES 
		        completion:nil];

Voila.

That’s how I ended up solving it, and it worked quite well. If you have other solutions, I’d love to hear them.

Until next time āœŒļø

Ā·Ā·Ā·

The Book Sabbatical

// Written by Jordan Morgan // Jul 31st, 2023 // Read it in about 4 minutes // RE: The Indie Dev Diaries

I don’t know if you’ve heard, but I’ve been writing a book series over iOS.

Kidding, I know you’ve heard šŸ˜….

Though, what is a little crazy is that I started this journey over two years ago. As best as my Twitter (not calling it X) investigations can see, I announced this on more or less February 16th, 2021:

So, only a few months removed from letting go of Spend Stack. Since then, it’s been a roller coaster of experiences. I’ll keep it short and succinct:

  1. Ok, I don’t have an app right now - I want to write a book!
  2. Hmm, too much for one book - I want a book series.
  3. Announce it, open sign ups - whoa, people actually want this, awesome.
  4. Write the first nine chapters and release it? Yup, I’ll do that.
  5. Release it! Oh my…I just made…a lot more money than I’ve ever made.
  6. AWWWWWW YEEEEEEAHHH I’M UNSTOPPABLE.
  7. About a solid year goes by, and sales remain solid.
  8. Another year goes by, and I sell about 3-4 a week.
  9. …and realize I’m still years away from finishing the first table of contents.
  10. A mix of being proud of myself laced with the rich, unforgiving sting of opportunity cost mixed with pure mental exhaustion begin to overtake me šŸ˜…. I slowly start to feel a real sadness of not having my own app to work on, and realize that’s a huge part of not only my identity, but something I genuinely enjoy and truly need.

Currently, I am at step 10. I need this series to be finished, and so I’m using an incredibly generous perk of my job at Buffer to do it - taking my two month sabbatical to write the rest of it.

let concatenatedThoughts = """

When I say "finished", I mean complete the original table of contents. As I'll mention later, I look forward to updating the book series for years and years to come.

"""

As you can guess, writing this thing has just been a massive, huge undertaking. I inadvertently searched for my limit, and I have found it. I’m there. I don’t mean to complain, in fact - the opposite, but what I do want to do is set the stage appropriately.

The book series has been amazing, and recently a wee-bit stressful too. It truly has epitomized ā€œAnywhere worth going takes hard workā€ to me. As a fun little thought exercise, I figured I’d list out the good parts and not-so-good parts that I’ve learned from the experience so far.

The Not-So-Fun Parts

Sometimes, I Feel like a Salesmen: The fact is, if you’re going to offer something for people to buy, you have to sell it. I don’t always enjoy this aspect, but you have to do it. I’ve tried it all: podcast sponsorships, content marketing, Twitter ads and more. Some of it works, some of it doesn’t. But for me it’s the mental part, which, admittedly, I probably put on myself. Even so, sometimes I get self-conscious when I send another tweet about the book series that people might just be like ā€œOh my gosh, we get it.ā€

Opportunity Cost: This, by far, is the biggest thing I have learned. When I was done with Spend Stack, I said something along the lines of ā€œAll I do is write code, I don’t have time to write. I love them both.ā€ Now, as you can guess, it’s flipped. I sorely miss having an app to make, but I simply don’t have the time - all I do is write.

W.W.D.C. Hits Different: When you don’t have your own app to work on, dub dub stings. Of course, this could go in the ā€œprosā€ section just as easily, because my favorite part of writing the book series is that I get to really dig into the improvements each year and write about them. But, not having my own personal app to apply them to, well - that sucks.

A Slight Identity Crisis: Who am I in this community, what is my place? For some who’ve started reading my blog or picked up my book series, I might seem to be an educator of sorts. One of those people who writes tutorials and such. But, I don’t really view myself that way. But, I can’t really say I’m an indie developer when I develop no apps. I hope to find a sweet spot sandwiched in-between both of these worlds.

A Metric Ton of Work: Obviously, doing all of this takes a lot of work. I feel like I’ve constantly had a college paper deadline looming over my head for the past two plus years. And that’s because I have, but if I didn’t set a two week update cadence, I probably never would’ve finished.

The Rainbows and Butterflies

I Get to Write: And I really do love writing. The book series and my experience with Spend Stack has taught me a valuable lesson: I want to write my blog, I want to update the book series and I want to make apps. But, to do those things, they each need a bit more of my attention divvied out realistically. Right now, it’s 85% book series, 10% writing on my blog and 5% working on an app. That’s not sustainable, but I’ll be able to address it sooner rather than later.

Seemingly Evergreen Income: The book is wonderful passive income. It pays for things that are important to me; vacation with my kids, sports and other things of that nature. And, fingers crossed, I think it will stay that way for years to come as I build trust in the community in regards to the series, and my habit of updating it has already been proven. In the two plus years, I’ve not missed one update, and I’m proud of that.

I Learn So, So Much: I rarely write a chapter where I kinda knew what I wanted to know going in. I’m always finding little nuggets of information I wasn’t aware of. People outside of our industry won’t understand this, but the feeling you get when you pour over the docs and find some awesome API you didn’t know about is such a dopamine rush. Here’s my favorite one so far, which I uncovered writing about Quick Look:

The People: Going to conferences or dub dub and having strangers (who turn into friends) come up and say ā€œHey, I bought your book series!ā€ is the highlight of my trips. Not because it’s an ego boost, but because it’s just nice to put faces to people who’ve gotten some value from your work. These conversations are always lovely, along with the tweets that I see where people are reading the series on a flight or whatever.

I Did Something That Was Meaningful to Me: Writing a book series has been a ā€œmoon shotā€ career goal of mine. And, I’ve about done it. That’s really awesome, and I’m just straight up proud of myself for doing it.

Final Thoughts

I am blessed to take two months off to see this through, and I’m thankful to Buffer for even offering it. I know not many can get a perk like this afforded to them.

I do want to stress something here; I am excited to work on this book series for the rest of my career, and I plan too. Right now, I’m in the trenches and getting it done. But once I’m there, it should slide into the natural cadence of my work life naturally. Updating it each year during W.W.D.C. fits in perfectly with the way that I work.

In fact, even before another dub dub - I have an extensive list of things I’d love to cover:

A list of things to update in the best-in-class series.

I have learned, though, that I want to do lots of things. And, if I have one project dominating all of my time - I can’t. Even so, I’m grateful to have learned that. And with that, I set off for the next 8 weeks to see this thing through, and also launch my next app. Onward!

You can catch me at my new office now until November 6th 😃:

A coffee shop called the Workshop.

Until next time āœŒļø

Ā·Ā·Ā·