Creating icons in Xcode playgrounds

I'm no good at drawing. I have Affinity Designer and I like it well enough, but it requires more expertise than I have, really. Usually when I want to draw things, I prefer to retreat back to code.

Xcode playgrounds are pretty OK for writing your graphics code. Select your drawing technology of choice to create an image, create a view that displays it, make it the live view with PlaygroundPage.current.setLiveView and you're done. Well, almost. How do you get the image out of there?

Say you're creating icons for an iOS project. You want a bunch of variously sized versions of the same icon (I'm assuming here you aren't finessing the different versions too much, or otherwise you wouldn't be reading a tutorial on how to generate images in code), and you want to get them into an asset catalog in Xcode. Xcode's asset catalog editor can accept dragged files, so that seems like a something we could try enable.

SwiftUI makes it really easy.

Start with a function that draws the icon into a CGImage. This one just draws a purplish rectangle. It won't win any ADAs, but it'll serve for this tutorial:

func makeImage(size: CGSize) -> CGImage {
    let ctx = CGContext(
        data: nil,
        width: Int(size.width),
        height: Int(size.height),
        bitsPerComponent: 8,
        bytesPerRow: 0,
        space: CGColorSpaceCreateDeviceRGB(),
        bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
    )!
    let rect = CGRect(origin: .zero, size: size)
    ctx.setFillColor(red: 0.9, green: 0.4, blue: 0.6, alpha: 1.0)
    ctx.fill(rect)
    let image = ctx.makeImage()!
    return image
}

Next define a bunch of values for the icon sizes that Xcode likes. As of Xcode 13 and iOS 15, something like this is a good representation of what you need:

enum IconSize: CGFloat {
    case phoneNotification = 20.0
    case phoneSettings = 29.0
    case phoneSpotlight = 40.0
    case phoneApp = 60.0
    case padApp = 76.0
    case padProApp = 83.5
}

extension IconSize: CustomStringConvertible {
    var description: String {
        switch self {
        case .phoneNotification: return "iPhone/iPad Notification (\(self.rawValue))"
        case .phoneSettings: return "iPhone/iPad Settings (\(self.rawValue))"
        case .phoneSpotlight: return "iPhone/iPad Spotlight (\(self.rawValue))"
        case .phoneApp: return "iPhone App (\(self.rawValue))"
        case .padApp: return "iPad App (\(self.rawValue))"
        case .padProApp: return "iPad Pro App (\(self.rawValue))"
        }
    }
}

Then define a struct that holds one extra bit of information: the scale we're working at.

struct IconVariant {
    let size: IconSize
    let scale: CGFloat

    var scaledSize: CGSize {
        let scaled = self.scale * self.size.rawValue
        return CGSize(width: scaled, height: scaled)
    }
}

extension IconVariant: CustomStringConvertible {
    var description: String { "\(self.size) @ \(self.scale)x" }
}

extension IconVariant: Identifiable {
    var id: String { self.description }
}

The descriptions are useful for you, the human; the Identifiable conformance will be helpful when you set up a SwiftUI view showing the variants.

Next define all the variants you want:

let icons: [IconVariant] = [
    IconVariant(size: .phoneNotification, scale: 2),
    IconVariant(size: .phoneNotification, scale: 3),
    IconVariant(size: .phoneSettings, scale: 2),
    IconVariant(size: .phoneSettings, scale: 3),
    IconVariant(size: .phoneSpotlight, scale: 2),
    IconVariant(size: .phoneSpotlight, scale: 3),
    IconVariant(size: .phoneApp, scale: 2),
    IconVariant(size: .phoneApp, scale: 3),
    IconVariant(size: .phoneNotification, scale: 1),
    IconVariant(size: .phoneSettings, scale: 1),
    IconVariant(size: .phoneSpotlight, scale: 1),
    IconVariant(size: .padApp, scale: 1),
    IconVariant(size: .padApp, scale: 2),
    IconVariant(size: .padProApp, scale: 2),
]

Then let's start work on getting those variants on screen. We'll use a simple SwiftUI view with stacks for it; it won't be pretty, but it'll do what's needed.

struct IconView: View {
    var body: some View {
        VStack {
            ForEach(icons) { icon in
                HStack {
                    let cgImage = makeImage(size: icon.scaledSize)
                    Text(String(describing: icon))
                    Image(cgImage, scale: 1.0, label: Text(String(describing: icon)))
                }
            }
        }
    }
}

PlaygroundPage.current.setLiveView(IconView())

As promised, functionality over form:

Screenshot of labeled squares

Now we need just the glue to enable dragging. Add a CGImage extension that makes it easier to export the image as PNG data:

