Skip to Content
Course content

165: The Factory Pattern in Swift

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've probably encountered situations where your code is littered with switch statements or if-else blocks just to decide which class to instantiate. It usually happens when you have a few different versions of the "same" thing, and the logic to decide which one to use starts leaking into your view controllers or business logic. That's exactly where the Factory Pattern saves your sanity.

I want to show you how to handle this using a payment processing system. Imagine we support Credit Cards, PayPal, and Apple Pay. Each has its own unique setup, but from the perspective of our checkout screen, they all just need to "process a payment."

Defining a common interface

First, we need a contract. We don't want our app to care if it's talking to a PayPal API or a Stripe SDK; we just want to call a method and have the money move. I'll start by defining a simple protocol.

protocol PaymentMethod {
    func processPayment(amount: Double)
}

class CreditCardPayment: PaymentMethod {
    func processPayment(amount: Double) {
        print("Processing $\(amount) via Credit Card...")
    }
}

class PayPalPayment: PaymentMethod {
    func processPayment(amount: Double) {
        print("Redirecting to PayPal for $\(amount)...")
    }
}

class ApplePayPayment: PaymentMethod {
    func processPayment(amount: Double) {
        print("Authenticating Apple Pay for $\(amount)...")
    }
}

The mistake: Creating objects in the UI layer

When I first built a system like this years ago, I did the "obvious" thing. I put the instantiation logic right in the checkout function. It looked like this:

func completePurchase(amount: Double, method: String) {
    let paymentProcessor: PaymentMethod
    
    if method == "credit_card" {
        paymentProcessor = CreditCardPayment()
    } else if method == "paypal" {
        paymentProcessor = PayPalPayment()
    } else {
        paymentProcessor = ApplePayPayment()
    }
    
    paymentProcessor.processPayment(amount: amount)
}

This works fine for three methods. But then the product manager asks for Google Pay, Klarna, and Bitcoin. Suddenly, my completePurchase function is 50 lines of instantiation logic. More importantly, if the CreditCardPayment class ever needs a new initializer argument (like an API key), I have to hunt down every single place in my UI code where I called CreditCardPayment() and update it. That's a recipe for bugs.

Centralizing creation with a Factory

To fix this, we move that "decision" logic into its own object. The Factory's only job is to look at the input and hand back the correct object. I like using an enum for the input because it prevents typos that happen with strings.

enum PaymentType {
    case creditCard
    case paypal
    case applePay
}

class PaymentFactory {
    static func makePaymentMethod(for type: PaymentType) -> PaymentMethod {
        switch type {
        case .creditCard:
            return CreditCardPayment()
        case .paypal:
            return PayPalPayment()
        case .applePay:
            return ApplePayPayment()
        }
    }
}

Now, look at how much cleaner the checkout logic becomes. The UI doesn't know CreditCardPayment even exists; it only knows about the PaymentMethod protocol and the PaymentFactory.

func completePurchase(amount: Double, type: PaymentType) {
    let processor = PaymentFactory.makePaymentMethod(for: type)
    processor.processPayment(amount: amount)
}

Why this actually matters in the long run

You might think, "I just moved the switch statement to a different file. What did I actually gain?"

  • Single Responsibility: The checkout function handles the purchase flow; the factory handles object creation.
  • Decoupling: If I decide to replace PayPalPayment with a newer PayPalV2Payment class, I only change one line in the factory. The rest of the app never even knows the class name changed.
  • Testability: I can now easily create a MockPayment class for my unit tests and tell the factory to return that instead of hitting a real API.



📋 Practical Task

Build a Notification Dispatcher Factory

Your task is to implement a notification system that can send alerts via different channels: Email, SMS, and Push Notification.

  1. Create a protocol named NotificationChannel with a method send(message: String).
  2. Create three concrete classes (EmailChannel, SMSChannel, and PushChannel) that conform to this protocol and print a unique message for each.
  3. Create a NotificationType enum with cases for each channel.
  4. Implement a NotificationFactory with a static method that takes a NotificationType and returns a NotificationChannel.
  5. In your main code, simulate sending a message to a user by using the factory to instantiate the correct channel based on a variable, then calling the send(message:) method.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.