All articles
iOS6 min read

Why Your Swift Enums Look Amateur (And How to Fix Them in 15 Minutes)

Master wildcards and case let to write code that makes other developers jealous

K
Karan Pal
Author
Image Generated by AI
Image Generated by AI

I was reviewing code the other day when I came across this… masterpiece:

switch networkResult {
case .success:
    if case .success(let data) = networkResult {
        if let user = try? JSONDecoder().decode(User.self, from: data) {
            updateUI(with: user)
        } else {
            showError("Failed to decode user")
        }
    }
case .failure:
    if case .failure(let error) = networkResult {
        if error.code == 401 {
            showLoginScreen()
        } else if error.code == 500 {
            showServerError()
        } else {
            showGenericError(error.localizedDescription)
        }
    }
}

I mean, it works… but it screams “I learned enums from a basic tutorial and never looked back.”

Look, we’ve all been there. Enums seem simple enough — you define some cases, throw them in a switch statement, and call it a day. But here’s the thing: if you’re still writing enum switches like it’s 2015, you’re missing out on some of the most elegant pattern matching features Swift has to offer.

In the next 15 minutes, I’m going to show you how to transform amateur enum handling into code so clean, other developers will wonder if you’ve been secretly studying functional programming for years.

📺 Coming soon to Swift Pal: A complete video breakdown of advanced enum patterns at _https://youtube.com/@swift-pal_

🎯 The Amateur Hour Problem

Let me show you what I mean by “amateur” enum usage. Here’s the kind of code I see in way too many iOS projects:

enum APIResult {
    case success(Data)
    case failure(APIError)
}

enum APIError {
    case networkError(String)
    case serverError(Int, String)
    case authenticationError
    case unknownError
}

// The amateur approach - verbose and repetitive
func handleAPIResult(_ result: APIResult) {
    switch result {
    case .success(let data):
        do {
            let response = try JSONDecoder().decode(APIResponse.self, from: data)
            if response.status == "success" {
                processSuccessfulResponse(response)
            } else {
                handleAPIFailure(response.errorMessage ?? "Unknown error")
            }
        } catch {
            handleDecodingError(error)
        }
        
    case .failure(let error):
        switch error {
        case .networkError(let message):
            showNetworkError(message)
        case .serverError(let code, let message):
            if code >= 500 {
                showServerError(message)
            } else if code == 401 {
                showAuthenticationError()
            } else if code == 403 {
                showPermissionError()
            } else {
                showClientError(code, message)
            }
        case .authenticationError:
            redirectToLogin()
        case .unknownError:
            showGenericError()
        }
    }
}

// More amateur patterns - checking enum cases manually
func isAuthenticationIssue(_ result: APIResult) -> Bool {
    if case .failure(let error) = result {
        if case .authenticationError = error {
            return true
        }
        if case .serverError(let code, _) = error, code == 401 {
            return true
        }
    }
    return false
}

// Extracting data with multiple if-case statements
func getUserFromResult(_ result: APIResult) -> User? {
    if case .success(let data) = result {
        if let response = try? JSONDecoder().decode(APIResponse.self, from: data) {
            if let userData = response.userData {
                return try? JSONDecoder().decode(User.self, from: userData)
            }
        }
    }
    return nil
}

This code isn’t wrong, but it’s… painful to read. Nested switches, repetitive pattern matching, and way too much manual case checking. It’s the kind of code that makes experienced Swift developers wince.

The problem is most developers learn basic enum switching and never discover the powerful pattern matching features that make Swift enums so elegant. They end up writing Java-style conditionals in Swift syntax.

🔥 The 15-minute Transformation

Now watch what happens when we apply proper pattern matching to that exact same functionality:

