Swift OptionSet Explained: Syntax, Use Cases, and Best Practices
Swift’s OptionSet is a protocol used to represent a group of related options that can be combined together. It is especially useful when a value may contain zero, one, or many flags at the same time. In this article, you will learn what OptionSet is, how its syntax works, how to define your own option sets, when to use it instead of an enum, and how to avoid the mistakes that commonly confuse beginners.
Quick answer: Use OptionSet when you need a type-safe collection of named flags that can be combined with each other, such as read/write/execute permissions. Each option usually has a unique bit value, and a single variable can store multiple options at once.
Difficulty: Intermediate
You'll understand this better if you know: basic Swift syntax, how structs and protocols work, and the difference between a single value and a collection of values.
1. What Is OptionSet?
OptionSet is a Swift protocol for defining named options that behave like a set, but are usually stored internally as bits inside an integer raw value. This makes it possible to combine options efficiently while still using readable, type-safe names in your code.
- An OptionSet value can contain multiple options at the same time.
- Each option is usually represented by a unique power-of-two bit value.
- You normally define it with a struct, not an enum.
- It conforms to set-like behavior such as checking membership and combining values.
- It is often used for flags, permissions, formatting options, and configuration choices.
A common point of confusion is enum versus OptionSet. An enum usually means “exactly one case at a time,” while OptionSet means “any combination of these named options.” That difference becomes important as soon as you need more than one choice at once.
2. Why OptionSet Matters
Without OptionSet, developers sometimes use plain integers for flags. That works technically, but it is harder to read and easier to misuse. Named options make code clearer and safer.
For example, imagine file permissions. A plain integer like 5 tells you very little by itself. A value like [.read, .execute] is much easier to understand.
OptionSet matters because it gives you:
- Readable code: named options are clearer than magic numbers.
- Type safety: you work with a dedicated type instead of arbitrary integers.
- Efficient storage: multiple flags fit into one raw value.
- Convenient APIs: you can insert, remove, and test options in a familiar way.
Use it when multiple independent options can be enabled together. Do not use it when only one choice should be valid at a time. In that case, an enum is a better fit.
3. Basic Syntax or Core Idea
An OptionSet type is usually a struct that conforms to the protocol and stores a rawValue. Each static property defines one option.
Defining a simple OptionSet
Here is the minimal pattern. Notice that each option gets a different bit value.
struct ShippingOptions: OptionSet {
let rawValue: Int
static let express = ShippingOptions(rawValue: 1 << 0)
static let insured = ShippingOptions(rawValue: 1 << 1)
static let signatureRequired = ShippingOptions(rawValue: 1 << 2)
}This creates three independent flags. The shift operator gives each option its own bit: 1, 2, and 4.
Creating combined values
You can combine multiple options in an array literal-like syntax.
let delivery: ShippingOptions = [.express, .insured]This value contains both options at once.
Checking whether an option is present
Use contains to test membership.
if delivery.contains(.insured) {
print("Insurance is enabled.")
}This reads naturally and avoids checking raw integer bits manually.
4. Step-by-Step Examples
Example 1: Basic permissions
This example shows the most common use case: permissions that can be combined.
struct Permissions: OptionSet {
let rawValue: Int
static let read = Permissions(rawValue: 1 << 0)
static let write = Permissions(rawValue: 1 << 1)
static let execute = Permissions(rawValue: 1 << 2)
}
let userPermissions: Permissions = [.read, .write]
print(userPermissions.contains(.read))
print(userPermissions.contains(.execute))This prints true and then false. The variable contains read and write, but not execute.
Example 2: Inserting and removing options
OptionSet values support set-like operations such as insertion and removal.
var settings: Permissions = [.read]
settings.insert(.write)
settings.remove(.read)
print(settings.contains(.read))
print(settings.contains(.write))After these operations, read is no longer present and write is enabled.
Example 3: Defining grouped convenience options
You can also define combined presets as static properties. This makes APIs cleaner.
struct DisplayOptions: OptionSet {
let rawValue: Int
static let bold = DisplayOptions(rawValue: 1 << 0)
static let italic = DisplayOptions(rawValue: 1 << 1)
static let underlined = DisplayOptions(rawValue: 1 << 2)
static let emphasis: DisplayOptions = [.bold, .italic]
}
let titleStyle = DisplayOptions.emphasisThis approach avoids repeating the same combinations throughout your code.
Example 4: Reading the raw value
Usually you work with named options, not raw integers. But sometimes raw values help with storage or debugging.
let options: Permissions = [.read, .execute]
print(options.rawValue)This prints 5, because read is 1 and execute is 4, and their bits combine into 5.
5. Practical Use Cases
OptionSet is a good fit for situations where options are independent and can be turned on together.
- File permissions such as read, write, and execute.
- Formatting flags like bold, italic, and underlined text.
- Search filters where several criteria can be active together.
- Feature toggles in a configurable component.
- Networking or logging settings with multiple enabled behaviors.
- Reusable API parameters that benefit from grouped presets like .default or .all.
If your state should be exactly one value, such as pending, approved, or rejected, use an enum instead.
6. Common Mistakes
Mistake 1: Using sequential raw values instead of unique bit values
One of the most common errors is assigning raw values like 1, 2, and 3. That looks reasonable at first, but 3 overlaps with the bits from 1 and 2.
Problem: Overlapping bit patterns make options impossible to test reliably. A combined value may appear to contain an option even when that was not intended.
struct BadOptions: OptionSet {
let rawValue: Int
static let first = BadOptions(rawValue: 1)
static let second = BadOptions(rawValue: 2)
static let third = BadOptions(rawValue: 3)
}Fix: Use powers of two so every option occupies its own bit position.
struct GoodOptions: OptionSet {
let rawValue: Int
static let first = GoodOptions(rawValue: 1 << 0)
static let second = GoodOptions(rawValue: 1 << 1)
static let third = GoodOptions(rawValue: 1 << 2)
}The corrected version works because each option uses a unique, non-overlapping bit.
Mistake 2: Using an enum when multiple choices are needed
Beginners often reach for an enum first because it also gives named values. But enums model one choice, not many combined choices.
Problem: An enum cannot naturally represent read and write at the same time without extra wrapper logic, so the design becomes awkward and harder to extend.
enum Permission {
case read
case write
case execute
}
let permission = Permission.readFix: Use OptionSet when the value may contain several enabled options.
struct Permission: OptionSet {
let rawValue: Int
static let read = Permission(rawValue: 1 << 0)
static let write = Permission(rawValue: 1 << 1)
static let execute = Permission(rawValue: 1 << 2)
}
let permission: Permission = [.read, .write]The corrected version works because one value can represent multiple permissions together.
Mistake 3: Forgetting the required rawValue property
To conform to OptionSet, your type needs a stored rawValue of a suitable integer type.
Problem: Without the required property, the type does not fully conform to the protocol and Swift will report a conformance error such as Type 'X' does not conform to protocol 'OptionSet'.
struct Flags: OptionSet {
static let a = Flags()
}Fix: Add the stored raw value property and create each option with an explicit raw value.
struct Flags: OptionSet {
let rawValue: Int
static let a = Flags(rawValue: 1 << 0)
}The corrected version works because it satisfies the protocol’s core requirement.
7. Best Practices
Practice 1: Define options with bit shifts for clarity
Writing 1 << 0, 1 << 1, and so on makes the pattern obvious and reduces the chance of accidental overlap.
// Less clear
static let read = Permissions(rawValue: 1)
static let write = Permissions(rawValue: 2)
// Preferred
static let read = Permissions(rawValue: 1 << 0)
static let write = Permissions(rawValue: 1 << 1)The preferred version makes the unique-bit structure easy to verify.
Practice 2: Expose convenience groups for common combinations
If the same combination appears often, define a reusable static property instead of rebuilding it everywhere.
struct Permissions: OptionSet {
let rawValue: Int
static let read = Permissions(rawValue: 1 << 0)
static let write = Permissions(rawValue: 1 << 1)
static let execute = Permissions(rawValue: 1 << 2)
static let readWrite: Permissions = [.read, .write]
}This keeps call sites shorter and makes the meaning of repeated combinations clearer.
Practice 3: Prefer named option checks over raw bit logic in app code
You can inspect bits directly, but that usually makes everyday code harder to read.
// Less preferred in normal application code
if (userPermissions.rawValue & Permissions.write.rawValue) != 0 {
print("Can write")
}
// Preferred
if userPermissions.contains(.write) {
print("Can write")
}The preferred version is easier to read, easier to maintain, and better expresses your intent.
8. Limitations and Edge Cases
- Not a replacement for enum: if only one state should be active, OptionSet is the wrong model.
- Raw values must not overlap: duplicate or overlapping bits create confusing behavior.
- Raw values may contain unknown bits: if a value is created from external data, it may include bits you did not define explicitly.
- Integer size matters: very large groups of options may need a wider raw value type such as UInt64 instead of Int.
- Debug output may not be descriptive by default: unless you add your own formatting, printing an option set may not be as readable as printing custom strings.
- Manual persistence needs care: storing only rawValue is convenient, but future changes to bit assignments can break compatibility.
If an OptionSet seems “not working,” the first thing to check is whether every option uses a unique power-of-two value. Most strange behavior comes from overlapping raw values.
9. Practical Mini Project
Let’s build a small, complete example that models user permissions in a simple admin system. This shows definition, combination, membership checks, insertion, and removal in one place.
struct UserPermissions: OptionSet {
let rawValue: Int
static let viewDashboard = UserPermissions(rawValue: 1 << 0)
static let editUsers = UserPermissions(rawValue: 1 << 1)
static let deletePosts = UserPermissions(rawValue: 1 << 2)
static let manageSettings = UserPermissions(rawValue: 1 << 3)
static let editor: UserPermissions = [.viewDashboard, .deletePosts]
static let administrator: UserPermissions = [.viewDashboard, .editUsers, .deletePosts, .manageSettings]
}
var currentUser: UserPermissions = UserPermissions.editor
if currentUser.contains(.viewDashboard) {
print("Show dashboard")
}
if !currentUser.contains(.editUsers) {
print("Hide user management")
}
currentUser.insert(.editUsers)
if currentUser.contains(.editUsers) {
print("User management enabled")
}
currentUser.remove(.deletePosts)
print("Raw permission value:", currentUser.rawValue)This mini project demonstrates a realistic pattern: define single permissions, create reusable roles from combinations, then update a user’s permissions at runtime. It is small, but the same design scales well to larger apps and APIs.
10. Key Points
- OptionSet represents named flags that can be combined in a single value.
- Each option should use a unique power-of-two raw value.
- Use contains, insert, and remove for readable set-style operations.
- Use OptionSet when multiple options can be active together.
- Use an enum instead when only one case should be valid at a time.
- Most bugs come from overlapping raw values or choosing the wrong model.
11. Practice Exercise
Create an OptionSet named Notifications with the options email, sms, and push.
- Create a variable that enables email and push.
- Print whether SMS is enabled.
- Add SMS.
- Print the final raw value.
Expected output: first print false, then print the raw value for all three enabled options.
Hint: Use powers of two for the raw values and call insert to add a new option.
struct Notifications: OptionSet {
let rawValue: Int
static let email = Notifications(rawValue: 1 << 0)
static let sms = Notifications(rawValue: 1 << 1)
static let push = Notifications(rawValue: 1 << 2)
}
var userNotifications: Notifications = [.email, .push]
print(userNotifications.contains(.sms))
userNotifications.insert(.sms)
print(userNotifications.rawValue)12. Final Summary
OptionSet is one of Swift’s most useful tools for representing combinations of named options. It gives you the readability of named values and the efficiency of bitmask-style storage, all while keeping your code type-safe and expressive. When you need flags such as permissions, formatting choices, or configurable features, it is often the best model.
The most important rules are simple: define your type with a rawValue, give each option a unique power-of-two bit, and use set-style methods like contains instead of manual bit checks in normal app code. If only one value should be active, choose an enum instead. A good next step is to practice by building your own OptionSet for app settings or permissions and comparing the result with an enum-based design.