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.

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.

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 -> 10010000

Here, & 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 -> 11110000

This 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  -> 00000011

A 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) // true

The 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 -> 00000101

The 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) // 0

This 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)  // 2

This 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) // -2

The 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

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 = 3

Here, 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 = 0b00000100

Each 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) != 0

This 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) != 0

AND 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 result

If you need zero-fill behavior, use an unsigned integer type when appropriate.

// Good
let value: UInt8 = 0b11111000
let shifted = value >> 1
print(shifted) // 124 -> 01111100

Choosing 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 bits

The 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) // 240

This 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) != 0

This 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) & 0b1111

This 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) & 0b111

The code is the same, but the meaning is much clearer. In bitwise programming, comments often carry essential context.

8. Limitations and Edge Cases

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 value

This 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

11. Practice Exercise

Try building a tiny settings system with bitwise flags.

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) // 7

12. 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.