Improving VIPER architecture with protocol’s extensions

viper-protocols-extensions

Modules written using VIPER architecture can have multiple functionalities. A single module like „Edit Profile” can be responsible for fetching user, displaying some basic information, editing user functionalities and uploading a profile photo. There is a lot of approaches to combine all of it into a single module, and I would like to show you mine, which I’ve used recently.

I assume that you are familiar with VIPER’s basics. If you are not, please check some tutorials about it first.

Example:

Let’s start from basics. At the beginning we will prepare all module items including protocols and classes.

protocol EditProfileViewInterface: class {}  
protocol EditProfilePresenterInterface {}  
protocol EditProfileInteractorInterface {}  
protocol EditProfileInteractorDelegate: class {}  
protocol EditProfileDataManagerInterface {}

class EditProfileWireframe {

}

class EditProfileViewController: UIViewController, EditProfileViewInterface {

    var presenter: EditProfilePresenterInterface!

}

class EditProfilePresenter: EditProfilePresenterInterface, EditProfileInteractorDelegate {

    var wireframe: EditProfileWireframe!
    var interactor: EditProfileInteractor!
    weak var viewInterface: EditProfileViewInterface?

}

class EditProfileInteractor: EditProfileInteractorInterface {

    var dataManager: EditProfileDataManagerInterface!
    weak var delegate: EditProfileInteractorDelegate?

}

class EditProfileDataManager: EditProfileDataManagerInterface {

}



I skipped the entire module setup intentionally because I would like to focus on functionalities. At this point let’s assume that we’ve got all dependencies setup up correctly. Now it’s time to start adding functionalities. The first thing which we'll take care of is getting a user from UserSessionManager. In the production app it would look like quite different, but to simplify the example let’s make it like that:

struct User {

    var firstName: String

}

class UserSessionManager {

    static let shared = UserSessionManager()

    private lazy var fakeUser = {
        return User(firstName: "User")
    }()

    var currentUser: User? {
        return fakeUser
    }

}



Now we need to combine all of those parts together. The whole task is really simple. Basically, we can just put dependencies and functionalities directly into our module. Unfortunately, that approach would be problematic later on - we won’t be able to reuse those parts of code in different modules. We can move it to base classes but that solution has another limitation - we can inherit only from a single class, so that won’t let us easily combine a few different functionalities into a single module. To resolve that problem, I will put functionalities inside additional protocols. Using protocol’s extensions, we can put default implementations of all methods there.

protocol DataManagerUserInterface {

    var userSessionManager: UserSessionManager { get }

}

protocol InteractorUserInterface {

    var userDataManager: DataManagerUserInterface { get }
    var userInfo: UserInfo? { get }

}

extension DataManagerUserInterface {

    var userSessionManager: UserSessionManager {
        return UserSessionManager.shared
    }

}

extension InteractorUserInterface {

    var userInfo: UserInfo? {
        guard let user = userDataManager.userSessionManager.currentUser else { return nil }
        return UserInfo(user: user)
    }

}

struct UserInfo {

    let firstName: String

    init(user: User) {
        self.firstName = user.firstName
    }

}


To include those functionalities into our „Edit Profile” module we just need to update our protocols. Looking at protocols we can easily get to know what kind of functionalities specific module has.

protocol EditProfileViewInterface: class {}  
protocol EditProfilePresenterInterface {}  
protocol EditProfileInteractorInterface: InteractorUserInterface {}  
protocol EditProfileInteractorDelegate: class {}  
protocol EditProfileDataManagerInterface: DataManagerUserInterface {}  


At this point there is only one thing we need to add to our classes - it’s user data manager. In our example, it’s just regular data manager, but we need to set it to fulfill protocol conformance.

class EditProfileInteractor: EditProfileInteractorInterface {

    var dataManager: EditProfileDataManagerInterface!
    weak var delegate: EditProfileInteractorDelegate?

    var userDataManager: DataManagerUserInterface {
        return dataManager
    }

}



Basically, that’s all we need to do. Now we can use all functionalities (currently only user information) from EditPresenter just like below.

