[SC]()

iOS. Apple. Indies. Plus Things.

Creating Light and Dark Mode Icons using Icon Composer

// Written by Jordan Morgan // Sep 7th, 2025 // Read it in about 3 minutes // RE: Icon Composer

This post is brought to you by Sentry, mobile monitoring considered "not bad" by 4 million developers.

Ah, new toys.

When Icon Composer was first announced, I thought, “Hey, here’s something that might help with my biggest blind spot in iOS development, making icons look somewhat okay.” Anything that helps the solo indie developer make an icon easier is fantastic news to me.

The biggest hang up? For the life of me, this thing is simplistic yet confusing at times. Making a unique icon for both light and dark mode was important for my upcoming app, Alyx. I finally figured out how, because if I can’t have Alyx with a sleep mask on when it’s dark mode — really, what do I even have anymore?

Alyx espresso shot glass in dark mode.

Here’s how it’s done:

1. Add all of the groups you want to use in totality. So, all of your layers (and their respective groups) for light and dark mode:

Adding groups in Icon Composer.

2. Now, select the individual layers that should be hidden in light mode — set their opacity to 0%:

Adding groups in Icon Composer.

3. Then, select the dark mode icon at the bottom of Icon Composer:

Selecting the dark mode icon variant.

4. Finally, choose the individual layers that should be hidden in dark mode — set their opacity to 0%:

Setting opacity to 0 percent in dark mode in Icon Composer.

And that should do it. What really tripped me up was that I started by choosing a dark mode icon, and I thought, “Okay, it clearly says "Dark" here, so I’ll hide the light layers now.” But, going the other way was confusing to me — because you don’t select a “Light” toggle at that point. Instead, it’ll say “Default” with no option to choose or add anything else. What this is trying to say is “The changes here apply to light mode.”

The more you know.

Until next time ✌️

···

Open Intent in iOS 26

// Written by Jordan Morgan // Aug 19th, 2025 // Read it in about 1 minutes // RE: App Intents

This post is brought to you by Sentry, mobile monitoring considered "not bad" by 4 million developers.

While working on Alyx and its (over 40!) Siri Shortcut actions, I came upon the need for an AppEntity to launch the app. In the past, I had similar intents for Elite Hoops:

struct OpenTeamIntent: AppIntent {
    static var isDiscoverable: Bool = false
    static var title: LocalizedStringResource = "Open Team"
    @Parameter(title: "Team")
    var team: TeamEntity
    
    @Dependency
    var deepLinker: DeepLinker
    
    init(team: TeamEntity) {
        self.team = team
    }
    
    init() {}
    
    @MainActor
    func perform() async throws -> some IntentResult {
        deepLinker.selectedCourtID = team.id
        return .result()
    }
}

Turns out, doing it this way is🥁….

Wrong!

There’s an intent type just for this type of thing, called OpenIntent. So, I adopted that correctly:

struct OpenTeamIntent: OpenIntent {
    static var isDiscoverable: Bool = false
    static var title: LocalizedStringResource = "Open Team"
    @Parameter(title: "Team")
    var target: TeamEntity
    
    @Dependency
    var deepLinker: DeepLinker
    
    init(target: TeamEntity) {
        self.target = target
    }
    
    init() {}
    
    @MainActor
    func perform() async throws -> some IntentResult {
        deepLinker.selectedCourtID = target.id
        return .result()
    }
}

But wait! It’s actually even easier than that! I don’t even need my own navigation class to do this anymore. Enter iOS 26 variant:

import AppIntents
import CaffeineKit

struct OpenDrinkIntent: OpenIntent {
    static let title: LocalizedStringResource = "Log Matched Drink"
    
    @Parameter(title: "Drink", requestValueDialog: "Which drink?")
    var target: CaffeineDrinkEntity
    
    func perform() async throws -> some IntentResult {
        return .result()
    }
}

#if os(iOS)
extension OpenDrinkIntent: TargetContentProvidingIntent {}
#endif

That’s it! Then, in your app in some view, there’s a modifier to handle it:

SomeView()
.onAppIntentExecution(OpenDrinkIntent.self) { intent in
    AlyxLogs.general.info("Opening drink intent: \(intent.target.name)")
    openTheThing()
}

To recap:

  1. Adopt OpenIntent
  2. Provide a target (yes, it MUST be named exactly that) of one of your AppEntity types.
  3. Add the TargetContentProvidingIntent conformance to it.
  4. And use the onAppIntentExecution modifier.

That’s an API at play I used with Visual Lookup support, by the way:

Until next time ✌️

···

The Business and the Boutique

