Greetings, traveler!
Have you ever used the OptionSet
protocol? It doesn’t look like an everyday tool, but it can be helpful in specific cases.
What is an OptionSet?
This type presents a mathematical set interface to a bit set. This is a bitset type, where every bit represents an option. If you are familiar with Objective-C, you may know an OptionSet
alternative called NS_OPTIONS
.
UIKit and Foundation examples
In Swift standard libraries, you can find examples of the OptionSet
like UIView.AutoresizingMask
, UIView.AnimationOptions
and JSONSerialization.ReadingOptions
.
// UIView.AutoresizingMask
let view = UIView()
view.autoresizingMask = [
.flexibleRightMargin,
.flexibleLeftMargin
]
// UIView.AnimationOptions
UIView.animate(
withDuration: 0.3,
delay: 0.5,
options: .autoreverse
) {}
// JSONSerialization.ReadingOptions
let jsonObject: [String: Any] = [
"user": "Bob",
"age": 30,
"info": [
"id": "11",
"rights": "admin"
],
]
let jsonData = try JSONSerialization.data(
withJSONObject: jsonObject,
options: .prettyPrinted
)
Custom OptionSet
You can create your own OptionSet
. The only thing that needs to be done is to create the associated type rawValue
to exhibit the type used for comparison. The rawValue
must be of a type that conforms to the FixedWidthInteger
protocol. You can use UInt8
or Int
. If you want to use UInt8
, remember that you can face additional challenges after reaching a 255 value.
All possible combinations can be stored in 4 bits of memory, each of which can take the value 0 or 1 depending on whether the additional option is selected. Like this:
- First option:
0001
- Second option:
0010
- Third option:
0100
- Fourth option
1000
So, we can create something like this:
struct UserRight: OptionSet {
let rawValue: Int
static let create = UserRight(rawValue: 1)
static let edit = UserRight(rawValue: 2)
static let read = UserRight(rawValue: 4)
static let delete = UserRight(rawValue: 8)
}
Bitwise shift operators
But this is not convenient. Here is where bitwise shift operators come to the rescue. The bitwise left shift operator <<
and bitwise right shift operator >>
move all bits in a number to the left or the right. Using them will make it to multiply or divide an integer by a factor of two. Shifting an integer’s bits to the left by one position doubles its value, whereas shifting it to the right by one position halves its value. Shifting to the right will have no effect if you reach the 0000
value.
struct UserRight: OptionSet {
let rawValue: Int
static let create = UserRight(rawValue: 1 << 1)
static let edit = UserRight(rawValue: 1 << 2)
static let read = UserRight(rawValue: 1 << 3)
static let delete = UserRight(rawValue: 1 << 4)
}
OptionSet vs Enum
While OptionSet
can look similar to Enum
, this entity is intended for a different purpose. We can use an OptionSet
type option as a set of multiple parameters. Of course, you can do such things with Enum
too, but it wasn’t designed to be used like this. Check out this example:
extension UserRight {
static let admin: UserRight = [.create, .edit, .read, .delete]
static let editor: UserRight = [.edit, .read]
}
Now, we can write such a code:
func canRead(_ userRights: UserRight) -> Bool {
userRights.contains(.read)
}
func canDelete(_ userRights: UserRight) -> Bool {
userRights.contains(.delete)
}
canRead(.editor) // true
canDelete(.admin) // true
canDelete(.editor) // false
Printable OptionSet with description
If you want to print out the option value to the console, you can add the CustomStringConvertible
protocol conformance to your OptionSet
. Consider this example:
extension UserRight: CustomStringConvertible {
var description: String {
Self.descriptions
.filter { contains($0.0) }
.map { $0.1 }
.joined(separator: ", ")
}
private static var descriptions: [(Self, String)] {
[
(.create, "create"),
(.edit, "edit"),
(.read, "read"),
(.delete, "delete"),
]
}
}
let admin = UserRight.admin
admin.description // create, edit, read, delete
Custom protocol with Enum benefits
You can create a custom protocol to replicate the OptionSet
behavior but with some convenient additions.
Create the OptionProtocol
, which conforms to some other protocols.
RawRepresentale
— to gain the ability to be converted to and from an associated raw value.Hashable
— for use withSet
.CaseIterable
— to get access to a static container with all cases.
protocol OptionProtocol: RawRepresentable, Hashable, CaseIterable {}
Now, we can use Enum
to create something that conforms to our protocol.
enum UserRightOption: String, OptionProtocol {
case create, edit, read, delete
}
For convenience, we can create a typealias
.
typealias UserRights = Set<UserRightOption>
We can create an extension to build OptionSet
-like static properties.
extension Set where Element == UserRightOption {
static var editor: UserRights {
[.read, .edit]
}
}
And since our type conforms to the CaseIterable
protocol, we can create a property to store all the cases.
extension Set where Element == UserRightOption {
static var admin: UserRights {
Set(Element.allCases)
}
}
Using the CaseIterable
protocol, we can also automatically generate the bitset values for all of these cases to create a similar value for any Set
containing our type. To do this, we can enumerate these values and then, while using the loop, use the bitwise OR |
operator. The bitwise OR operator compares the bits of two numbers. Since it returns a new number whose bits are set to 1 if the bits are equal to 1 in either input number, we can immediately assign this value to the rawValue
property.
extension Set where Element: OptionProtocol {
var rawValue: Int {
var rawValue = 0
for (index, element) in Element.allCases.enumerated() {
if self.contains(element) {
rawValue |= (1 << index)
}
}
return rawValue
}
}
Conclusion
We rarely encounter the OptionSet
protocol in projects. However, it is a really convenient tool when we may want to use operations that a Set
can offer, like contains
, insert
, remove
, symmetricDifference
, union
, intersection
, etc.
If you enjoyed this article, please feel free to follow me on my social media: