[SC]()

iOS. Apple. Indies. Plus Things.

Generating Random Numbers Elegantly in Swift

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

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

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 ✌️.

···

Spot an issue, anything to add?

Reach Out.