// Written by Jordan Morgan // Jul 27th, 2025 // Read it in about 2 minutes // RE: The Indie Dev Diaries

This post is brought to you by Sentry, mobile monitoring considered "not bad" by 4 million developers.

Some apps are made to be big boy business ventures. Some are for the love of the game, man. I’ve been building a business, but yet…the itch remains.

If you’ve been around here a while, you’ll remember Spend Stack, a (if I may) delightful little personal finance app. Or, was it a budget tracker? Or a way to track a running total of stuff? I dunno, it was sort of all those things (which became an issue).

But, for all of its faults, there was one thing it absolutely was: An iOS-first playground for me. Drag and drop, multiple windows — all things that were new and novel at the time — were implemented over any actual product feature. I absolutely loved working on it. I’ll never forget its App Store features, being on demo units, the press coverage, App of the Day!

While Elite Hoops is doing great and it remains my primary focus, it’ll never really be that fun little indie darling. I want one of those, too. I want my cake. And I want to eat all of it.

But over the years, I’ve learned a bit more. If I ever made another “love of the game” app, two things would have to be true:

  • I would need a stable, business-could-grow project. I have that now with Elite Hoops.
  • And, if I made another love of the game app, it would need a clear audience and “thing it does” defined first.

Introducing Alyx

And so that brings me to Alyx!

Alyx espresso shot glass.

It has a clear mission — to track caffeine and see how it affects your body and sleep.

And, it's kinda fun!

So, what does that mean?

  • I’ll try to over-polish freaking everything.
  • I’ll go 200% in on Apple APIs like Shortcuts, widgets, etc.
  • Will I spend each summer integrating Apple’s new toys over anything else? You bet!
  • Dare I include an App Shortcut as part of onboarding, and jerryrig a way to detect when it was ran, then update the onboarding UI using TextRenderer APIs to make a video game-ish text bubble congratulating them? No-brainer!

It’s funny, I’ve actually used Alyx for two years now. I figured now was a good time to ship it out, and have it be my muse. Another “Spend Stack” if you will, but refined a bit from all rough edges I hit releasing, managing and creating it.

I’m trying my hardest to hit iOS 26 with this one, that’s always been a huge goal of mine. Ship a new app with a new version of iOS! I’m looking good so far, and because it’s my playground, the entire app is built around App Intents. Literally anything you can do in-app, you can do via an intent (even small stuff, like setting the app’s theme).

Elite Hoops is here to grow and make money. Alyx hopefully will do the same, but it’ll be my pretty little playground. Now I have a business, and my boutique shop.

Until next time ✌️

···

Confirmation and Result Interactive Snippets

// Written by Jordan Morgan // Jul 10th, 2025 // Read it in about 4 minutes // RE: App Intents

This post is brought to you by Sentry, mobile monitoring considered "not bad" by 4 million developers.

Recently, on the Superwall blog, I wrote up a guide over interactive snippets. If you haven’t checked that out, I’d go there first. Continuing on that same beat, I wanted to also highlight an important difference in interactive snippets.

There are two types you can provide:

  1. Result snippets: These show data that you don’t have to confirm (i.e. here’s how much caffeine you’ve had today).
  2. Confirmation snippets: These show data, but also require you to make some sort of decision before continuing (i.e here’s some coffee I want to order).

Both of them are interactive, and can have buttons or toggles that fire intents. At the API level, they both have the same signatures. However, confirmation snippets request said confirmation inside of the perform function. From Apple’s sample code:

struct FindTicketsIntent: AppIntent {

    func perform() async throws -> some IntentResult & ShowsSnippetIntent {
        let searchRequest = await searchEngine.createRequest(landmarkEntity: landmark)

        // Kicks off a confirmation snippet here
        try await requestConfirmation(
            actionName: .search,
            snippetIntent: TicketRequestSnippetIntent(searchRequest: searchRequest)
        )

        // Resume searching...
    }
}

And that is where things diverge. To understand that divergence better, consider that interactive snippets are typically dismissed by the user tapping the system provided “Done” button found below them:

Result type of an interactive snippet.

All “result” interactive snippets work this way. Alternatively, they can also just swipe it away to yeet it out of there.

But when you need confirmation, the “Done” button isn’t displayed, and instead the system will show contextualized buttons based on the actionName parameter. In the code above, that was set to “.search”, and so it is — you get a “Cancel” and “Search” button:

Confirmation type of an interactive snippet.

And then the intent carries on, here’s the full sample, I’ve edited the comment to demonstrate the flow better:

struct FindTicketsIntent: AppIntent {