// The professional approach - elegant and expressive
func handleAPIResult(_ result: APIResult) {
    switch result {
    case .success(let data) where canDecodeSuccessResponse(data):
        guard let response = try? JSONDecoder().decode(APIResponse.self, from: data),
              response.status == "success" else {
            handleDecodingError()
            return
        }
        processSuccessfulResponse(response)
        
    case .success(let data):
        handleDecodingError()
        
    case .failure(.networkError(let message)):
        showNetworkError(message)
        
    case .failure(.serverError(500..., let message)):
        showServerError(message)
        
    case .failure(.serverError(401, _)), .failure(.authenticationError):
        redirectToLogin()
        
    case .failure(.serverError(403, let message)):
        showPermissionError(message)
        
    case .failure(.serverError(let code, let message)):
        showClientError(code, message)
        
    case .failure(.unknownError):
        showGenericError()
    }
}

// Clean pattern matching for boolean checks
func isAuthenticationIssue(_ result: APIResult) -> Bool {
    switch result {
    case .failure(.authenticationError), .failure(.serverError(401, _)):
        return true
    default:
        return false
    }
}

// Elegant data extraction with case let
func getUserFromResult(_ result: APIResult) -> User? {
    guard case .success(let data) = result,
          let response = try? JSONDecoder().decode(APIResponse.self, from: data),
          let userData = response.userData,
          let user = try? JSONDecoder().decode(User.self, from: userData) else {
        return nil
    }
    return user
}

Same functionality. Half the code. Infinitely more readable.

Notice how we eliminated nested switches, combined related cases, used range patterns, and made the data extraction flow naturally. This is what professional Swift enum handling looks like.

🎭 Case Let: Extract Data Like a Pro

The key to elegant enum handling is understanding case let patterns. Most developers know the basics, but there's so much more you can do:

enum UserState {
    case guest
    case loggedIn(User, sessionToken: String)
    case premium(User, subscription: Subscription, features: [String])
    case suspended(User, reason: String, until: Date)
}

// Basic case let (what most developers know)
switch userState {
case .loggedIn(let user, let token):
    print("User \(user.name) is logged in")
default:
    print("User is not logged in")
}

// Advanced case let patterns (what pros use)
switch userState {
// Extract only what you need with partial patterns
case .loggedIn(let user, sessionToken: _):
    welcomeUser(user)
    
case .premium(let user, subscription: _, features: let features) where features.contains("darkMode"):
    enableDarkModeForUser(user)
    
case .premium(let user, subscription: let sub, features: _) where sub.isExpired:
    showRenewalPrompt(for: user)
    
case .suspended(_, reason: let reason, until: let date) where date > Date():
    showSuspensionMessage(reason: reason, until: date)
    
case .suspended(let user, reason: _, until: let date) where date <= Date():
    // Suspension expired, auto-reactivate
    reactivateUser(user)
    
default:
    showGuestExperience()
}

See how we’re extracting only the data we need for each case? No more temporary variables, no more nested if let statements. Just clean, expressive pattern matching.

🃏 Wildcards: The Secret Weapon

Wildcards (_) are where pattern matching gets really powerful. They let you say "I don't care about this value" and focus on what actually matters:

enum APIResponse {
    case success(data: Data, headers: [String: String], statusCode: Int)
    case redirect(location: URL, statusCode: Int)
    case clientError(message: String, statusCode: Int, headers: [String: String])
    case serverError(message: String, statusCode: Int, retryAfter: TimeInterval?)
}

// Focus on what matters with wildcards
func handleResponse(_ response: APIResponse) {
    switch response {
    // Only care about the data, not headers or status code
    case .success(data: let data, headers: _, statusCode: _):
        processData(data)
        
    // Only care about redirect location
    case .redirect(location: let url, statusCode: _):
        navigateTo(url)
        
    // Pattern match on status code ranges, ignore other details
    case .clientError(message: _, statusCode: 400...499, headers: _):
        showClientError()
        
    // Care about retry time, but not the specific message or status
    case .serverError(message: _, statusCode: _, retryAfter: let retryTime?):
        scheduleRetry(after: retryTime)
        
    case .serverError(message: let msg, statusCode: _, retryAfter: nil):
        showPermanentServerError(msg)
    }
}

