[SC]()

iOS. Apple. Indies. Plus Things.

Fun Alignment Guide Tricks

// Written by Jordan Morgan // Feb 9th, 2024 // Read it in about 3 minutes // RE: SwiftUI

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

Setting your own vertical or horizontal alignment guide isn’t something I’ve thought about much when writing my own SwiftUI code. When they were announced, and later demo’d during a dub dub session in SwiftUI’s early days, I remember thinking, “Yeah, I don’t get that. Will check out later.”

Lately, though, I’ve seen two novel use cases where using one is exactly what was required. Or, at the very least, it solved a problem in a manageable way.

Rather than write out a bunch of code myself, I’ll get straight to it and show you the examples from other talented developers.

Creating the “Bottom Sheet”

Life, death and creating our own bottom sheet implementation, right? Sheet presentations have become more doable with the nascent iOS 16.4 APIs, allowing developers to set backgrounds and a corner radius on them. But, every now and then, you just have some weirdo requirement that necessitates rolling up your sleeves and doing it yourself.

The first inclination I’ve often seen is to use some sort of ZStack, .padding or .offset incantation:

@State private var bottomSheetOffset: CGFloat = 0.0

GeometryReader { geometry in
    VStack {
        Text("Sheet Content")
    }
    .frame(width: geometry.size.width, 
          height: geometry.size.height, 
          alignment: .top)
    .clipShape(
        UnevenRoundedRectangle(topLeadingRadius: 32, 
                               topTrailingRadius: 32, 
                               style: .continuous)
    )
    .frame(height: geometry.size.height, alignment: .bottom)
    .offset(y: bottomSheetOffset)
}

Or, maybe some sort of .transition:

@State private var showBottomSheet: Bool = false 

NavigationStack {
    // Content
    if showBottomSheet {
        VStack {
            Spacer()
            VStack(spacing: 18) {
                // Sheet content                
            }
            .padding([.leading, .trailing, .bottom])
            .background(.thickMaterial)
            .clipShape(RoundedRectangle(cornerRadius: 24, style: .continuous))
            .padding()
        }
        .safeAreaPadding([.bottom], 40)
        .transition(.move(edge: .bottom).animation(.easeInOut(duration: 0.3)))
    }
}

Me? I’ve done the ZStack route before:

