Swift Bitwise and Shift Operators: &, |, ^, ~, <<, >>
Swift bitwise and shift operators let you work directly with the individual bits inside integer values. This is useful for flags, masks, binary protocols, permissions, graphics, and performance-sensitive code. In this tutorial, you will learn what the operators &, |, ^, ~, <<, and >> do in Swift 5+, how signed and unsigned integers affect results, how to avoid common mistakes, and how to apply these operators in realistic code.
1. What Is Swift Bitwise and Shift Operations?
Bitwise operators compare or change numbers at the bit level. Instead of treating an integer like a single decimal value such as 42, they work with its binary form such as 00101010. Shift operators move bits left or right.
These operators are available for Swift integer types such as Int, UInt8, Int16, and similar fixed-width integers.
- & performs a bitwise AND and keeps only bits that are set in both values.
- | performs a bitwise OR and sets bits that appear in either value.
- ^ performs a bitwise XOR and sets bits that differ.
- ~ flips every bit in a value.
- << shifts bits left.
- >> shifts bits right.
Bitwise code is usually easiest to understand when you think in binary, even if your program later displays decimal numbers.
2. Why Bitwise and Shift Operators Matter
Many beginners write code for normal arithmetic for years without using bitwise operators. That is fine, because not every project needs them. But when you do need them, they are often the clearest and most efficient tool.
You should reach for bitwise and shift operators when you need compact storage, low-level data handling, flag combinations, protocol parsing, or direct control over bits. For example, a permissions system can store multiple true or false settings inside one integer.
You should not use bitwise code when a regular Boolean property, enum, array, or dictionary is clearer and the compact binary representation adds no value. Bitwise code can become hard to read if used only to look clever.
- Use bitwise operators for flags, masks, packed values, and binary data.
- Use shifts when multiplying or dividing by powers of two in binary-oriented logic.
- Avoid bitwise code when it makes business logic harder to understand.
3. Basic Syntax or Core Idea
Before writing more advanced examples, it helps to see the minimal syntax. The examples below use fixed-width integer types so the bit patterns are easier to reason about.
Bitwise AND, OR, and XOR
In this example, two UInt8 values are combined in three different ways. Each operator compares matching bit positions.
let a: UInt8 = 0b10101100 // 172
let b: UInt8 = 0b00111100 // 60
let andResult = a & b
let orResult = a | b
let xorResult = a ^ b
print(andResult) // 44 -> 00101100
print(orResult) // 188 -> 10111100
print(xorResult) // 144 -> 10010000Here, & keeps only shared 1 bits, | combines all 1 bits, and ^ keeps only the positions where the bits differ.
Bitwise NOT
The ~ operator inverts every bit. Because fixed-width integers have a limited number of bits, the result depends on the type width.
let value: UInt8 = 0b00001111
let inverted = ~value
print(inverted) // 240 -> 11110000This flips 0 bits to 1 and 1 bits to 0. Since UInt8 is 8 bits wide, all 8 positions are inverted.
Left Shift and Right Shift
Shift operators move all bits left or right by a certain number of positions.
let number: UInt8 = 0b00001111
let leftShifted = number << 2
let rightShifted = number >> 2
print(leftShifted) // 60 -> 00111100
print(rightShifted) // 3 -> 00000011A left shift moves bits toward more significant positions. A right shift moves them toward less significant positions. For unsigned integers, shifted-in bits become 0.
Warning: Shifts are not just visual movement. They change the numeric value, and the result depends on the integer type and whether it is signed or unsigned.
4. Step-by-Step Examples
Example 1: Use AND to check whether a flag is enabled
A common bitwise task is checking whether a specific flag exists inside a combined set of flags. Each flag is usually a power of two so it uses exactly one bit.
let canRead: UInt8 = 0b00000001
let canWrite: UInt8 = 0b00000010
let canDelete: UInt8 = 0b00000100
let userPermissions = canRead | canDelete
let hasWritePermission = (userPermissions & canWrite) != 0
let hasDeletePermission = (userPermissions & canDelete) != 0
print(hasWritePermission) // false
print(hasDeletePermission) // trueThe AND operation isolates one bit. If the result is not zero, that flag was present in the original value.
Example 2: Use OR to combine multiple options
Bitwise OR is often used to turn bits on. This is how you build a value that contains many independent settings.
let bold: UInt8 = 0b00000001
let italic: UInt8 = 0b00000010
let underline: UInt8 = 0b00000100
let style = bold | underline
print(style) // 5 -> 00000101The result contains both the bold bit and the underline bit. OR is ideal when you need to merge multiple one-bit options into one value.
Example 3: Use XOR to toggle bits
XOR is useful for toggling a bit. If a bit is 1, XOR with 1 turns it to 0. If a bit is 0, XOR with 1 turns it to 1.
let darkModeFlag: UInt8 = 0b00000001
var settings: UInt8 = 0b00000000
settings = settings ^ darkModeFlag
print(settings) // 1
settings = settings ^ darkModeFlag
print(settings) // 0This pattern is helpful when you want a compact on/off toggle inside a larger integer.
Example 4: Use shifts to extract packed values
Suppose one byte stores red, green, and blue information in different bit positions. A shift followed by a mask can extract each piece.
let packed: UInt8 = 0b10110110
// Top 3 bits
let red = (packed >> 5) & 0b00000111
// Middle 3 bits
let green = (packed >> 2) & 0b00000111
// Bottom 2 bits
let blue = packed & 0b00000011
print(red) // 5
print(green) // 5
print(blue) // 2This is a very common bitwise pattern: shift the target bits into position, then apply a mask with AND.
Example 5: Signed right shift behaves differently
Right shifts on signed integers preserve the sign in Swift. This is called an arithmetic shift. It can surprise developers who expect zeros to be inserted on the left.
let positive: Int8 = 4
let negative: Int8 = -4
print(positive >> 1) // 2
print(negative >> 1) // -2The negative value remains negative after shifting because the sign bit is preserved. This matters when you process signed data at the bit level.
5. Practical Use Cases
- Represent user permissions such as read, write, and delete in one integer field.
- Parse binary file formats or network packet headers where each bit or group of bits has a defined meaning.
- Pack multiple small values into one integer to save memory or match an external protocol.
- Manipulate color channels, pixel formats, or low-level graphics data.
- Toggle feature flags efficiently when many on or off options need compact storage.
- Build masks that extract or clear selected bits in embedded-style or systems-oriented Swift code.
6. Common Mistakes
Mistake 1: Using decimal-looking values instead of bit masks
Beginners sometimes choose random numbers for flags instead of powers of two. That causes flags to overlap.
Warning: Bad code: overlapping flags cannot be checked reliably.
// Bad: these values overlap in binary
let read: UInt8 = 1
let write: UInt8 = 2
let delete: UInt8 = 3Here, 3 is 0b00000011, which overlaps with the first two flags.
// Good: each flag uses a unique bit
let read: UInt8 = 0b00000001
let write: UInt8 = 0b00000010
let delete: UInt8 = 0b00000100Each corrected flag is a power of two, so each one occupies exactly one bit.
Mistake 2: Checking flags with OR instead of AND
To test whether a flag is present, some developers use OR. That does not isolate the existing bit.
Warning: Bad code: OR adds bits and does not answer whether a flag was already set.
let canEdit: UInt8 = 0b00000010
let permissions: UInt8 = 0b00000001
// Bad
let hasEdit = (permissions | canEdit) != 0This returns true even when the original bit was not present, because OR creates a value with the bit turned on.
// Good
let hasEdit = (permissions & canEdit) != 0AND is correct because it preserves only the bits shared by both values.
Mistake 3: Ignoring signed versus unsigned shifts
Signed right shifts can surprise you because the sign bit is preserved.
Warning: Bad code: assuming signed right shift always inserts zero bits.
// Bad assumption
let value: Int8 = -8
let shifted = value >> 1
print(shifted) // -4, not a zero-filled positive resultIf you need zero-fill behavior, use an unsigned integer type when appropriate.
// Good
let value: UInt8 = 0b11111000
let shifted = value >> 1
print(shifted) // 124 -> 01111100Choosing the correct integer type is part of writing correct bitwise code.
Mistake 4: Forgetting type width when using NOT
The result of ~ depends on the width of the integer type. Beginners often expect a simple mathematical negative.
Warning: Bad code: assuming bitwise inversion ignores integer size.
// Bad expectation
let small: UInt8 = 15
let inverted = ~small
print(inverted) // 240, because UInt8 has 8 bitsThe result is not based on unlimited binary digits. It is based on the fixed width of UInt8.
// Good: choose the type intentionally
let small: UInt8 = 15
let mask: UInt8 = 0b11111111
let inverted = small ^ mask
print(inverted) // 240This makes the fixed-width inversion idea more explicit and easier to reason about.
7. Best Practices
Use named masks instead of raw numbers
Bitwise code becomes hard to maintain when numbers like 4 or 32 appear without context. Named constants make your intent clear.
let isAdmin: UInt8 = 0b00000001
let isModerator: UInt8 = 0b00000010
let roles: UInt8 = isAdmin | isModerator
let canAccessAdminPanel = (roles & isAdmin) != 0This is easier to understand than checking for unexplained numeric values.
Choose fixed-width integer types for binary work
When your logic depends on exact bit size, prefer types like UInt8, UInt16, or UInt32. The default Int depends on platform word size.
// Better for protocol fields or packed bytes
let header: UInt16 = 0b1011000011110000
let version = (header >> 12) & 0b1111This avoids ambiguity and gives you stable behavior across environments.
Separate masking and shifting into readable steps
One long bitwise expression may be correct but hard to review. Breaking it into steps helps other developers and reduces bugs.
let packet: UInt16 = 0b1010101111001101
let statusMask: UInt16 = 0b0000000011110000
let maskedStatus = packet & statusMask
let status = maskedStatus >> 4
print(status)This makes debugging much easier than squeezing everything into a single line.
Document the bit layout near the code
Bitwise operations are much safer when you write down what each bit means. A short comment can prevent major confusion later.
// Byte layout: 7 6 5 | 4 3 2 | 1 0
// red green blue
let pixel: UInt8 = 0b10110110
let red = (pixel >> 5) & 0b111The code is the same, but the meaning is much clearer. In bitwise programming, comments often carry essential context.
8. Limitations and Edge Cases
- Bitwise and shift operators work with integer types, not floating-point types like Double or Float.
- The result of ~ depends on the width and signedness of the integer type being used.
- Right shift on signed integers preserves the sign bit, which can produce negative results that surprise beginners.
- Using Int can make bit-width assumptions less explicit because its size follows the platform word size.
- Shift counts should be chosen carefully. In practice, your code should only shift by values that make sense for the target type width.
- Bitwise code is compact but often less readable than higher-level logic, so it can increase maintenance cost if overused.
9. Practical Mini Project
Let us build a small permission manager using bitwise operators. The project will define several permissions, combine them into one value, and provide functions to add, remove, toggle, and check permissions. This is a realistic use of &, |, ^, and ~.
struct Permissions {
static let read: UInt8 = 0b00000001
static let write: UInt8 = 0b00000010
static let delete: UInt8 = 0b00000100
static let publish: UInt8 = 0b00001000
}
struct UserAccount {
var permissionBits: UInt8 = 0
mutating func grant(_ permission: UInt8) {
permissionBits = permissionBits | permission
}
mutating func revoke(_ permission: UInt8) {
permissionBits = permissionBits & ~permission
}
mutating func toggle(_ permission: UInt8) {
permissionBits = permissionBits ^ permission
}
func has(_ permission: UInt8) -> Bool {
return (permissionBits & permission) != 0
}
}
var account = UserAccount()
account.grant(Permissions.read)
account.grant(Permissions.write)
print(account.has(Permissions.read)) // true
print(account.has(Permissions.delete)) // false
account.toggle(Permissions.delete)
print(account.has(Permissions.delete)) // true
account.revoke(Permissions.write)
print(account.has(Permissions.write)) // false
print(account.permissionBits) // final packed permission valueThis mini project demonstrates several key bitwise patterns. OR grants a permission by setting a bit, AND with NOT removes a permission by clearing a bit, XOR toggles a permission, and AND checks whether a specific permission exists. These are the same operations you will see in many production systems that store flags compactly.
10. Key Points
- & keeps only the bits that are 1 in both operands.
- | combines bits and is commonly used to set flags.
- ^ keeps differing bits and is useful for toggling flags.
- ~ inverts every bit in the chosen integer type.
- << and >> move bits left and right.
- Fixed-width integer types like UInt8 make bitwise logic easier to reason about.
- Signed right shifts preserve the sign bit, so negative values need extra care.
- Bit masks should use powers of two so each flag occupies exactly one bit.
11. Practice Exercise
Try building a tiny settings system with bitwise flags.
- Create three settings: sound, music, and notifications.
- Store them inside one UInt8 value.
- Turn on sound and notifications.
- Check whether music is enabled.
- Then toggle music on.
- Expected output: first print false, then print true, then print the final packed numeric value.
- Hint: use | to set flags, & to test them, and ^ to toggle one.
let sound: UInt8 = 0b00000001
let music: UInt8 = 0b00000010
let notifications: UInt8 = 0b00000100
var settings: UInt8 = 0
// Turn on sound and notifications
settings = settings | sound
settings = settings | notifications
// Check music before toggling
let hasMusicBefore = (settings & music) != 0
print(hasMusicBefore) // false
// Toggle music on
settings = settings ^ music
// Check music after toggling
let hasMusicAfter = (settings & music) != 0
print(hasMusicAfter) // true
print(settings) // 712. Final Summary
Swift bitwise and shift operators give you direct control over the binary representation of integer values. In this tutorial, you learned what &, |, ^, ~, <<, and >> do, how to use them with masks and flags, how shifts help with packed data, and why signed versus unsigned integers matter.
You also saw how these operators appear in real programming tasks such as permission systems and binary parsing. Although bitwise code is more specialized than everyday Swift syntax, it becomes much easier once you think in terms of individual bits and use clear masks, comments, and fixed-width types.
If you want to keep improving, practice by packing and unpacking small values, building your own flag types, and reading binary examples carefully. Once these patterns feel natural, many low-level Swift tasks will become much more approachable.