A Bug Unleashes Brilliance: Decoupling and Factory Design Pattern in Swift

Greetings

Hello, I’m Ong Yue Huei (AJ), an iOS software engineer at Money Forward, Inc.‘s Tax Return Group.

In this article, I’ll show how decoupling has been a game-changer in our project and when we can introduce the factory design pattern.

One quick question before we dive in: have you ever been in a situation where an update in the external API throws a wrench in your project and you’re forced to do some refactoring or replacement in every single files because they are all highly coupled to that API?

Well… I did. Been there, done that. And that’s what motivates me to write this article. Who knows, it might be a helpful read for someone else experiencing the same thing or looking for a reference for their next refactoring adventure.

I will be breaking it down into four main parts: the background, the problem I encountered, the solution I tried out, and the enhancement I did.

Let’s dive in.

Background

A few months back, our company rolled out a new feature in our TaxReturn app – AI-OCRで仕訳 (Journalizing with AI-OCR). (Note that the linked content is only available in Japanese.) It lets user snap a receipt or select one from their photo library, analyze the information and auto-fill it to create a journal. A quick shoutout: it’s a cool feature, so if you haven’t tried it out, do give it a go!

Back to the point, if you are an iOS engineer, you might be aware that some SourceType (photoLibrary and savedPhotosAlbum) in UIImagePickerController are deprecated. If you are still using them in your project, there is a little bug hanging around, which might or might not affect your apps but it’s still best to play safe and fix it.

Now, regarding the bug. I’ve created a new app, focused on just that one feature (select image via UIImagePickerController’s photoLibrary) to reproduce this bug.

Select an image of type GIF.

That’s it. A mysterious preview view pops up! You can visit GIPHY or any other site to download a GIF and give it a try. This preview view is not created by me—it’s all Apple’s doing. How do I un-create it? I can’t. It’s an unwelcome party crasher; I’m calling it a bug.

And since it is deprecated, it is best to replace it with PHPickerViewController, as suggested by Apple.

The Problem

To fix this, I need to replace all the views that use the deprecated API with a new one. Just a replacement you can say, a straightforward and simple task but a real time-eater! Why? Because all these views are highly coupled with the deprecated API, and I have no choice but to replace the API in each of the individual view.

Now that I need to update each view one by one, it would of course be totally fine to only do this just to fix the bug. But what if, one day, I wish to replace PHPickerViewController with something else? I would need to, once again, replace everything one by one. Back to the square one, just thinking about that makes the future me sad and that is when my brain starts racing.

The Solution

In order to display an image, all a view needs to know is — the image itself, whether it’s a UIImage or the image’s URL; I’ll go with UIImage here. In reality, all a view needs to know is which image is chosen by the user, not how the picker is created or the logic of obtaining the result of type UIImage.

Allow me to explain the current implementation with a diagram to help you better understand what’s happening. I’ll add a zoomed-out version of every image to give a clearer picture of the dependency direction, and I’ll be using blue to represent the external API.

In the current implementation, each view creates its own picker and writes its own logic of getting UIImage from the picker.

The zoomed-out version:

Remember the OCP (Open for extension and closed for modification) from SOLID?

Some pickers required different setting for images and live images. Say the current app supports just images, but one day your boss drop a boom, ‘Let’s support live images too.’ I bet nobody wants to modify all the views just because we’ll be extending the behavior or replacing the picker API.

Let’s decouple the API from the view so that any changes from the API will no longer affect the view.

Here’s the idea:

View will be depending on a local picker (SinglePhotoLibraryPicker) and an abstraction (ImagePickerData), which can be a protocol or a closure – it doesn’t really matter as long as it passes our image to the view; I’ll use protocol here. The logic of getting images will be in the local picker and the view just decides what to do with the image received.

The zoomed-out version:

Notice that the API is no longer directly coupled with the view. The view now does not need to know which API will be used; it is all handled by the SinglePhotoLibraryPicker. If it happens that we need to replace the external API, we do not need to modify each view anymore! But just the one and only SinglePhotoLibraryPicker.

Here’s how the code looks like after the refactoring:

The abstraction ImagePickerData:

protocol ImagePickerData: AnyObject {
    func imagePicker(vc: UIViewController, didFinishPicking image: UIImage)
    // ... you can add fail and cancel handling too ...
}

The local picker SinglePhotoLibraryPicker:

import PhotosUI

final class SinglePhotoLibraryPicker {
    weak var delegate: (any ImagePickerData)?

    func makeViewController() -> UIViewController {
        let vc = PHPickerViewController(configuration: yourConfig)
        vc.delegate = self
        return vc
    }
}

