[SC]()

iOS. Apple. Indies. Plus Things.

Keeping Up with Swift and iOS

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

Man, kids, amirite?

This last week I had one get sick, one wake me up during the night three times and then my oldest (come on - my oldest, and easiest kiddo!?) decided to pick a fight every night this week about what blanket his brother could sleep with.

All that to say, I’m a little tired these days. The blog posts floweth slower, the side projects dust up a bit - you basically have enough juice in ya to go to your job and keep your kids alive. And yet, the call of the ol’ blog remains strong.

So, all that to say, I ended up here: I wanted to write something, but something short (for once!) and helpful. Which reminded me about a Twitter D.M. I had received earlier in the week. It read, in part:

But one thing I’ve been wondering is, what’s the best way to keep up? There’s visionOS, SwiftUI was recent. I feel like it’s hard to know where to focus or at least see what’s happening.

Fantastic question! Let’s answer it then. The age old question of “How do you keep up with Swift and iOS: The Abbreviated Version Because I’m Tired” - rolls of the tongue, no?

The answer for me? Less is more. We’ve got 1,001 ways to consume iOS content - a wonderful place to be, no doubt - so I just hone in on a few and roll with it.

My approach is not novel. I simply have all of these bookmarked, and I start the week checking them out.

iOS At Large

  • Apple Developer News: This is my first stop. Mission critical information seems to hit here first. For example, did you know that we all get 25 hours of compute time for free now using Xcode Cloud? You would if you kept your eyeballs locked here.
  • New Technologies from WWDC: This one is great. Each year, they have a page like this that simply shows you the TL;DR of what’s new. The heavy hitters. Along those lines…
  • All WWDC Code Samples: Did you know that Apple puts all of the current year’s WWDC code samples in one place? Once you watch a session and want to dig in, this is the spot you’ll want to go.
  • Tech Notes: I was happy to see that Apple has recently revived its tech notes formats. It’s an absolute goldmine, written by Apple’s finest.
  • The Developer App: Of course, most of this is housed within the Apple Developer App. Formerly known as the WWWDC app, it was a time-honored tradition to update this annually and see the new icon for that year’s conference. Today, it’s simply used year-round. Check out their News tab every week to see important stuff from Cupertino & Friends™️.

Honing Skills

  • App Store Articles: This is a fun resource. I check this out every few months, if nothing for inspiration. It’s updated frequently, too. There’s tips on how to get featured, use nascent features such as custom product pages and more.
  • iOS Dev Weekly: This is my go-to newsletter for finding interesting articles written by the community throughout the week. There are so many smart people writing valuable stuff, it’s hard to keep up. Dave does it all for you, though. It’s a must-read and a Friday tradition for me at this point.
  • Framework Updates: This may be my favorite link out of all of them. If I had to recommend one to keep up with, it’s this one. Apple puts all of the important framework changes right in here.

Swift

  • Swiftly: Look, I am probably not as great at writing code as you think. I am good at looking at references, and then making code work, though! This is one such reference that “unstucks” me when I forget how to do something seemingly simple in Swift.
  • The Official Swift Blog: You’d be surprised at how many folks aren’t aware that there is an official Swift blog. It’s fantastic! Everything from language features to proposals are discussed there.

Bonus
The Human Interface team has absolutely slayed it with their web presence recently. The HIG is required reading, but more than that - they even have changelogs for it now. Further, Apple recently started a newsletter giving you the best parts of the links above, “Hello Developer”, it’s great! I think if you’re in the developer program, you automatically get enrolled in it.

And that’s how I do it. Ten basic links that I visit often. Of course, this is nothing to say of the wonderful world of writers, YouTubers and bloggers out there putting out wonderful work. Find what clicks with you, and keep at it. But, if you’re curious how I’ve done it, this is it.

Anyways, I’m going to ask my kids to let me sleep now.

Until next time ✌️.

···

Checking Out Assistive Access

// Written by Jordan Morgan // Nov 20th, 2023 // Read it in about 3 minutes // RE: The Indie Dev Diaries

Assistive Access is a new accessibility mode Apple introduced alongside iOS 17. It did take me a bit to fully grok what place the nascent mode should play in the accessibility landscape, but after playing around with it - I think I’ve got an idea.

Assistive Access was built to reduce cognitive load, paring down iOS to its bare minimum to make sure it’s accessible for anyone to use. Here’s the Home Screen with it turned on (with the grid option showing on the left, and the list option on the right) - image courtesy of Apple:

A screenshot of Assistive Access on iOS.

It’s hard to take a wrong turn when Assistive Access is running, with call-to-action buttons displaying prominently along with guardrails aplenty. The purpose and direction is clear, and that’s all thanks to Apple’s talented accessibility team. Now, consider apps running in Assistive Access. Here’s Messages:

A screenshot of Assistive Access on iOS with the Messages app open.

…and Camera:

A screenshot of Assistive Access on iOS with the camera app open.

The apps are more than simplified, text is bigger, every transition is a navigation push (and a slower one, at that), and if there is a button or action to take, it’s always located right at the top or bottom. They’re purposefully large too - basically, impossible to miss.

let concatenatedThoughts = """

Assistive Access is quite customizable, too. Guardians can choose the apps that are available, options of what to hide or show within them, and even things like toggling the battery indicator. You can find these options under Settings -> Accessibility -> Assistive Access.

"""

Considering those user experiences - you can begin to see why senior citizens, folks with cognitive disabilities, or a parent or guardian overseeing the technological landscape for their kids are some ideal use cases I think Apple had in mind when building this. At first, I described Assistive Access as “iOS on easy mode”, but the more I thought about it - I think that’s doing a disservice to Assistive Access.

In fact, it’s perfectly named (in true Apple fashion). It gives access to iOS to literally anybody who might need a bit of assistance moving around. Or, maybe those who simply should require it - I can see myself enabling this on my phone if my oldest kid wanted to go a nearby neighbor’s house and I wanted a way to quickly reach him.

When you take all of this into consideration, the few steps of friction in place to turn it on altogether start to make more sense. It’s purposeful friction. This isn’t something that I think will be toggled on and off on a whim, instead - I liken it to something that you enable and use for very long (if not indefinite) periods of time.

Though I am always naturally curious about new accessibility advancements from Cupertino & Friends©, it does lead us to the obvious question as developers:

What do I need to do in my apps to make them work well with Assistive Access?

Well, it turns out - basically nothing?

When Assistive Access is on, really the only thing to consider is the size you get to draw layouts. In short, you’ll get a reduced frame to display your app. Remember, the large back button is always displayed, and due to that - you’ll have a smaller window. This should be no issue at all if you’ve followed modern layout practices to any degree. Though, I do think safe area layout guides report updated margins for Assistive Access.

However, if you add UISupportsFullScreenInAssisstiveAccess in your info.plist key, you’ll get the full screen once again. That is, your app will simply scroll under the back button instead of being clipped by it. As far as I can tell, every stock Apple app works this way aside from Camera1, so at first I wasn’t sure why this wasn’t the default. But looking at my own apps, it quickly made sense. For example, a tab bar would be completely hidden if that key were showing.

TL;DR - You have to completely design a specific UX for Assistive Access, as Apple has done with their own apps. None of us have, which is why I think that key defaults to NO.

Here’s the big one, though: it’s simply puzzling to me that there isn’t API to determine if it’s enabled or not. No UIAccessibility.isAssistiveAccesRunning, like we have with VoiceOver, bold text, switch control and basically every single other accessibility technology that Apple offers. I would absolutely love to offer a mode of my app for Assistive Access, similar to the ones Apple has crafted for Messages, Music, Phone and their other apps. But as of right now, I just can’t. That, to put it kindly, doesn’t feel great - and greatly limits what Assistive Access could truly be.

Final Thoughts

If iOS is truly all about empowering people to do basically anything imaginable with the super powered computers in our pockets, then Assistive Access is about making sure everyone can do the core, most-important parts of those experiences, too. For some, opening an app to a plethora of menus, images, sliders, buttons and more produces the opposite effect of productivity - it’s hard to do anything when you can do everything. For those folks, a streamlined, easier-to-use app is, almost paradoxically from a development standpoint, what opens them up to use the app at all.

It’s not even less is more, it’s less is anything. I’m glad Apple thought of such a mode, and it brings me joy to think that those who maybe couldn’t even use iOS before, can now. I just hope Apple opens it up to the rest of us though, because we’d love to help in that mission, too.

Until next time ✌️.

  1. And that makes sense, because it doesn’t need to scroll at all. It’s just the viewfinder. 

···

The First 100 Subscribers for Elite Hoops

// Written by Jordan Morgan // Nov 7th, 2023 // Read it in about 3 minutes // RE: The Indie Dev Diaries

Elite Hoops recently crossed 100 subscribers. It’s a great, if not little, milestone for me personally - this was my goal to hit before year’s end. And in November, I’m there already (about a month after launching). Yay!

A screenshot of Revenue Cat's dashboard showing 100 active subscribers.

They say your first x of anything is always the hardest. The first 1,000 fans. The first million. The first dollar made. Whatever it is for you, for me, it was the first 100 subs. Today, I thought I’d unpack how I got there so anybody else can steal some tips and tricks who are just starting out.

let concatenatedThoughts = """

Even better, this doesn't include one-time purchase users. There are a few of those each week, which is always nice to see. It's priced at $150.

"""

New Apps Have it Hard