Advanced Wildcard Patterns

Here’s where wildcards get really clever:

enum PaymentResult {
    case success(transactionId: String, amount: Decimal, currency: String)
    case declined(reason: DeclineReason, amount: Decimal, currency: String)
    case error(PaymentError, attemptCount: Int)
}

func logPaymentEvent(_ result: PaymentResult) {
    switch result {
    // Log successful payments over $100 differently  
    case .success(transactionId: let id, amount: let amount, currency: _) where amount > 100:
        logHighValueTransaction(id: id, amount: amount)
        
    // Standard success logging (ignoring currency entirely)
    case .success(transactionId: let id, amount: let amount, currency: _):
        logTransaction(id: id, amount: amount)
        
    // Only care about specific decline reasons
    case .declined(reason: .insufficientFunds, amount: _, currency: _):
        trackInsufficientFundsDecline()
        
    case .declined(reason: .fraudSuspected, amount: let amount, currency: _):
        flagSuspiciousTransaction(amount: amount)
        
    // Ignore specific decline reason for other cases
    case .declined(reason: _, amount: _, currency: _):
        trackGenericDecline()
        
    // Only track errors that we've retried multiple times
    case .error(_, attemptCount: let count) where count >= 3:
        logPersistentPaymentFailure()
        
    case .error(_, attemptCount: _):
        // Don't log one-off errors
        break
    }
}

Combining Wildcards with Ranges

This is where it gets really elegant:

enum HTTPResponse {
    case response(statusCode: Int, data: Data?, headers: [String: String])
}

func handleHTTPResponse(_ response: HTTPResponse) {
    switch response {
    case .response(statusCode: 200...299, data: let data?, headers: _):
        processSuccessData(data)
        
    case .response(statusCode: 200...299, data: nil, headers: _):
        handleEmptySuccessResponse()
        
    case .response(statusCode: 300...399, data: _, headers: let headers) where headers["Location"] != nil:
        handleRedirect(to: headers["Location"]!)
        
    case .response(statusCode: 400...499, data: _, headers: _):
        handleClientError()
        
    case .response(statusCode: 500...599, data: _, headers: _):
        handleServerError()
        
    case .response(statusCode: let code, data: _, headers: _):
        handleUnexpectedStatusCode(code)
    }
}

Notice how we’re pattern matching on ranges, optional values, and dictionary contents all in one elegant expression. This is the kind of code that makes other developers stop and ask “How did you do that?”

🔗 Tuple Pattern Matching

Here’s where Swift pattern matching really starts to shine — when you need to handle combinations of multiple enum values:

enum NetworkConnection {
    case wifi(strength: Int)
    case cellular(generation: String, strength: Int) 
    case offline
}

enum BatteryState {
    case charging(percentage: Int)
    case discharging(percentage: Int)
    case full
    case critical
}

enum AppMode {
    case foreground
    case background
    case inactive
}

// Traditional approach (amateur hour)
func determineDataSyncStrategy(
    connection: NetworkConnection,
    battery: BatteryState, 
    appMode: AppMode
) -> DataSyncStrategy {
    
    switch connection {
    case .wifi:
        switch battery {
        case .charging, .full:
            switch appMode {
            case .foreground:
                return .aggressive
            case .background:
                return .moderate
            case .inactive:
                return .minimal
            }
        case .discharging(let percentage):
            if percentage > 50 {
                switch appMode {
                case .foreground:
                    return .moderate
                default:
                    return .minimal
                }
            } else {
                return .minimal
            }
        case .critical:
            return .disabled
        }
    case .cellular:
        // ... more nested switches
        return .minimal
    case .offline:
        return .disabled
    }
}