extension CGImage {
    var png: Data? {
        guard let mutableData = CFDataCreateMutable(nil, 0),
              let destination = CGImageDestinationCreateWithData(mutableData, "public.png" as CFString, 1, nil)
        else { return nil }
        CGImageDestinationAddImage(destination, self, nil)
        guard CGImageDestinationFinalize(destination) else { return nil }
        return mutableData as Data
    }
}

To make the images in the view draggable, you'll need to use the onDrag view modifier. It requires a function that returns a NSItemProvider. The nicest way to create one is probably with a custom class that conforms to NSItemProviderWriting. Something like this:

final class IconProvider: NSObject, NSItemProviderWriting {
    struct UnrecognizedTypeIdentifierError: Error {
        let identifier: String
    }

    let image: CGImage

    init(image: CGImage) {
        self.image = image
    }

    func loadData(
        withTypeIdentifier typeIdentifier: String,
        forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void
    ) -> Progress? {
        guard typeIdentifier == "public.png" else {
            completionHandler(nil, UnrecognizedTypeIdentifierError(identifier: typeIdentifier))
            return nil
        }
        completionHandler(self.image.png, nil)
        // Progress: all done in one step.
        let progress = Progress(parent: nil)
        progress.totalUnitCount = 1
        progress.completedUnitCount = 1
        return progress
    }

    static var writableTypeIdentifiersForItemProvider: [String] {
        ["public.png"]
    }
}

And then the last thing needed is the onDrag handler. Add it to the Image line in the IconView you created earlier.

Image(cgImage, scale: 1.0, label: Text(String(describing: icon)))
    .onDrag {
        NSItemProvider(object: IconProvider(image: cgImage))
    }

Refresh the playground preview and the images are waiting for you to drag them into the asset catalog.

Date component ranges in Swift

Ever needed to iterate over a list of days or months in Swift? Ever needed to have a random-access collection of those?

The first thing they teach you in the How to not Operate on Dates Horribly Wrong class (aka Calendrical Fallacies) is to forget about using seconds for calendar correct calculations. On the Apple platforms you should be using Foundation's Calendar type instead. You will still write bugs, but hopefully they'll be more interesting than calendar drift caused by leap seconds and days.

To get a list of months you can start with an integer range and do fancy operations with it. Define a few example values to get started:

let now = Date()
let valueRange = 1 ... 120
let calendar = Calendar(identifier: .gregorian)

Making a list of dates corresponding to that valueRange is easy enough:

let months = valueRange.map {
    calendar.date(byAdding: .month, value: $0, to: now)!
}

The force unwrap operator, !, is uncomfortable there, but it's caused by Calendar's somewhat dodgy API design: because Calendar.Component contains cases such as calendar and timeZone, the calculation function has to return an optional. If you're hard coding .month, I think using a force unwrap there is quite OK.

Anyway, after that, months is a nice array of dates and we're ready to go home, right?

Well, sure. If the size of the list really is 120, that's pretty much it. But it wouldn't be much a blog post then. If you start dealing with larger ranges, the list will start to get a little cumbersome. The valueRange we started with it is an example of where we'd like to end up at: it's just two integers wrapped in a ClosedRange. If you change it to 1 ... 1200, it doesn't get any larger in memory. But moments in time are represented with Dates, which are really just floating point values representing the number of seconds since a point in history, and if you created a range like now ... now.addingTimeInterval(100000) you'll just get timestamps separated by seconds, it won't conform to RandomAccessCollection because Date doesn't conform to Strideable and you'd run into problems with calendar math if you tried to build something fancier based on that.

So we need something else.

Swift has some helpers for creating sequences, such as, well, the sequence function, but nothing that builds on ranges, as far as I can tell. But building our own type that wraps a range and adds calendar logic on top of it isn't a big task, once you figure out the necessary protocols. That's what we really want, in the end: logically a lazy map over an integer range, one that doesn't create a list but just maps one integer to a date, while getting the calendar math right. The map function is out because it builds an array.

We can get started by defining a type:

struct CalendarRange {
    let calendar: Calendar
    let component: Calendar.Component
    let epoch: Date
    let values: ClosedRange<Int>
}

We use epoch as the zero point. It's extremely arguable if it is necessary as a property here; you could just hard code something like Date(timeIntervalSinceReferenceDate: 0) and that would most probably be just fine. Some people would leave Calendar out too, just relying on .autoupdatingCurrent, but that's a an easy way to write untestable code. Adding convenience initializers is permitted.

Now we just need to add the necessary conformances to build up to RandomAccessCollection. The conformance tower goes like this, starting from the top: RandomAccessCollectionBidirectionalCollectionCollectionSequence. We can skip over implementing Sequence because Collection gives it us for free:

extension CalendarRange: Collection {
    typealias Index = ClosedRange<Int>.Index

    var startIndex: Index { self.values.startIndex }
    var endIndex: Index { self.values.endIndex }