    static let title: LocalizedStringResource = "Find Tickets"

    static var parameterSummary: some ParameterSummary {
        Summary("Find best ticket prices for \(\.$landmark)")
    }
    
    @Dependency var searchEngine: SearchEngine

    @Parameter var landmark: LandmarkEntity

    func perform() async throws -> some IntentResult & ShowsSnippetIntent {
        let searchRequest = await searchEngine.createRequest(landmarkEntity: landmark)

        // The new UI shows here, so you can think of `TicketRequestSnippetIntent`
        // As its own confirmation snippet. Once the user is done and they tap search,
        // We'll come back right here, and the search engine code will fire
        try await requestConfirmation(
            actionName: .search,
            snippetIntent: TicketRequestSnippetIntent(searchRequest: searchRequest)
        )

        // This happens after the confirmation of the number of tickets is done
        try await searchEngine.performRequest(request: searchRequest)

        // And finally, a new snippet is shown
        return .result(
            snippetIntent: TicketResultSnippetIntent(
                searchRequest: searchRequest
            )
        )
    }
}

What can be tricky here is that this flow shown today technically uses three different interactive snippets:

1. First, there’s the ClosestLandmarkIntent, which returns that landmark view:

Result type of an interactive snippet.

2. Then, tapping “Find Best Ticket Prices” takes us to the above code, where TicketRequestSnippetIntent asks for confirmation on the ticket count:

Result type of an interactive snippet.

3. And, it circles back to TicketResultSnippetIntent to show the results. If you tap “Book Now”, we go back to first step:

Result type of an interactive snippet.

I hope that helps clear up when you’d use a “result” interactive snippet versus a confirmation one. Just because Apple refers to result interactive snippets as a result of something, they are very much well-suited to powerful tasks complete with interactivity.

Now go forth, and make all quick, impactful interactions a snippet!

Until next time ✌️

···

iOS 26: Notable UIKit Additions

// Written by Jordan Morgan // Jun 10th, 2025 // Read it in about 4 minutes // RE: UIKit

This post is brought to you by Sentry, mobile monitoring considered "not bad" by 4 million developers.

2019 was the year Apple popped the lid off of SwiftUI. At the time, the responses came quick and fast and they were, well, a bit sensational. “UIKit is dead”, “It’s deprecated!”, “Useless now!”…the list goes on. But, yet again, here we are at dub dub 25’ and more UIKit improvements have landed.

Had initial reactions to SwiftUI come to fruition, you’d think UIKit would have no part in the Liquid Glassification of Apple’s ecosystem. But, in reality — of course it does. UIKit is still the framework propping up iOS, and if you peek under the hood far enough…it’s doing the same for SwiftUI, too.

As it tradition, let’s see what Cupertino & Friends™️ have for us this year in our iron-clad UI framework.

If you want to catch up on this series first, you can view the iOS 11, iOS 12, iOS 13, iOS 14, iOS 15, iOS 16, iOS 17, and iOS 18 versions of this article.

Observable Objects

Let’s start our notable UIKit additions post by…talking about SwiftUI. As one does. Observable made SwiftUI simply better, and now its UIKit’s turn to reap the same benefits. UIKit and Observable classes now work hand-in-hand. This is interesting, as the common line of thinking was that as SwiftUI evolved, the gap between it and UIKit would widen.

Instead, it’s shrinking.

For example, look at the animation interop changes from last year’s W.W.D.C., which allow UIKit and SwiftUI to coordinate animations.

With Observable, UIKit now tracks any changes in update methods like layoutSubviews. The implication? You don’t need to manually invalidate views and do the bookkeeping to update them:

import UIKit
import Observation

@Observable
class HitCounter {
    var hits: Int = 0
}

class TestVC: UIViewController {
    private var counter: HitCounter = .init()
    private let hitLabel: UILabel = .init(frame: .zero)

    override func viewDidLoad() {
        super.viewDidLoad()
        hitLabel.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(hitLabel)
        NSLayoutConstraint.activate([
            hitLabel.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
            hitLabel.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)
        ])
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            self.counter.hits = 10
        }
    }
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        
        // Dependency wired up
        self.hitLabel.text = "Hits: \(self.counter.hits)"
    }
}

#Preview {
    TestVC(nibName: nil, bundle: nil)
}

let concatenatedThoughts = """

Speaking of gaps shrinking, just look at all of the SwiftUI-ness in that tiny body of work. Observable tracking. Automatic view updates. Previewing canvas macros. You love to see it.

"""