There are so many takes out there about the App Store and how it treats fresh releases. Put a keyword as the first word in the title! Game ASO! Use in-app events solely for search! Ask for a rating immediately to boost them!

I don’t blame anyone for trying these things, because if you are a new app on the App Store, it’s tough to get going. You don’t show up in search, it’s not easy to find your app, you need ratings and reviews…the list goes on.

But, that’s just how it works - so we have to deal with it.

Here’s how I’m evening the odds:

  • Apple Search Ads: They’ve worked well for me. My conversion rate is over 65%, and staying steady there for the last three weeks. I followed the guide written by the guy behind Apple Search Ads Optimization AI and learned quite a bit from it. I spend around $16 a day on these.
  • Mailing List: I built this up beforehand, and I previously wrote a bit about it. It’s helped tremendously, and the customers on it convert well.
  • Instagram: Social doesn’t work well for every app, but it does for mine. Sharing a play I created with Elite Hoops works two-fold: it helps an important segment of my target customer (youth basketball coaches) by learning a new play they can use, and it shows my app in action.

Business Casual

If you want an app to grow, cliche as it sounds, it’s helped me to think of it more as a business instead of an indie project. Spend Stack was an indie thingy for me, a way to show off a good looking app that used Apple’s APIs but inside a product that didn’t make a lot of sense.

I think the sweet spot is to hit both of those segments (well-crafted, doing the platform well while being a product with an obvious use-case). You see it with Flighty, Slopes, Cardpointers and several other apps that manage to use Apple’s Cool New Thing™️ but in a way that truly makes their customer’s problems go away, or get a bit easier. Plus, getting those Apple features in the App Store can be a wonderful way to get steady downloads.

Put simply: when you think about your app as a business, it makes you do business-y things. Take my mailing list:

  • I ask to join it during onboarding. Nearly 60% of new users join.
  • When they do, I use an automation to send them a “Welcome to Elite Hoops - here’s how to get started” email.

That seems so pedestrian, right? But, this is the simple kind of stuff I never used to do. And it helps! Along the same lines, I take note of what kind of coaches are using the app to help me decide where to take things next:

A Mixpanel dashboard showing the types of coaches who use Elite Hoops.

Timelines

Even though I just sorta launched Elite Hoops without the usual pomp and flare, I did plan ahead to gain my first 100 subs. Here is a condensed timeline:

  1. Get a website up with email capture.
  2. Get as many potential customers there as you can (the goal being mailing list sign ups)
  3. Get approved by the App Store and open up pre-orders.
  4. Tout features on your mailing list leading up to launch.
  5. Hit the big red button.

That was basically how I went about it. On launch day, it charted in the Sports category - peaking at 85 maybe? - and on iPad it charts consistently around 80-120. But, I didn’t launch to an empty room. Coaches already knew about it, and that helped get things moving. With people eager to try it out on launch day, I already had a nice chunk of feedback to take in and chew on.

What’s Next?

My goal is to get to 5,000 paying users. That would, barring any pricing tweaks, put Elite Hoops at $200,000 ARR (or $16,666 MRR). If I look at my numbers now, if things stayed as they are and it grew at the same rate (~2-6 new paying users daily) then I would be there in about three and a half years. There are so many ideas on how to get there:

  • Hit the biggest request: User logins and play sharing with teams in-app.
  • Pricing experiments: At $40 a year, I think it’s reasonably priced - if not too low, perhaps. Definitely too low for colleges, of which there are many already who are paying for Elite Hoops.
  • Keep sharing: Social is doing well for me, and I try to send at least two emails a month on features, updates or new videos I created on its nascent YouTube channel.
  • Connections: There are a several coaches and trainers who’ve reached out to partner up. I don’t want what it looks like yet, but that could help.
  • SEO: I’ve got several ideas of how to use its website to reach more coaches who are searching for this type of app via Google.

Regardless, I’m proud of where Elite Hoops is at. The best part is that it truly is helping coaches, which is what it absolutely must do to succeed. My favorite piece of feedback so far has been this:

Hello, was very skeptical at first, as I’m sure you’re aware…almost every “coaching app” or “whiteboard app” for the iPad is super minimal and kind of cheesy in my opinion. I was truthfully in the process of looking into making something like this until I gave Elite Hoops a shot. I think it’s phenomenal so far.

Can’t argue with that! If you want to give Elite Hoops a spin, check it out here.

Until next time ✌️.

···

Generating Random Numbers Elegantly in Swift

// Written by Jordan Morgan // Oct 24th, 2023 // Read it in about 2 minutes // RE: Swift

I love caffeine. Like, I love caffeine. Lately, I’ve been pecking away at a little pet project to log mine. Since I mostly consume espresso, my caffeine logging usually boils down to one of four choices. I log either a…

  • Single shot (~ 64mgs of caffeine)
  • Double shot (~ 128mgs of caffeine)
  • Triple shot (~ 192mgs of caffeine)
  • Quad shot (~ 256mgs of caffeine)