// Professional approach - tuple pattern matching
func determineDataSyncStrategy(
    connection: NetworkConnection,
    battery: BatteryState,
    appMode: AppMode
) -> DataSyncStrategy {
    
    switch (connection, battery, appMode) {
    // Optimal conditions - wifi, good battery, active use
    case (.wifi(strength: 70...), .charging(_), .foreground),
         (.wifi(strength: 70...), .full, .foreground):
        return .aggressive
        
    // Good wifi, but conserve battery in background
    case (.wifi(strength: 50...), .charging(_) | .full, .background):
        return .moderate
        
    // Wifi available, decent battery, but user not active  
    case (.wifi(strength: 30...), .discharging(let percentage), _) where percentage > 50:
        return .moderate
        
    // Low battery - always be conservative
    case (_, .discharging(let percentage), _) where percentage <= 20,
         (_, .critical, _):
        return .minimal
        
    // Cellular with good conditions
    case (.cellular(generation: "5G", strength: 80...), .charging(_) | .full, .foreground):
        return .moderate
        
    // Any cellular in background - be conservative
    case (.cellular(_, _), _, .background | .inactive):
        return .minimal
        
    // Offline or poor connection
    case (.offline, _, _),
         (.wifi(strength: ...29), _, _),
         (.cellular(_, strength: ...40), _, _):
        return .disabled
        
    // Default fallback
    default:
        return .minimal
    }
}

Real iOS App State Management

Here’s a practical example for managing complex app states:

enum UserSession {
    case guest
    case authenticated(User)
    case expired(User, reason: String)
}

enum FeatureFlag {
    case enabled(rolloutPercentage: Int)
    case disabled
    case beta(userIds: Set<String>)
}

enum PurchaseStatus {
    case free
    case premium(expiry: Date)
    case trial(daysRemaining: Int)
}

func shouldShowFeature(
    session: UserSession,
    feature: FeatureFlag,
    purchase: PurchaseStatus
) -> Bool {
    
    switch (session, feature, purchase) {
    // Feature disabled for everyone
    case (_, .disabled, _):
        return false
        
    // Beta feature - only for specific users
    case (.authenticated(let user), .beta(userIds: let userIds), _):
        return userIds.contains(user.id)
        
    // Premium feature requiring active subscription
    case (.authenticated(_), .enabled(_), .premium(expiry: let date)) where date > Date():
        return true
        
    // Trial users get premium features
    case (.authenticated(_), .enabled(_), .trial(daysRemaining: let days)) where days > 0:
        return true
        
    // Free features for authenticated users (rollout percentage check)
    case (.authenticated(let user), .enabled(rolloutPercentage: let percentage), .free):
        return user.isInRollout(percentage: percentage)
        
    // Guests never see authenticated features
    case (.guest, _, _):
        return false
        
    // Expired sessions don't get new features
    case (.expired(_, _), _, _):
        return false
        
    default:
        return false
    }
}

This pattern lets you express complex business logic clearly and catch edge cases you might miss with traditional conditional chains.

⚡ Advanced Patterns That Blow Minds

Now let’s get into the really advanced stuff that will make your colleagues think you’re some kind of Swift wizard:

Where Clauses with Complex Conditions

enum ServerResponse {
    case data(payload: Data, timestamp: Date, cacheControl: String?)
    case error(code: Int, message: String, retryAfter: TimeInterval?)
}

