Polyglot
Languages Swift conditionals
Swift § conditionals

Conditionals

Swift’s principal conditional construct is if/else if/else. The condition must be a Bool (no truthiness coercion). Swift adds the guard statement — an “early-exit if false” form that admits substantial linear code paths. The if let and guard let forms admit unwrapping optionals; the if case form admits inline pattern matching. Swift 5.9+ admits if/else as expressions returning values. The combination — strict-Bool conditions, the guard early-exit, the if let unwrapping, the expression-form, the conditional-binding shorthand — is the substance of Swift’s selection surface.

if/else

The principal form:

if condition {
    // body
} else if other {
    // body
} else {
    // body
}

Examples:

if x > 0 {
    print("positive")
} else if x < 0 {
    print("negative")
} else {
    print("zero")
}

The braces are required; the parentheses around the condition are not:

if (x > 0) { /* admitted but redundant */ }
if x > 0 { /* conventional */ }

The condition must be a Bool — Swift admits no truthiness coercion:

let n = 5
if n {                                             // ERROR: Int is not Bool
}
if n != 0 {                                        // OK
}

guard

The guard admits early exit if false:

func process(input: String?) -> String {
    guard let input, !input.isEmpty else {
        return "default"                           // exit if condition false
    }

    // input is unwrapped and non-empty; continue
    return input.uppercased()
}

The form: guard condition else { exit-statement }. The else block must exit the scope via:

  • return
  • throw
  • break
  • continue
  • A function returning Never (e.g., fatalError).

The conventional Swift discipline favours guard for precondition checks — admits substantial linear code paths:

func process(user: User?) async throws {
    guard let user else { throw AuthError.notFound }
    guard user.isActive else { throw AuthError.inactive }
    guard user.hasPermission(.read) else { throw AuthError.forbidden }

    // proceed with confidence — all preconditions checked
    try await fetch(for: user)
}

The pattern is one of Swift’s most distinctive idioms.

if let and guard let

The conditional binding for optional unwrapping:

let name: String? = "Alice"

// if let — branch on presence:
if let unwrapped = name {
    print(unwrapped)                               // String, not String?
} else {
    print("no name")
}

// guard let — early exit on absence:
func greet(name: String?) {
    guard let name else { return }
    print("Hello, \(name)")
}

The shorthand form (Swift 5.7+) admits omitting the right-hand side when it matches the binding name:

if let name {                                      // shorthand for `if let name = name`
    print(name)
}

guard let name else { return }                     // shorthand

For multiple bindings:

if let user, let email = user.email, !email.isEmpty {
    sendNotification(to: email)
}

guard let user, let permissions = user.permissions, !permissions.isEmpty else {
    throw .insufficientPermissions
}

if case

For inline pattern matching:

let value: Int? = 42

if case .some(let n) = value {
    print(n)                                       // 42
}

if case let n? = value {                           // alternate optional pattern
    print(n)
}

// With where:
if case .some(let n) = value, n > 0 {
    print("positive: \(n)")
}

The if case admits substantial pattern matching outside switch:

enum Shape {
    case circle(radius: Double)
    case square(side: Double)
}

let shape: Shape = .circle(radius: 5)

if case .circle(let r) = shape {
    print("radius: \(r)")
}

Treated in Pattern matching.

Ternary

The C-style ternary ?::

let max = a > b ? a : b
let status = user.isActive ? "active" : "inactive"
let display = name ?? "anonymous"                  // similar but for nil-coalescing

The conventional discipline:

  • Use ternary for short value-returning conditionals.
  • Use if/else for substantial branches.
  • Use ?? for nil-default.
  • Avoid nested ternaries — they admit confusion.

if/else as expressions (Swift 5.9+)

Since Swift 5.9, if/else produces a value:

let max = if a > b { a } else { b }

let status = if user.isActive {
    "active"
} else if user.isPending {
    "pending"
} else {
    "inactive"
}

// Conventional pre-5.9 alternative:
let status = {
    if user.isActive { return "active" }
    if user.isPending { return "pending" }
    return "inactive"
}()

The expression-form admits substantial conciseness; the conventional discipline embraces the form for short conditionals.

switch as expression (Swift 5.9+)

Same era admits switch as expression:

let category = switch n {
case 0: "zero"
case 1...10: "small"
case 11...100: "medium"
default: "large"
}

Treated in Pattern matching.

defer

The defer admits deferred execution at scope exit:

func processFile(path: String) throws -> String {
    let file = try FileHandle(forReadingFrom: URL(fileURLWithPath: path))
    defer { file.closeFile() }                     // runs at scope exit, even on throw

    return try String(contentsOf: file)
}

The defer is conventional for resource cleanup; multiple defers run in LIFO order.

