How to Generate a Random Number in Swift (Modern API)

How to Generate a Random Number in Swift (Modern API)

Written by

in

Quick answer: Call the standard library’s random APIs: Int.random(in: 1...6), Double.random(in: 0..<1), Bool.random(), plus randomElement() and shuffled() on collections. For repeatable results, pass a custom RandomNumberGenerator via the using: overload instead of seeding the system generator.

Why this happens

Swift didn’t always have a random API. Early on, the Swift book included a LinearCongruentialGenerator as a demonstration of protocol conformance, and a lot of tutorials copied that code verbatim. In practice, most people reached for the C functions that ship with Darwin: arc4random(), arc4random_uniform(_:), or drand48().

That changed in Swift 4.2, shipped with Xcode 10. The standard library gained random(in:) on numeric types, Bool.random(), randomElement(), shuffle()/shuffled(), and the RandomNumberGenerator protocol. If you’re following an older answer, you’re writing more code than you need — and possibly shipping a subtle bias bug.

How to fix it

1. Call random(in:) on the numeric type

Every standard numeric type has it, so there’s no casting and no UInt32 leaking into your code:

let diceRoll = Int.random(in: 1...6)         // 1 through 6
let index = Int.random(in: 0..<items.count)  // 0 ..< count
let fraction = Double.random(in: 0..<1)      // 0.0 ..< 1.0
let coin = Bool.random()                     // true or false

The range type decides whether the upper bound is included. 1...6 can return 6; 0..<6 cannot. Float, CGFloat, Int8 through UInt64 all have the same method.

One trap: an empty range traps at runtime. Int.random(in: 0..<0) crashes, so check count > 0 first when the collection is dynamic.

2. Use randomElement() and shuffle() for collections

Don’t index into an array with a random Int when the collection already knows how to pick:

let suits = ["hearts", "spades", "diamonds", "clubs"]

let suit = suits.randomElement()   // Optional<String>, nil if empty
let deck = suits.shuffled()        // new array, works on let

var mutable = suits
mutable.shuffle()                  // in place, needs var

randomElement() returns an optional rather than crashing, so unwrap it deliberately — that’s the difference from Int.random(in:).

3. Replace arc4random_uniform and never use %

The old pattern still compiles on Apple platforms:

import Foundation

let diceRoll = Int(arc4random_uniform(6)) + 1

It works, but it returns UInt32, needs a cast, and comes from the Darwin C library rather than the Swift standard library. Int.random(in: 1...6) is the direct replacement.

The real bug to avoid is modulo arithmetic:

let bad = Int(arc4random()) % 6   // biased

arc4random() returns a value in 0..<2^32, and 2^32 is not a multiple of 6, so the first few remainders come up slightly more often than the rest. The effect is small but real, and it matters for anything that has to be fair. Int.random(in:) uses rejection sampling internally, so every value in the range has the same probability. If you must use a raw generator, use arc4random_uniform(_:), which does the same rejection for you.

4. Seed a generator when you need repeatable results

Int.random(in:) uses SystemRandomNumberGenerator, which is seeded by the OS and cannot be reproduced. For tests, procedural level generation, or anything you want to replay from a bug report, conform to RandomNumberGenerator:

struct SeededGenerator: RandomNumberGenerator {
    private var state: UInt64

    init(seed: UInt64) {
        self.state = seed
    }

    mutating func next() -> UInt64 {
        state &+= 0x9E3779B97F4A7C15
        var z = state
        z = (z ^ (z >> 30)) &* 0xBF58476D1CE4E5B9
        z = (z ^ (z >> 27)) &* 0x94D049BB133111EB
        return z ^ (z >> 31)
    }
}

Every random API has a using: overload that takes it:

var gen = SeededGenerator(seed: 42)

let roll = Int.random(in: 1...6, using: &gen)
let pick = suits.randomElement(using: &gen)
let shuffled = suits.shuffled(using: &gen)

Two rules. Pass the generator as inout — it’s a struct with mutable state, so a copy gives you a copy of the sequence. And don’t share one instance across threads; give each task its own seeded copy. Also keep seeded generators away from anything security-related: a reproducible sequence is the opposite of what you want for tokens or keys.

If you already link GameplayKit, GKLinearCongruentialRandomSource(seed:) and GKRandomDistribution give you seeded, repeatable randomness without writing a generator — at the cost of an Apple-only framework dependency.

5. Don’t roll inside a SwiftUI body

A random call in body re-rolls on every redraw, which looks like a flickering value or a view that changes when unrelated state updates:

struct DiceView: View {
    @State private var roll = Int.random(in: 1...6)  // used once per view identity

    var body: some View {
        Text("\(roll)")
            .onTapGesture { roll = Int.random(in: 1...6) }
    }
}

The initial value of @State is only consumed the first time the view identity appears; later redraws reuse the stored value. For anything more complex, compute the random value in your model and let the view read it.

How to prevent it

  • Target the Swift 4.2+ APIs and treat older arc4random snippets as legacy.
  • Never use % to squeeze a random value into a range — use random(in:) or arc4random_uniform.
  • Guard empty ranges and empty collections before calling any random API.
  • Seed only in tests, previews, and debug tooling; keep seeds out of production paths.
  • Keep random calls out of body and out of computed properties the view reads on every pass.

FAQ

Is Int.random(in:) inclusive of the upper bound?

It depends on the range you pass. Int.random(in: 1...6) can return 6, while Int.random(in: 1..<6) stops at 5. The same rule applies to Double.random(in:).

What’s the difference between shuffle() and shuffled()?

shuffle() reorders the array in place and requires a var. shuffled() returns a new array and works on a let, which is usually what you want inside a view or a function that shouldn’t mutate its input.

Can I get the same random sequence twice?

Only with a seeded generator passed through a using: overload. The default SystemRandomNumberGenerator is seeded by the operating system, so two runs of the same code produce different values.

Should I still use arc4random_uniform?

It still works on Apple platforms and is unbiased, but it returns UInt32 and isn’t part of the Swift standard library. Int.random(in:) does the same job with a native signature and works anywhere Swift runs.

Sources & credits

Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.