func handleServerResponse(_ response: ServerResponse) {
    let now = Date()
    
    switch response {
    // Data is fresh and cacheable
    case .data(payload: let data, timestamp: let time, cacheControl: let cache?) 
         where now.timeIntervalSince(time) < 300 && cache?.contains("max-age") == true:
        cacheAndDisplayData(data)
        
    // Data is stale but we'll use it
    case .data(payload: let data, timestamp: let time, cacheControl: _)
         where now.timeIntervalSince(time) > 300:
        displayStaleData(data)
        refreshInBackground()
        
    // Fresh data, no caching
    case .data(payload: let data, timestamp: _, cacheControl: nil):
        displayData(data)
        
    // Temporary server error - retry with exponential backoff
    case .error(code: 500...599, message: let msg, retryAfter: let delay?)
         where delay != nil && delay! < 60:
        scheduleRetry(after: delay!, message: msg)
        
    // Rate limited - respect the retry-after header
    case .error(code: 429, message: _, retryAfter: let delay?)
         where delay != nil:
        showRateLimitMessage(retryAfter: delay!)
        
    // Client error - don't retry
    case .error(code: 400...499, message: let msg, retryAfter: _):
        showUserError(msg)
        
    default:
        showGenericError()
    }
}

Nested Pattern Matching with Associated Values

enum ValidationResult {
    case valid
    case invalid([ValidationError])
}

enum ValidationError {
    case required(field: String)
    case format(field: String, expected: String)
    case range(field: String, min: Double?, max: Double?)
    case custom(field: String, message: String)
}

func processValidation(_ result: ValidationResult) {
    switch result {
    case .valid:
        submitForm()
        
    // Single required field error
    case .invalid([.required(field: let field)]):
        highlightField(field, message: "\(field.capitalized) is required")
        
    // Single format error
    case .invalid([.format(field: let field, expected: let format)]):
        highlightField(field, message: "Expected format: \(format)")
        
    // Multiple errors, but all are required fields
    case .invalid(let errors) where errors.allSatisfy({ 
        if case .required(_) = $0 { return true }; return false 
    }):
        showGenericRequiredFieldsError()
        
    // Mix of errors - show first few
    case .invalid(let errors) where errors.count > 3:
        showSummaryErrors(errors.prefix(3))
        
    case .invalid(let errors):
        showDetailedErrors(errors)
    }
}

Custom Pattern Matching Operators

Here’s where things get really wild — you can define your own pattern matching behavior:

struct Version {
    let major: Int
    let minor: Int  
    let patch: Int
}

// Custom pattern matching operator
func ~= (pattern: ClosedRange<Version>, value: Version) -> Bool {
    return pattern.contains(value)
}

// Enable Version comparison
extension Version: Comparable {
    static func < (lhs: Version, rhs: Version) -> Bool {
        if lhs.major != rhs.major { return lhs.major < rhs.major }
        if lhs.minor != rhs.minor { return lhs.minor < rhs.minor }
        return lhs.patch < rhs.patch
    }
}

enum CompatibilityCheck {
    case supported(minimumVersion: Version)
    case deprecated(lastSupportedVersion: Version)
    case unsupported
}

func checkAppCompatibility(
    currentVersion: Version,
    requirement: CompatibilityCheck
) -> String {
    
    let iOS13 = Version(major: 13, minor: 0, patch: 0)
    let iOS15 = Version(major: 15, minor: 0, patch: 0)
    let iOS16 = Version(major: 16, minor: 0, patch: 0)
    
    switch (requirement, currentVersion) {
    // Using custom pattern matching with ranges
    case (.supported(minimumVersion: let minVersion), iOS16...):
        return "Fully supported on iOS 16+"
        
    case (.supported(minimumVersion: let minVersion), iOS13...iOS15) 
         where currentVersion >= minVersion:
        return "Supported with limited features"
        
    case (.deprecated(lastSupportedVersion: let lastVersion), let version)
         where version <= lastVersion:
        return "Deprecated but functional"
        
    case (.unsupported, _):
        return "Not supported"
        
    default:
        return "Compatibility unknown"
    }
}

Binding Multiple Values Simultaneously

enum DatabaseResult {
    case success(records: [Record], metadata: QueryMetadata)
    case partialSuccess(records: [Record], errors: [DatabaseError], metadata: QueryMetadata)
    case failure(DatabaseError)
}

struct QueryMetadata {
    let executionTime: TimeInterval
    let recordCount: Int
    let hasMore: Bool
}

