SwiftRandomKit is a powerful Swift library that provides a composable, protocol-based approach to random value generation. It offers a flexible and type-safe way to create and combine random generators for various data types.
- 🎲 Type-safe random generators
- 🧩 Composable and chainable API
- 🔄 Support for custom random number generators
- 📦 Rich set of built-in generators
- 🛠 Extensive collection of combinators
- 🧬 Opt-in
@RandomGenerablemacro that derives generators for your own types (via theDerivepackage trait)
SwiftRandomKit works with Swift's isolation model instead of against it. Generators are ordinary values, confined to the isolation domain where you build and run them:
- The
RandomGeneratorprotocol does not requireSendable. Your own generators can hold non-Sendable state and produce non-Sendable values, and transform closures likemap { }are not@Sendable, so they can capture anything the surrounding context can. - Simple leaf generators (
IntGenerator,FloatGenerator,BoolGenerator,Always, and the ready-made generators likeDiceorUUIDGenerator) conform toSendableand can be shared across isolation domains freely. - Composed pipelines (anything built with
map,flatMap,filter, ...) are notSendable. Moving one into aTaskworks when its region is disconnected — Swift's region isolation checks the transfer. To use a pipeline from several tasks, build one per task: generators are cheap, immutable descriptions.
Add the following to your Package.swift file (Swift 6.1+ toolchain required):
dependencies: [
.package(url: "https://github.com/ibrahimkteish/SwiftRandomKit.git", from: "2.2.0")
]To also enable the @RandomGenerable derivation macro, opt into the Derive trait
(see Deriving Generators):
dependencies: [
.package(url: "https://github.com/ibrahimkteish/SwiftRandomKit.git", from: "2.2.0", traits: ["Derive"])
]import SwiftRandomKit
// Create a simple random number generator
let diceGen = IntGenerator(in: 1...6)
let roll = diceGen.run() // e.g., 4
// Generate random characters
let letterGen = RandomGenerators.letter
let letter = letterGen.run() // Random letter (a-z, A-Z)
let digitGen = RandomGenerators.number
let digit = digitGen.run() // Random digit (0-9)
// Generate arrays of random values
let fiveRolls = diceGen.array(5).run() // e.g., [3, 1, 6, 2, 5]
// Use a custom random number generator
var myRNG = MyCustomRandomNumberGenerator()
let customRoll = diceGen.run(using: &myRNG)SwiftRandomKit supports Swift's function call syntax, allowing you to call generators directly as functions:
import SwiftRandomKit
// Create a random number generator
let diceGen = IntGenerator(in: 1...6)
// Traditional way
let roll1 = diceGen.run()
// Function call syntax - more concise!
let roll2 = diceGen()
// Works with custom RNGs too
var myRNG = MyCustomRandomNumberGenerator()
let roll3 = diceGen(using: &myRNG)This syntax provides a more concise and natural way to generate random values, making your code cleaner and more expressive.
SwiftRandomKit can derive a generator for your own structs and enums from a single attribute — ideal for test fixtures, SwiftUI preview data, and seeded demo content:
import SwiftRandomKit
@RandomGenerable
struct User {
@Gen(IntGenerator(in: 18...99)) var age: Int
var name: String // String default: 8 alphanumeric characters
var isAdmin: Bool
var nickname: String? // Optional default: nil half the time
}
let user = User.generator.run() // User(age: 42, name: "k0bSoKdI", ...)
let team = User.generator.array(5).run() // five random users
// Derived generators are ordinary generators: combinators and seeded RNGs work.
var rng = LCRNG(seed: 42)
let admin = User.generator.filter(\.isAdmin).run(using: &rng)Enums derive too, including associated values — a case is picked uniformly at random:
@RandomGenerable
enum Reward {
case coins(Int)
case badge(name: String)
case nothing
}How derivation works:
- Structs are built through their memberwise initializer. Every stored property is
generated by its
@Gen(...)override if it has one, otherwise by its type's default generator (see below). Constants with a value (let version = 1),static,lazy, and computed properties are skipped. - Enums pick a case uniformly at random and generate associated values the same way as struct properties.
- Classes, actors, and generic types are not supported.
The macro needs swift-syntax to compile, which is a heavy build-time dependency — so it
is opt-in behind the Derive package trait. With the trait disabled (the default),
swift-syntax is never built and SwiftRandomKit stays a zero-build-cost dependency.
- Package.swift consumers (Swift 6.1+): declare the dependency with
traits: ["Derive"]as shown in Installation. - Xcode app projects: Xcode 26.4+ supports enabling package traits on a dependency. On older Xcode versions, wrap SwiftRandomKit in a local package that enables the trait and re-exports it.
The RandomGenerable protocol — available with or without the trait — is what powers
derivation: a type conforms by exposing a canonical static var generator. The
standard types come pre-conformed with deliberately simple defaults:
| Type | Default |
|---|---|
Int, UInt8, ... (all fixed-width integers) |
uniform over the type's full range |
Double, Float |
uniform over 0...1 |
Bool |
fair coin flip |
String |
8 random alphanumeric characters |
Character |
random ASCII letter |
Optional<T> |
T's default, nil half the time |
Array<T> |
0–10 elements of T's default |
When a default doesn't fit, override per property with @Gen(...), or conform your own
leaf types manually — no macro needed:
struct Temperature { var celsius: Double }
extension Temperature: RandomGenerable {
static var generator: some RandomGenerator<Temperature> {
FloatGenerator(in: -40...50).map(Temperature.init)
}
}Types that conform — manually or via the macro — compose into other derived types
automatically: a @RandomGenerable struct with a Temperature property just works.
IntGenerator: Generate random integers within a rangeFloatGenerator: Generate random floating-point numbersBoolGenerator: Generate random boolean valuesAnyRandomGenerator: Type-erased container for any random generator
RandomGenerators.letter: Random letters (a-z, A-Z)RandomGenerators.number: Random digits (0-9)RandomGenerators.letterOrNumber: Random alphanumeric charactersRandomGenerators.uppercaseLetter: Random uppercase letters (A-Z)RandomGenerators.lowercaseLetter: Random lowercase letters (a-z)RandomGenerators.ascii: Random ASCII charactersRandomGenerators.latin1: Random Latin-1 charactersRandomGenerators.character(in:): Custom range of characters
Array: Generate arrays of random elements with fixed sizeArrayGenerator: Generate arrays with random sizeDictionary: Generate dictionaries with random key-value pairsElement: Pick random elements from collectionsCollection: Generate custom collections of random elementsShuffled: Generate shuffled versions of collections
Always: Always produce the same valueLCRNG: Linear Congruential Random Number Generator
Map: Transform the output of a generatorFlatMap: Create generators that depend on previous random valuesConcat: Concatenate the output of multiple generatorsZip: Combine multiple generators into tuplesCollect: Collect results from multiple generatorsPrint: Debug generator outputsRemoveDuplicates: Remove duplicate values from a generatorFilter: Generate only values that satisfy a predicateRetry: Retry generation until a condition is metAttemptBounded: Limit generation attempts with configurable fallback strategiesTryMap: Transform with operations that might failFrequency: Weight outputs by frequencyOptional: Generate optional values from a generatorTuple: Create tuples from a generator's output
Additional generators available in the SwiftRandomKitGenerators product:
ColorGenerator: Generate random colors (supports UIKit and SwiftUI)CreditCardGenerator: Generate valid credit card numbers for different card typesDice: Generate random dice rolls of various typesIPAddressGenerator: Generate random IP addresses (IPv4 or IPv6)LatLongGenerator: Generate random geographic coordinates (latitude and longitude)SafariPasswordGenerator: Generate strong passwords following Safari's patternSudokuGenerator: Generate valid Sudoku puzzles with varying difficultyUUIDGenerator: Generate random UUID strings in standard formatVersionNumberGenerator: Generate random semantic version numbers
SwiftRandomKit provides various transformations to modify and combine generators:
// Transform values with map
let diceGen = IntGenerator(in: 1...6)
let doubledDice = diceGen.map { $0 * 2 }
let result = doubledDice.run() // 2, 4, 6, 8, 10, or 12
// Use flatMap for dependent generators
let coinFlip = BoolGenerator()
let weightedDice = coinFlip.flatMap { isHeads in
isHeads ? IntGenerator(in: 1...6) : IntGenerator(in: 4...9)
}// Fixed-size arrays
let fiveDice = IntGenerator(in: 1...6).array(5)
// Variable-size arrays
let countGen = IntGenerator(in: 3...7)
let variableDice = IntGenerator(in: 1...6).arrayGenerator(countGen)
// Collect results from different generators
let smallNumberGen = IntGenerator(in: 1...10)
let mediumNumberGen = IntGenerator(in: 11...50)
let largeNumberGen = IntGenerator(in: 51...100)
let mixedNumbers = [smallNumberGen, mediumNumberGen, largeNumberGen].collect().run()
// e.g., [7, 23, 86]// Always produce the same value
let alwaysSix = Always(6)
// Can be combined with other generators using flatMap
let loadedDice = BoolGenerator().flatMap { isLoaded in
isLoaded ? alwaysSix.eraseToAnyRandomGenerator() : IntGenerator(in: 1...6).eraseToAnyRandomGenerator()
}
print(loadedDice())// Erase the concrete type for API simplicity
func createDiceGenerator() -> AnyRandomGenerator<Int> {
return IntGenerator(in: 1...6).eraseToAnyRandomGenerator()
}
// Store different generator types in a collection
let generators: [AnyRandomGenerator<Int>] = [
IntGenerator(in: 1...100).eraseToAnyRandomGenerator(),
BoolGenerator().map { $0 == true ? 1 : 0 }.eraseToAnyRandomGenerator()
]
print(generators.collect().run())// Generate a secure password with specific requirements
let passwordGen = RandomGenerators.uppercaseLetter
.array(2).flatMap { uppercase in
RandomGenerators.lowercaseLetter.array(5).flatMap { lowercase in
RandomGenerators.number.array(2).flatMap { digits in
Always(Array("!@#$%^&*")).element().map { special in
let allChars = uppercase + lowercase + digits + (special != nil ? [special!] : [])
return String(allChars)
}
}
}
}
let securePassword = passwordGen.run() // e.g., "KTabnre45$"A simpler approach using the zip operator:
// A simpler way to generate a random password
let simplePasswordGen = RandomGenerators.uppercaseLetter.array(2)
.zip(RandomGenerators.lowercaseLetter.array(5))
.zip(RandomGenerators.number.array(2))
.zip(Always(Array("!@#$%^&*")).element())
.map { (components, special) in
let (upperAndLower, digits) = components
let (upper, lower) = upperAndLower
let chars = upper + lower + digits + (special != nil ? [special!] : [])
return String(chars)
}
let password = simplePasswordGen.run() // e.g., "ABcdefg12#"// Generate only even numbers
let evenNumbers = IntGenerator(in: 1...100).filter { $0.isMultiple(of: 2) }
let evenNumber = evenNumbers.run() // Always an even number
// Retry until a condition is met
let diceGen = IntGenerator(in: 1...6)
let sixGenerator = diceGen.retry(maxAttempts: 10, until: { $0 == 6 })
let result = sixGenerator.run() // Will be 6 if found within 10 attempts
// Filter with fallback for maximum attempts
let primeGen = IntGenerator(in: 1...100).attemptBounded(
maxAttempts: 20,
condition: { number in
// Check if number is prime (simplified)
if number <= 1 { return false }
if number <= 3 { return true }
if number.isMultiple(of: 2) || number.isMultiple(of: 3) { return false }
var i = 5
while i * i <= number {
if number.isMultiple(of: i) || number.isMultiple(of: (i + 2)) { return false }
i += 6
}
return true
},
fallbackStrategy: .useDefault(17) // Default to 17 if no prime found in 20 attempts
)
let prime = primeGen.run() // A prime number or 17 if none found
// Using a different generator as fallback
let highRoll = diceGen.attemptBounded(
maxAttempts: 3,
condition: { $0 > 4 },
fallbackStrategy: .useAnotherGenerator { Always(6).run() }
)
let highDiceRoll = highRoll.run() // 5 or 6, or always 6 if not found in 3 attempts
⚠️ Warning: Be careful when using the.keepTryingfallback strategy, as it can lead to infinite loops if the condition can never be satisfied. For example,Always(5).retry(fallbackStrategy: .keepTrying, until: { $0.isMultiple(of: 2) })will hang indefinitely.
// Create a custom dice that can be loaded
struct LoadedDiceGenerator: RandomGenerator {
let bias: Int
let probability: Double
init(bias: Int, probability: Double = 0.7) {
self.bias = bias
self.probability = probability
}
func run<RNG: RandomNumberGenerator>(using rng: inout RNG) -> Int {
FloatGenerator<Double>(in: 0...1)
.flatMap {
$0 < probability ? Always(bias).eraseToAnyRandomGenerator()
: IntGenerator(in: 1...6).eraseToAnyRandomGenerator()
}
.run(using: &rng)
}
}
let loadedDice = LoadedDiceGenerator(bias: 6)
let roll = loadedDice.run() // 6 with 70% probabilityThis project is available under the MIT license. See the LICENSE file for more info.