    func index(after i: Index) -> Index { self.values.index(after: i) }

    subscript(index: Index) -> Date {
        self.calendar.date(
            byAdding: self.component,
            value: self.values[index],
            to: self.epoch
        )!
    }
}

The idea is simple: delegate everything else to the range but execute the calculation in subscript. There's again the awkward bang, but there's going to be some variation of it regardless of how you package this up, as long as you're using Calendar which is the right thing to do. You can dress it up in a preconditionFailure if you prefer.

Next: add BidirectionalCollection.

extension CalendarRange: BidirectionalCollection {
    func index(before i: Index) -> Index { self.values.index(before: i) }
}

That wasn't extremely complicated. Let's see about RandomAccessCollection next.

extension CalendarRange: RandomAccessCollection {}

Nice. RandomAccessCollection doesn't require any extra members, it just places restrictions on what kind of index you can use. The one we inherited from ClosedRange suits it fine.

Now we can test it. Make a debug function and call it with some values:

func show<T>(_ values: T) where T: RandomAccessCollection {
    print("----- values count: \(values.count)")
    print(values.startIndex, String(describing: values.first))
    print(values.endIndex, String(describing: values.last))
}

show(CalendarRange(calendar: gregorian, component: .month, epoch: Date(), values: -12_000 ... 12_000))
show(CalendarRange(calendar: gregorian, component: .day, epoch: Date(), values: -12_000 ... 12_000))

And hey, it works! It's not exactly fast, though. Running that takes noticeably long. To figure out what's happening with code this simple you don't even need to break out Instruments. Just throw a couple of debug prints in there and you'll discover that count will call index(after:) for each index. It'll also probably put Xcode in a state where you have force quit it, but such is the price of science.

What we're observing here is the magic of Swift's default protocol requirement implementations: you get count for free, but it's the lower common denominator. Sometimes that's the best you can do, and sometimes you can help make it suck less. In this case, improving count is trivial. ClosedRange has a perfectly good implementation, and we just need to, once again, delegate to it:

extension CalendarRange {
    var count: Int { self.values.count }
}

Now it's blazing fast. It's possible there are other methods it would be beneficial to specialize; I haven't looked that deep into it.

One more thing we could do, if we find ourselves using this kind of thing a lot, is return to that statement earlier about what we need: a range with a lazy mapping function. If we think about it a bit more, in the general case, it wouldn't even have to be a range. In some cases it might be useful to do this with a non-closed Range or an Array or some other collection. And as I said, what we're doing in this code is just delegating everything but the subscript implementation to the wrapped range.

A generalized version of this is not much work:

struct MappedRandomAccess<Wrapped, Output>: RandomAccessCollection
where Wrapped: RandomAccessCollection {
    typealias Index = Wrapped.Index
    let values: Wrapped
    let f: (Wrapped.Element) -> Output

    var startIndex: Index { self.values.startIndex }
    var endIndex: Index { self.values.endIndex }
    func index(after i: Index) -> Index { self.values.index(after: i) }
    func index(before i: Index) -> Index { self.values.index(before: i) }
    var count: Int { self.values.count }
    subscript(index: Index) -> Output { self.f(self.values[index]) }
}

This version requires the conversion to be supplied from outside, but that's easy enough too:

let epoch = Date(timeIntervalSinceReferenceDate: 0)

show(MappedRandomAccess(values: -12_000 ... 12_000) {
    calendar.date(
        byAdding: .month,
        value: $0,
        to: epoch
    )!
})

The type of that value is MappedRandomAccess<ClosedRange<Int>, Date>, but you could make that a bit easier to deal with using a typealias.

It's very possible this last generalization step isn't worth it in your code base, even if you need the date stepping functionality; using a closure does have a cost and the type is decidedly pointier than the non-general version, even if you manage to hide some of the time with a typealias. But there is a neatness to it.

Whichever version you decide to go with, you now have a memory efficient method of storing a large number of dates separated by a constant calendar unit. It can come useful sometimes.

Async Swift and ArgumentParser

Swift 5.5 brought us async functions. ArgumentParser is the most popular way to write command line interfaces with Swift. Swift 5.5 supports an asynchronous main function, but ArgumentParser does not, as of version 1.0.2.

To bridge this gap, you can call ArgumentParser manually from your asynchronous main function, like this:

import ArgumentParser

struct MyCommand: ParsableCommand {
    @Argument var arg: String

    // Usually you'd write a `run` function, but it has to be synchronous.
    func runAsync() async throws {
        /* … */
    }
}

@main
enum Main {
    static func main() async throws {
        let args = Array(CommandLine.arguments.dropFirst())
        do {
            let command = try MyCommand.parse(args)
            try await command.runAsync()
        } catch {
            MyCommand.exit(withError: error)
        }
    }
}

© Juri Pakaste 2024