🌐 Language: Deutsch | English | Español | Français
A Swift package that orders any collection so it looks shuffled yet stays perfectly reproducible: same candidates, same context, same order — every time, on every device. Zero dependencies, Sendable by design, protocol-oriented API.
- 🎲 Deterministic, not random: the same candidates and the same context always produce the same order — no seed to store, nothing to persist
- 🧩 Context-scoped: the very same candidates produce an independent, reproducible order for every context (
"home","recommendations", ...) - ➕ Stable under change: removing a candidate, then adding it back, returns it to its original position relative to the others
- 📦 Zero dependencies: no CryptoKit, no Foundation-only hashing, no third-party package — a small hand-rolled FNV-1a hash is the only moving part
- 🧵 Sendable by design: a stateless value type; a single instance can be shared and reused across concurrent calls
- 🔌 Three ways to identify a candidate: protocol conformance, a
KeyPath, or a closure — pick whichever fits your type - 📖 Fully documented: DocC comments on the whole public API
- 🧪 Thoroughly tested: Swift Testing suites covering ordering guarantees, all three API entry points, and error paths
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/hoseiocean/DeterministicShuffleKit.git", from: "1.0.0")
]Or in Xcode: File → Add Package Dependencies → Enter the repository URL:
https://github.com/hoseiocean/DeterministicShuffleKit.git
import DeterministicShuffle
struct Product: DeterministicShuffleIdentifiable {
let sku: String
let name: String
var shuffleIdentifier: String { sku }
}
let shuffler = DeterministicShuffler()
let products: [Product] = [
Product(sku: "sneakers", name: "Sneakers"),
Product(sku: "boots", name: "Boots"),
Product(sku: "sandals", name: "Sandals")
]
let home = try shuffler.shuffled(products, context: "home")
let recommendations = try shuffler.shuffled(products, context: "recommendations")
// `home` and `recommendations` hold the same products in two different,
// independently reproducible orders.Use this when a type you don't own — or shouldn't make conform — already has a suitable identifier:
struct Article {
let slug: String
let title: String
}
let articles: [Article] = [...]
let ordered = try shuffler.shuffled(
articles,
context: "technology",
identifiedBy: \.slug
)Use this when no single property is a unique identifier on its own:
struct Company {
let exchange: String
let symbol: String
}
let companies: [Company] = [...]
let ordered = try shuffler.shuffled(
companies,
context: "featured"
) { company in
"\(company.exchange):\(company.symbol)"
}do {
let ordered = try shuffler.shuffled(products, context: "home")
} catch DeterministicShuffleError.emptyIdentifier {
// A candidate's identifier was blank.
} catch DeterministicShuffleError.duplicateIdentifier(let identifier) {
// Two candidates shared `identifier`.
}Each candidate is identified by a non-empty, unique string. That identifier is combined with a DeterministicShuffleContext and hashed with FNV-1a, a fast, seedless, non-cryptographic hash — the resulting order is the candidates sorted by that hash.
This gives three guarantees:
- Reproducible: the hash is a pure function of the context and the identifier, so the same inputs always produce the same order, across runs and devices
- Context-scoped: a different context changes every hash, and therefore the whole order, without touching the candidates themselves
- Locally stable: each candidate's position is computed independently of the others, so adding or removing a candidate never reshuffles the rest — removing one and adding it back returns it to its original position
On the rare occasion two candidates hash to the same value, the order falls back to comparing their identifiers directly, so the result stays fully deterministic even then.
- Swift 6.0+
- iOS 13+ / macOS 10.15+
Contributions are welcome! Please read our Contributing Guide before contributing to understand our development process and design decisions.
MIT License - see LICENSE for details.