Here, UIKit automatically sets up observation tracking for the hits property. When it changes, the view gets invalidated, viewWillLayoutSubviews runs again — and just like that, the label is updated. Again, the handshakes between SwiftUI and UIKit seem to grow firmer each year, and that’s only a good thing for the ecosystem.

Also, while we’re here — what’s always been the knock on SwiftUI? It’s that large lists just don’t…list well. While changes have been made under the hood to make that situation better, the conventional wisdom from those of us who’ve been around was something like “Just wrap UICollectionView!”

Now, maybe that doesn’t ring as true with the new long list optimizations? Either way, it is easier to do now. The same technique above extends to UIListContentConfiguration, which means cells in collection views can automatically be updated from Observable models, too:

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "customcell", for: indexPath)
    let myModel = store.model(at: indexPath)
    cell.configurationUpdateHandler = { cell, state in
        var content = UIListContentConfiguration.cell()
        content.text = myModel.name
        cell.contentConfiguration = content
    }

    return cell
}

Even better? You can back-deploy this to iOS 18 by adding the UIObservationTrackingEnabled key to your info.plist file.

Update properties

A new lifecycle method on UIView and UIViewController!? What year is this?

I love it. Meet updateProperties(), which gets called right before layoutSubviews(). Like many life cycle functions, you can manually invoke it — setNeedsUpdateProperties(). So, what does it do and when do you use it?

In short, you can apply styling, or hydrate view-related details from models and other related tasks without forcing a full layout pass. You have one tiny thing to update? You don’t necessarily need to update the whole view hierarchy, but that’s just kinda how things have worked up until now. The key takeaway is that this runs independently from other layout methods (i.e. layoutSubviews()).

At a top level, the top-down layout pass goes like this now:

  1. Update traits
  2. Update properties.
  3. Layout subviews.

Apple wants to compartmentalize how you think about view layout now, meaning the job of updating view properties from your app’s data can happen in its own place (updateProperties()) and the job of your layout logic can happen in another (layoutSubviews()):

// Before, these two jobs were typically handled in the same place.
// Now, they can occur independently
class MyVC: UIViewController {
    override func updateProperties() {
        super.updateProperties()

        myLabel.text = myModel.text
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        myLabel.setFrameDimensions()
    }
}

Flushing animation updates

This change rocks. Keeping with the layout bookkeeping, traditionally UIKitters had to change things in an animation block, and then tell UIKit to invalidate the view to lay it out again:

// Before
UIView.animate {
    myModel.confirmationColor = validation.isValid ? .green : .red
    confirmationView.layoutIfNeeded()
}

You’ve got two jobs here — updating state, and telling your view to reflect it. An indecerous workflow in 2025. So, if you’re rolling with observable models, now you can skip the whole “Hey I updated a property, please redraw stuff in this animation block” dance:

UIView.animate(options: .flushUpdates) {
    myModel.confirmationColor = validation.isValid ? .green : .red
}

Due to the automatic change tracking UIKit has, UIKit will just take care of it for you now. We didn’t even have to reference confirmationView, UIKit knows that it depends on the confirmationColor of myModel.

This even works outside of Observable objects. You can even tweak constraint animations using the same .flushUpdates option. If you’ve been HasHTaG BleSSeD in your career to manually hold references to NSLayoutConstraint in the name of simply nudging a label down by 20 points, well…I guess you’re even more blessed now.

Bonus Points

  • Lots of new API around supporting the UIMenu changes in iPadOS.
  • Inspector API tweaks, specifically in regards to using them in a split view controller.
  • Key command now lets you support whether or not the command runs repeatedly or not, keyCommand.repeatBehavior = .nonRepeatable.
  • You can host SwiftUI scenes in UIKit apps via the UIHostingSceneDelegate protocol. Perfect for gradually adopting SwiftUI, or using the immersive spaces only found in the framework.
  • HDR extends to colors instead of only media, meaning you can crank things up when creating UIColor instances, and preferring their HDR selection in the color picker.
  • Symbols have .drawOff and .drawOn animations. It’s a nice touch, and it reminds of the magic replace stuff UIKit added last year.

Also, time marches forward. As such, some good ol’ friends of yore got slapped with a deprecation. UIApplication APIs are deprecated in favor of their UIScene counterparts, for example. To be honest, I thought that had already been done. So, if you haven’t embraced the UIScene world, well…you probably should.

What can I say? Another year, another UIKit. Like previous years, it’s much better. It continues to set the standard for what an imperative UI framework should be like, but at the same time — it’s slowly started to borrow ideas from its declarative counterpart. Interesting times! In iOS 26, UIKit is better than ever.

Until next time ✌️

···