Compile-time conditions

For platform/version-specific code:

#if os(iOS)
    import UIKit
#elseif os(macOS)
    import AppKit
#elseif os(Linux)
    import Foundation
#endif

#if DEBUG
    print("debug build")
#endif

if #available(iOS 16, macOS 13, *) {
    // use APIs available only on these versions
} else {
    // fallback
}

The #if is preprocessor-style (compile-time); #available is runtime version checking.

Common patterns

Early return

func process(input: String?) throws -> Result {
    guard let input else {
        throw ParseError.empty
    }
    guard input.count <= maxLength else {
        throw ParseError.tooLong
    }
    guard input.matches(/^[a-z]+$/) else {
        throw ParseError.invalidFormat
    }

    // main body
    return parse(input)
}

The pattern reduces nesting; the conventional Swift style.

Optional unwrapping with guard

func sendEmail(to user: User?, subject: String?, body: String?) {
    guard let user, let subject, let body else {
        return
    }

    // all unwrapped
    actualSend(to: user.email, subject: subject, body: body)
}

Validation chain

func validate(form: Form) -> [ValidationError] {
    var errors: [ValidationError] = []

    if form.name.isEmpty {
        errors.append(.emptyName)
    }
    if let email = form.email, !email.contains("@") {
        errors.append(.invalidEmail)
    }
    if form.age < 0 || form.age > 150 {
        errors.append(.invalidAge)
    }

    return errors
}

if let chain

if let user = currentUser,
   let permissions = user.permissions,
   permissions.contains(.admin) {
    grantAccess()
}

Default value with ??

let port = config.port ?? 8080
let name = user?.name ?? "anonymous"
let timeout = options.timeout ?? defaultTimeout

Pattern with if case

let response: Result<Data, Error> = ...

if case .success(let data) = response {
    process(data)
} else if case .failure(let error) = response {
    handleError(error)
}

// Conventional alternative is `switch`:
switch response {
case .success(let data): process(data)
case .failure(let error): handleError(error)
}

Conditional cast

if let user = sender as? User {
    user.notify()
}

// In a switch:
switch sender {
case let user as User: user.notify()
case let admin as Admin: admin.report()
default: break
}

Defer for cleanup

func writeAtomically(_ data: Data, to path: String) throws {
    let tmpPath = path + ".tmp"
    try data.write(to: URL(fileURLWithPath: tmpPath))
    defer {
        try? FileManager.default.removeItem(atPath: tmpPath)
    }

    try FileManager.default.moveItem(atPath: tmpPath, toPath: path)
    // tmp removal admitted only if move fails
}

Conditional method call

optionalCallback?()                                // calls only if non-nil

if let callback = optionalCallback {
    callback()
}

Bool-returning helper

extension Optional {
    var isSome: Bool {
        self != nil
    }
}

if value.isSome {
    // ...
}

where clause in if let

if let n = parseInt(input), n > 0 && n < 100 {
    print("valid: \(n)")
}

The condition after , admits substantial filtering.

if/else as expression (5.9+)

let status: Status = if user.isActive {
    .active
} else if user.isPending {
    .pending
} else {
    .inactive
}

The form admits substantial conciseness — particularly for value-returning conditionals.

#if for platform-specific code

struct Logger {
    func log(_ message: String) {
        #if DEBUG
            print("[DEBUG] \(message)")
        #else
            // production logging
        #endif
    }
}

#available for runtime checks

if #available(iOS 16, *) {
    useNewAPI()
} else {
    useOldAPI()
}

A note on case let

In if/while/guard, the case let form admits binding within a pattern:

if case let .some(n) = value, n > 0 {
    /* ... */
}

while case let .next(item) = iterator.next() {
    /* ... */
}

for case let .matched(value) in items {
    /* ... */
}

The mechanism admits substantial pattern-driven control flow; treated in Pattern matching.

A note on the conventional discipline

The contemporary Swift conditional advice:

  • Use if/else if/else for boolean dispatch.
  • Use guard for early-exit precondition checks.
  • Use if let / guard let for optional unwrapping.
  • Use the shorthand form (if let name) for same-name patterns (5.7+).
  • Use if case for inline pattern matching.
  • Use ?? for nil-default values.
  • Use ?. for optional chaining.
  • Use the ternary sparingly — only short value conditionals.
  • Use if/switch as expressions (5.9+) for substantial conciseness.
  • Use defer for resource cleanup.
  • Use #available for runtime version checks.

The combination — strict-Bool conditions, the guard early-exit, the conditional binding for optionals, the case-pattern matching, the expression-form (5.9+), the defer for cleanup — is the substance of Swift’s selection surface. The discipline produces clear, type-safe, linear code with substantial flexibility for conditional logic.