func processQueryResult(_ result: DatabaseResult) {
    switch result {
    // Bind multiple values with conditions
    case .success(records: let records, metadata: let meta) where meta.executionTime > 5.0:
        logSlowQuery(records: records, time: meta.executionTime)
        displayRecords(records)
        
    case .success(records: let records, metadata: let meta) where meta.hasMore:
        displayRecords(records)
        showLoadMoreButton()
        
    case .success(records: let records, metadata: _):
        displayRecords(records)
        
    // Pattern match on partial success with error categorization  
    case .partialSuccess(records: let records, errors: let errors, metadata: _)
         where errors.allSatisfy({ $0.isRecoverable }):
        displayRecords(records)
        showWarningBanner("Some data may be incomplete")
        
    case .partialSuccess(records: let records, errors: let errors, metadata: _):
        displayRecords(records)
        showErrorSummary(errors)
        
    case .failure(let error) where error.isRetryable:
        showRetryOption(for: error)
        
    case .failure(let error):
        showFatalError(error)
    }
}

🚫 Common Mistakes & Anti-Patterns

Alright, time for some real talk. Advanced pattern matching is powerful, but it’s easy to go overboard and create code that’s clever but unreadable. Here are the mistakes I see developers make:

Anti-Pattern #1: Over-Engineering Simple Cases

// ❌ Don't do this - it's showing off, not communicating
enum SimpleState {
    case loading
    case loaded(String)
    case error(String)
}

// Overkill for simple logic
func handleState(_ state: SimpleState) {
    switch state {
    case .loading where Date().timeIntervalSince1970.truncatingRemainder(dividingBy: 2) == 0:
        showEvenSecondLoadingAnimation()
    case .loading:
        showLoadingAnimation()
    case .loaded(let data) where data.count > 100:
        showPaginatedData(data)  
    case .loaded(let data):
        showData(data)
    case .error(let message) where message.lowercased().contains("network"):
        showNetworkError(message)
    case .error(let message):
        showGenericError(message)
    }
}

// ✅ Do this - simple and clear
func handleState(_ state: SimpleState) {
    switch state {
    case .loading:
        showLoadingAnimation()
    case .loaded(let data):
        showData(data)
    case .error(let message):
        showError(message)
    }
}

Anti-Pattern #2: Nested Pattern Matching Hell

// ❌ This is unreadable  
enum ComplexResult {
    case success(Response)
    case failure(ResponseError)
}

enum Response {
    case data(PayloadType)
    case redirect(URL)
}

enum PayloadType {
    case user(UserData)
    case product(ProductData)
}

// Don't do this nested nightmare
switch result {
case .success(.data(.user(let userData))) where userData.isPremium && userData.subscription?.isActive == true:
    // Handle premium user
    break
case .success(.data(.user(let userData))) where userData.isPremium && userData.subscription?.isActive != true:
    // Handle expired premium
    break
case .success(.data(.user(let userData))):
    // Handle regular user
    break
// ... this goes on forever
}

// ✅ Extract intermediate values for clarity
switch result {
case .success(let response):
    handleSuccessResponse(response)
case .failure(let error):
    handleError(error)
}

func handleSuccessResponse(_ response: Response) {
    switch response {
    case .data(let payload):
        handlePayload(payload)
    case .redirect(let url):
        handleRedirect(to: url)
    }
}

func handlePayload(_ payload: PayloadType) {
    switch payload {
    case .user(let userData):
        handleUserData(userData)
    case .product(let productData):
        handleProductData(productData)
    }
}

Anti-Pattern #3: Performance Ignorance

// ❌ This where clause is expensive and called repeatedly
enum DataSet {
    case items([LargeDataItem])
}

func processData(_ data: DataSet) {
    switch data {
    case .items(let items) where items.map({ expensiveCalculation($0) }).reduce(0, +) > 1000:
        handleLargeDataset(items)
    case .items(let items):
        handleNormalDataset(items)  
    }
}