extension SinglePhotoLibraryPicker: PHPickerViewControllerDelegate {
    func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
        // ... logic for getting image from the results ...
        delegate?.imagePicker(vc: picker, didFinishPicking: image)
    }
}

The view that uses the picker:

let photoLibraryImagePicker = SinglePhotoLibraryPicker()
photoLibraryImagePicker.delegate = self
let photoVC = photoLibraryImagePicker.makeViewController()
// ... do something with the vc ...

extension TheView: ImagePickerData {
    func imagePicker(vc: UIViewController, didFinishPicking image: UIImage) {
        //  ... do something with the image ... 
        show(image)
    }
}

The Enhancement

After some thought, I realized that there is an enhancement that can be made by bringing in the factory design pattern.

The APIs for camera and photo library are different, which results in both pickers (camera and photo library) needing to be created in every view that uses them.

In the current implementation, the view is still directly coupled with the external API, UIImagePickerController, that is used for capturing images via camera. It looks something like this:

The zoomed-out version:

The view that uses both pickers:

let photoLibraryImagePicker = SinglePhotoLibraryPicker()
photoLibraryImagePicker.delegate = self
let photoVC = photoLibraryImagePicker.makeViewController()
// ... do something with the vc ...

let imagePicker = UIImagePickerController()
imagePicker.delegate = self
// ... do something with the vc(imagePicker) ...

extension TheView: ImagePickerData {
    func imagePicker(vc: UIViewController, didFinishPicking image: UIImage) {
        //  ... do something with the image ... 
        show(image)
    }
}

extension TheView: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    func imagePickerController(
        _ picker: UIImagePickerController,
        didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]
    ) {
        // ... logic for getting image from the results ...
        //  ... do something with the image ... 
        show(image)
    }
}

First, let’s break the tie between external API used for camera from the view—decouple it, just as I did for the photo library. I’ve created another local picker (CameraPicker) that shares the same ImagePickerData interface as the SinglePhotoLibraryPicker because both pickers return the same data type — image.

The zoomed-out version:

The view after decoupling:

let photoLibraryImagePicker = SinglePhotoLibraryPicker()
photoLibraryImagePicker.delegate = self
let photoVC = photoLibraryImagePicker.makeViewController()
// ... do something with the vc ...

let cameraImagePicker = CameraPicker()
cameraImagePicker.delegate = self
let cameraVC = cameraImagePicker.makeViewController()
// ... do something with the vc ...

extension TheView: ImagePickerData {
    func imagePicker(vc: UIViewController, didFinishPicking image: UIImage) {
        //  ... do something with the image ... 
        show(image)
    }
}

Now that we have both pickers (SinglePhotoLibraryPicker and CameraPicker) with the same behavior (picking an image), we can create both pickers in a factory instead of creating them in every single view. What’s even better is that the view now depends on a factory’s abstraction instead of a concrete type. This will aid us in testing later, allowing for injection, such as a mock.

The zoomed-out version:

The factory:

final class SinglePickerFactory: PickerFactory {
    enum ImageSource {
        case camera
        case photoLibrary
    }

    func makePicker(from source: ImageSource, delegate: (any ImagePickerData)?) -> ImagePicker {
        switch source {
        case .camera:
            let cameraImagePicker = CameraPicker()
            cameraImagePicker.delegate = delegate
            return cameraImagePicker
        case .photoLibrary:
            let photoLibraryImagePicker = SinglePhotoLibraryPicker()
            photoLibraryImagePicker.delegate = delegate
            return photoLibraryImagePicker
        }
    }
}

The view after factory design:

let singlePickerFactory: some PickerFactory
var picker: (any ImagePicker)?

picker = imagePickerFactory.makePicker(from: .photoLibrary, delegate: self) // or (from: .camera, ...)
let vc = picker?.makeViewController()
// ... do something with the vc ...

extension TheView: ImagePickerData {
    func imagePicker(_ picker: any ImagePicker, vc: UIViewController, didFinishPicking image: UIImage) {
        //  ... do something with the image ... 
        show(image)
    }
}

I didn’t mention much about ImagePicker because it’s just an abstraction implemented by the pickers.

final class SinglePhotoLibraryPicker: ImagePicker { ... }
final class CameraPicker: ImagePicker { ... }

With the introduction of picker factory, all the views now do not need to know how the picker is created. The factory takes care of that since the creation responsibility is now passed to it. The view just asks for the picker from the factory, and the factory will create and return it. This enhances code decoupling further, reduces the view’s responsibilities and makes it easier to be tested.

Conclusion

Decoupling and design patterns can be game-changers when you grasp the know-why and know-how. I get a real kick out of playing with them, weaving them into projects, and I hope you find the same joy too.

Still learning, happy coding.

Published-date