As such, I have this little guy:

enum EspressoShot: Int, CaseIterable {
	case single = 64, double = 128, triple = 192, quadShot = 256
}

While whipping up some testing data, I wanted a random value from those. Easy enough to support:

enum EspressoShot: Int, CaseIterable {
	case single = 64, double = 128, triple = 192, quadShot = 256
	
	static func randomShot() -> EspressoShot {
		return EspressoShot.allCases.randomElement() ?? .single
	}
}

Certainly, that works:

let randomShot: EspressoShot = EspressoShot.randomShot()

But, after browsing some docs, I saw that you can easily plug in Swift’s SystemRandomNumberGenerator for your own types. RandomNumberGenerator itself is a protocol, but you don’t have to manually adopt it most of the time. Take a look:

enum EspressoShot: Int, CaseIterable {
	case single = 64, double = 128, triple = 192, quadShot = 256
	
	static func random<G: RandomNumberGenerator>(using generator: inout G) -> EspressoShot {
		return EspressoShot.allCases.randomElement(using: &generator)!
	}
	
	static func random() -> EspressoShot {
		var g = SystemRandomNumberGenerator()
		return EspressoShot.random(using: &g)
	}
}

The first function supports passing in any random number generator that adopts the bespoke protocol. However, the bottom one is super useful - we can simply use the built-in random number generator, and then pass it to the previous function to get our random value:

let randomShot: EspressoShot = EspressoShot.random()

Beautiful. Here’s why I like this:

  • The call site is a bit leaner.
  • It’s also more familiar, as Type.random() is a common Swift convention.
  • We don’t have to deal with force unwraps at the call site. It’s hidden.
  • It also supports using any random number generator, while our hand rolled one could not.
  • I’ll always use Swift’s implementation whenever possible. Their random number generator is automatically seeded, thread-safe and uses the appropriate APIs depending on the platform Swift is running (i.e. Windows (BCryptGenRandom), Linux (getrandom(2)) or Apple (arc4random_buf(3))).

It’s de rigueur for Swift codebases to be, well….Swifty, is it not? And this feels a bit more Swifty to me.

Update: The open source maestro himself, Sindre Sorhus, offered a nice solution wherein you can make any type conforming to CaseIterable have these capabilities:

extension CaseIterable {
	public static func randomCaseIterableElement(using generator: inout some RandomNumberGenerator) -> Self {
		allCases.randomElement(using: &generator)!
	}

	public static func randomCaseIterableElement() -> Self {
		var generator = SystemRandomNumberGenerator()
		return randomCaseIterableElement(using: &generator)
	}
}

enum Foo: String, CaseIterable {
	case a
	case b
	case c
}

print(Foo.randomCaseIterableElement())

Check out his gist here.

Until next time ✌️.

···

Masking Third Party Dependencies

// Written by Jordan Morgan // Oct 17th, 2023 // Read it in about 1 minutes // RE: Swift

Since shipping Elite Hoops, I’ve found myself using a few third-party dependencies. Namely, Mixpanel for some very lightweight analytics. Did the person open this tab, see that thing, etc.

Today’s little post is simply a reminder to abstract this code away. For example, instead of this:

func didTapNewTeam() {
	Mixpanel.mainInstance().track(event: "Created a New Team")
	// Code to create a new team
}

Opt for a light Struct to house that logic:

struct Telemetry {
	static func sendDidTapNewTeam() {
        guard !UIDevice.current.isSimulator else { return }
        Mixpanel.mainInstance().track(event: "Created a New Team")
    }
}

Now, your call site is none-the-wiser:

func didTapNewTeam() {
	Telemetry.sendDidTapNewTeam()
	// Code to create a new team
}

However, this can save you some heartache in the end. To wit - when I began Elite Hoops, I was using Telemetry Deck. While it’s a nice product, it didn’t quite click for me. But, since I used this pattern all over already, changing over to Mixpanel was a one line change. I only had to edit didTapNewTeam(). If I hadn’t, I’d be making several updates throughout the app where I used any analytics.

In college, I remember seeing this pattern floated around as the “repository pattern” - wherein your “DAO” (data access object - we’re going real .NET here for a minute) had no clue about the inner-working of your actual CRUD code. It just called save(), delete() and update(). But, whichever database service or API you were using was all housed within those functions.

Whatever you call it, be it abstraction or some sort of pattern, I recommend masking any third-party code behind your own object or struct. By nature, you’re giving up some control when you use it, and for that reason third-party code tends to be inherently obstreperous at some point. If you mask it away, though, it makes refactoring affairs a breeze.

Until next time ✌️.

···