class EditProfilePresenter: EditProfilePresenterInterface, EditProfileInteractorDelegate {

    var wireframe: EditProfileWireframe!
    var interactor: EditProfileInteractorInterface!
    weak var viewInterface: EditProfileViewInterface?

    var firstName: String? {
        return interactor.userInfo?.firstName
    }
}


As you can see we’ve got all functionalities correlated to a user outside Edit Profile module which is a convenience regarding testing and reusing it in different modules. To expand specific module with the additional functionality, we just need to update our protocols, set require dependency, and we’re ready to go.

Similarly, we can prepare protocols responsible for editing user (please notice that we are using DataManagerUserInterface written previously since we need the same user session manager here). Just like before, it’s really easy to expand our module with that functionality. The only thing we need to do is to add another base protocols and extensions.

protocol InteractorEditUserInterface {

    var userDataManager: DataManagerUserInterface { get }
    func updateUser(firstName: String)

}

extension InteractorEditUserInterface {

    func updateUser(firstName: String) {
        guard var user = userDataManager.userSessionManager.currentUser else { return }
        user.firstName = firstName
        userDataManager.userSessionManager.updateCurrentUser(user)
    }

}



Expanding our module with that new functionality is exactly the same like before:

protocol EditProfileViewInterface: class {

    func nameTextFieldContent() -> String

}
protocol EditProfilePresenterInterface {

    var firstName: String? { get }
    func didChangeNameText()

}
protocol EditProfileInteractorInterface: InteractorUserInterface, InteractorEditUserInterface {}  
protocol EditProfileInteractorDelegate: class {}  
protocol EditProfileDataManagerInterface: DataManagerUserInterface {}

class EditProfilePresenter: EditProfilePresenterInterface, EditProfileInteractorDelegate {

    var wireframe: EditProfileWireframe!
    var interactor: EditProfileInteractorInterface!
    weak var viewInterface: EditProfileViewInterface?

    var firstName: String? {
        return interactor.userInfo?.firstName
    }

    func didChangeNameText() {
        if let text = viewInterface?.nameTextFieldContent() {
            interactor.updateUser(firstName: text)
        }
    }

}


Testability

We can easily test those functionalities since it’s not put directly in a specific module. Let’s try to write some unit tests for InteractorUserInterface. We can use regular UserSessionManager, but in our example, we'll use stub version of it.

class StubUserSessionManager: UserSessionManager {

    var stubUser: User?

    override var currentUser: User? {
        return stubUser
    }

}


Before we use it, we need to have stub version of data manager which will use that session manager:

class StubUserDataManager: DataManagerUserInterface {

    lazy var stubUserSessionManager = {
        return StubUserSessionManager()
    }()

    var userSessionManager: UserSessionManager {
        return stubUserSessionManager
    }

}



Now we can create interactor which conforms to InteractorUserInterface but it uses stub version of UserSessionManager.

class InteractorUserInterfaceTests: XCTestCase {

    class TestInteractor: InteractorUserInterface {

        lazy var stubUserDataManager: StubUserDataManager = {
            return StubUserDataManager()
        }()

        var userDataManager: DataManagerUserInterface {
            return stubUserDataManager
        }

    }

    var interactor: TestInteractor!

    override func setUp() {
        interactor = TestInteractor()
    }

    func testUserInfoHasCorrectFirstName() {
        interactor.stubUserDataManager.stubUserSessionManager.stubUser = User(firstName: "testName")
        XCTAssertEqual(interactor.userInfo?.firstName, "testName")
    }

}


Conclusion

Thanks to this approach, my VIPER modules are really tiny - they contain only logic specific for a single module. All shared functionalities I’ve got inside protocols which let me combine them really easily. It’s also really, convenient because of reusability. I do not need to copy the same parts of code in different places. Last but not least: testability - since those protocols are not strictly correlated to a specific module, it’s really easy to cover it using unit tests just like in the example above. At this tutorial, I presented you how to expand interactors and data managers with new features but using that approach you can easily add additional functionalities for the rest module items like presenter and view interface as well.

I’ve prepared playground, which contains a whole example, including unit tests. Feel free to download it from here.