VStack {
    // Content
}
.overlay {
    ZStack {
        // Background dim
        Color.black
            .opacity(0.15)
            .onTapGesture {
                dimissOnCurtainTap()
            }
        // Container to easily push the UI down
        VStack {
            Spacer()
            // The UI
            VStack {
                // Sheet content
            }
            .padding(8)
            .frame(minWidth: 0, maxWidth: .infinity)
            .frame(height: containerHeight)
            .background {
                RoundedRectangle(cornerRadius: theme.screenCornerRadius)
                    .fill(Color(uiColor: .systemBackground))
                    .padding(EdgeInsets(top: 0,
                                        leading: contentPadding,
                                        bottom: contentPadding,
                                        trailing: contentPadding))
            }
            .offset(y: containerYOffset)
        }
    }
    .ignoresSafeArea()
    .opacity(internalIsPresenting ? 1 : 0)
    .allowsHitTesting(internalIsPresenting)

They all mostly work (to varying degrees), but here’s an approach from Ian Keen I liked using an alignment guide:

VStack {
    // Content
}
.overlay(alignment: .bottom) {
   Color.white.frame(height: 50)
      .alignmentGuide(.top) { $0[.bottom] }
}

That’s an abridged version, you’d still need to hook in the offset to show it, but the idea is that to actually place the bottom sheet — you use .alignmentGuide(.top) { $0[.bottom] }. This basically says “align this content’s top origin to the parent’s bottom origin”, which puts the bottom sheet in the right spot to later present.

Smooth Animations

Ben Scheirman had a great example showing how alignment guides can give you the animation you could be after. I’d encourage you to read his post on the matter, though the gist is that by using alignment guides — he can get two rectangles in a ZStack to animate left and right smoothly from the center (they begin one on top of the other):

@State private var isLinked: Bool = false 

ZStack {
     BarView()
         .alignmentGuide(HorizontalAlignment.center) { d in
             d[isLinked ? .center : .trailing]
         }
     BarView()
         .alignmentGuide(HorizontalAlignment.center) { d in
             d[isLinked ? .center : .leading]
         }
 }

The result is that they “split” from the center, evenly. Without using alignment guides for this particular scenario, SwiftUI’s layout system can have some unintended effects on the resulting animation. His post shows this clearly with some pictures, go check it out.

If you want to dig in a bit deeper over how alignment guides work, I’d recommend reading these posts:

  • Paul Hudson has a great, overall explainer (as he always does).
  • SwiftUI Lab has an insane, in-depth post on the matter (and, as he always does).

When I don’t really “get” an API, I find the only way I learn it is by getting to the point where I can answer this question:

When would this API help me? Would I know when to reach for it?

After seeing these examples and then going back to the docs, I feel like I’m getting there with alignment guides.

Until next time ✌️.

···

Pricing Indie Apps: The Perks of a Wallflower Rule

// Written by Jordan Morgan // Jan 26th, 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.

If there’s one thing I’m a sucker for, it’s the “coming of age” indie flick.

Paper Towns? Loved it.
Me, Earl and the Dying Girl? Absorbed me.
The Florida Project? That one wrecked me.

Which brings us to one of my favorites: The Perks of Being a Wallflower. In particular, this quote:

The protagonist, a high school teenager wrestling with young puppy love, expresses his confusion over why the girl he so covets dates someone who is (wait for it)…kind of a jerk?

His english teacher drops some sage wisdom with the now classic line:

We accept the love we think we deserve.

Aside from being an excellent line, I’ve been thinking about how much this correlates to indie apps too. And so here I am wondering, are we pricing our apps according to what we simply think that they are worth? Because if we are, well - then we’re doing something iOS indies do all too much: undervaluing our work.

When I think back about Spend Stack, I fondly remember how it was positioned: It’s a $2.99 app. Though, when we price something, we are also inherently telling a story before anybody ever downloads it: “It’s worth this much.”

I don’t think we give enough weight to that line of thinking.

With Elite Hoops, I wanted to (if anything) overprice it at launch. Even though I consider it in an M.V.P. state still, I boldly said to users that they should pay me $40 a year for it, or $10 a month or $149.99 for a one-off payment.

Now, three months later, I think I underpriced myself. But, when I look at my own app - all I see are the holes. The missing features. The rough edges. The animation that, after hours of tweaking, still just doesn’t feel quite right.

Consumers, by and large, will not have the same eye as you. In a truism that we all say we know but don’t often practice - they just want something to solve their problem. And Elite Hoops does that. And so I charge a good little sum for it.

Is there nuance here? Absolutely. If your app sucks, none of this matters. But if your app is even just okay in your indie eyes, but does something useful - then don’t undercharge for it. Don’t worry about the forum comments, Reddit posts or tweets that say you’re “another subscription” or whatever else. You’ll get that even if you charged $1.00.

But, when you look at the indie scene - do I firmly believe that there are more folks underpricing their apps than overpricing them. Absolutely.

You could rely on the intricate complexities of pricing theories, or you could simply charge a little bit higher than what you think you can get away with - and then let the market truly adjust from there. So when I talk to to other indies, I always encourage them to follow “Perks of Being a Wallflower” pricing: Charge the amount that you truly think you deserve.

Until next time ✌️.

···

Using Keychain Access to Store Sensitive Data

// Written by Jordan Morgan // Jan 14th, 2024 // Read it in about 1 minute // RE: macOS

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

Here’s something I didn’t know about Keychain Access on macOS - secure notes. After a little research, it seems to be a sensible place to store your sensitive data. Just open it up, and choose File -> New Secure Note Item:

A screenshot of Keychain Access.

Recently, I was moving off of 1 Password, where I had previously stored my social security numbers. Previously, I had relegated Keychain Access to simply managing passwords or certificates, I had no idea you could use it for something like this. If you put the file in your iCloud keychain, it’ll also sync to your other devices.

It goes to show you that macOS is always the dog that’s teaching you new tricks. It wasn’t but a few months ago that I learned you could setup 2FA codes within macOS’ password screen:

A screenshot of setting up 2FA in Password Settings.

Which is phenomenal, because you know what isn’t great? Literally every 2FA app - iOS, macOS or otherwise.

Until next time ✌️.

···

Using @Binding with @Environment(Object.self)

// Written by Jordan Morgan // Dec 31st, 2023 // Read it in about 2 minutes // RE: SwiftUI

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

iOS 17 brought us more changes to how we manage state and interface changes in SwiftUI. Just as we were given @StateObject in iOS 14 to plug some holes - the Observation framework basically changes how we manage state altogether. However, if you’re just moving to it as I am, you may get confused on how bindings work.

Traditionally, with the “old” way, we could snag a binding to an observed object like this:

class Post: ObservableObject {	
	@Published var text: String = ""
}

struct MainView: View {
	@StateObject private var post: Post = .init()

	var body: some View {
		VStack {
			// More views
			WritingView()
		}
		.environmentObject(post)
	}
}

struct WritingView: View {
	@EnvironmentObject private var post: Post

	var body: some View {
		TextField("New post...", text: $post.text)
	}
}

However, with Observation, passing around the post works a little differently. If you’re like me, you might’ve thought getting a binding to mutate the text would look like this:

@Observable
class Post {	
	var text: String = ""
}

struct MainView: View {
	@State private var post: Post = .init()

	var body: some View {
		VStack {
			// More views
			WritingView()
		}
		.environment(post)
	}
}

struct WritingView: View {
	@Environment(Post.self) private var post: Post

	var body: some View {
		// ! Compiler Error !
		TextField("New post...", text: $post.text)  // Cannot find '$post' in scope
	}
}

I haven’t watched the session over Observation in some time, so I was puzzled by this. It turns out, you create a binding directly in the body:

struct WritingView: View {
	@Environment(Post.self) private var post: Post

	var body: some View {
		@Bindable var post = post
		TextField("New post...", text: $post.text)
	}
}

This is mentioned directly in the documentation as it turns out, with an identical example as seen here:

Use this same approach when you need a binding to a property of an observable object stored in a view’s environment.

I don’t know why @Bindable was designed like this, I’m sure there is a technical reason, but as an API consumer it seems counterintuitive. Which is odd, considering all of the ease of use the Observation framework brings. Regardless, another solution proposed in a thread over the issue mentions that you could drop the object through another view:

struct WritingView: View {
	@Environment(Post.self) private var post: Post

	var body: some View {
		PostTextView(post: post)
	}
}

struct PostTextView: View {
	@Bindable var post: Post
	
	var body: some View {
		TextField("New post...", text: $post.text)
	}
}

So, if you get stuck with grabbing a binding to an Observable object - you can use either of these approaches.

Until next time ✌️.

···

Wrapping up 2023, and Looking Towards 2024

// Written by Jordan Morgan // Dec 19th, 2023 // 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.

The end of 2023 is here, and that can only mean two things:

  • I’m writing a blog post about how my year went, and
  • A new episode of Launched, featuring Charlie and I’s annual Christmas Special, will land soon!

For now, though, I wanted to chat about the year. I never really know what to write in these kinds of posts, and at the expense of being too self-serving - I figured I’d try to lay out what I did, didn’t do and what’s next. The hope is that you can glean some lessons from all of it.

This year was fantastic to me professionally-speaking, even though I didn’t accomplish even a quarter of what I wanted to. But, all I’m saying is, if ya’ll truly could see the juxtaposition of what I wanted to do, versus what I did?

Well, the chasm would be as wide as the Grand Canyon. And yet, I am quite impressed with what I got off the ground:

I finished the book series 🎉!
Taking a work sabbatical to finish this thing up was key to my year. At the clip I was going, the original table of contents wouldn’t be finished until 2025. That’s…too long. However, being on the other side of it? It feels fantastic to see what I came up with. I’m truly proud of the book series.

To wit, I just referenced it yesterday to get UndoManager to play nice.

And, as promised, the book series is never truly finished. I liken it to my magnum opus, a critical reference guide of all that I have learned. While I’m taking some breathing room from it for now (since it’s been a marathon for the last two and a half years), there’s plenty more to come:

A screenshot of Craft docs showing updates to come for the book series.

Each year during W.W.D.C., I plan to update the series over the summer. And, thankfully, it continues to sell well.

I shipped Elite Hoops 🎉!
Elite Hoops continues to grow each day! Getting another app out, and officially rejoining the ranks of indie developer, was cathartic. I love making apps and trying to grow them. My goal was to have 100 paying users before the year is up, and today I am at 327. So, it feels like I might have something.

2024 will have a lot of work on my end to grow things even more, and we’ll see how it goes.

What I Didn’t Do 😅!
Well, a lot, actually. Remember this little app I made?

A screenshot of a goal tracking app with blocks of different habits to track and goals.

Or the exquisite Freeform boards I made of my goals?

A screenshot of Freeform by Apple with yearly goals and resolutions in place.

…all nicely split out into timelines?

A screenshot of Freeform by Apple with yearly goals and resolutions in place.

Yeah, all of that came off the rails by February. So, for 2024, I’m trying to simplify and be much more realistic. Here’s what I came up with for 2024:

A screenshot of Day One listing off my yearly goals.

I kind of thought about it in a similar fashion to my goal framework of last year (daily, weekly, monthly, and yearly goals) but there were ways where that felt too rigid. So, this time - I’m simply trying to build some habits, do some useful things each month for Elite Hoops, and over the year - ship my next app, do Elite Hoop’s roadmap and maybe start on another app I’m excited about.

Will it work? Who knows, but here’s hoping!

From me to you, I wish you and your family and friends an amazing holiday, New Year and 2024 🥂!

Until next time ✌️.

···