// ✅ Pre-calculate expensive operations
func processData(_ data: DataSet) {
    switch data {
    case .items(let items):
        let totalWeight = items.lazy.map(expensiveCalculation).reduce(0, +)
        if totalWeight > 1000 {
            handleLargeDataset(items)
        } else {
            handleNormalDataset(items)
        }
    }
}

When to Choose Simplicity Over Cleverness

Use complex pattern matching when:

Choose simple approaches when:

Team Adoption Guidelines

If you want to introduce advanced pattern matching to your team:

  1. Start with case let basics — make sure everyone is comfortable extracting associated values
  2. Introduce wildcards gradually — show how they reduce noise in switches
  3. Add where clauses sparingly — only when they genuinely improve readability
  4. Establish team conventions — agree on when to use complex vs simple patterns
  5. Code review ruthlessly — call out clever code that doesn’t improve clarity
// Team-friendly pattern matching
enum NetworkResult<T> {
    case success(T)
    case failure(NetworkError)
}

// Good: Clear business logic expressed naturally
func handleUserData(_ result: NetworkResult<User>) {
    switch result {
    case .success(let user) where user.needsOnboarding:
        showOnboardingFlow(for: user)
    case .success(let user):
        showMainApp(for: user)  
    case .failure(.authenticationRequired):
        showLoginScreen()
    case .failure(let error):
        showError(error)
    }
}

// Avoid: Showing off with unnecessary complexity
func handleUserData(_ result: NetworkResult<User>) {
    switch result {
    case .success(let user) where user.createdAt > Date().addingTimeInterval(-86400) && user.loginCount < 2:
        // This condition is too complex for a where clause
        break
    // ...
    }
}

Your Next Steps for Mastery

  1. Practice with your own code — take one messy enum switch and apply these patterns
  2. Learn tuple destructuring — handle multiple enums simultaneously
  3. Experiment with where clauses — add conditional logic to patterns
  4. Study functional programming — understand how pattern matching fits into larger paradigms
  5. Contribute to open source — see how experienced Swift developers use these patterns

Quick Reference Card

Keep this handy while refactoring:

// Essential patterns to remember:

// Basic case let
case .success(let data): // Extract associated value

// Wildcards for ignoring data  
case .response(statusCode: 200, data: let data, headers: _): // Ignore headers

// Where clauses for conditions
case .items(let items) where items.count > 10: // Conditional matching

// Multiple case matching
case .loading, .refreshing: // Same handling for multiple cases

// Range patterns
case .response(statusCode: 200...299, data: let data): // Success range

// Tuple matching
case (.connected, .premium): // Multiple enums

// Optional patterns
case .data(let content?): // Non-nil optionals only

// Nested extraction
case .user(.premium(subscription: let sub)): // Deep extraction

And there you have it! In just 15 minutes, you’ve learned how to transform amateur enum switching into professional-grade pattern matching that’ll make your colleagues wonder when you became a Swift expert.

The key isn’t to use every advanced pattern in every switch statement — it’s to recognize when these patterns make your code cleaner and more expressive. Start with simple case let extractions, add wildcards to ignore irrelevant data, and gradually work up to complex tuple matching and where clauses.

Your enums will never look amateur again.

📺 Want to see these patterns in action with live coding examples? Check out the complete video tutorial on Swift Pal: _https://youtube.com/@swift-pal_

🎉 Enjoyed this article? Your support means the world to me!

💬 Drop a comment below! I love hearing about your experiences and answering questions

🎬 Subscribe on Youtube and become early subscribers of my channel: https://www.youtube.com/@swift-pal

💼 Let’s connect on LinkedIn for more professional insights: https://www.linkedin.com/in/karan-pal

Happy coding! 🚀

● The newsletter

New articles, straight to your inbox.

No spam, no filler — just new writing on iOS, the web, and AI when it ships. Unsubscribe anytime.

Keep reading