Đơn giản hóa các "massive" ViewController với Swift

Introduction

Như chúng ta đã biết, ViewController là một khái niệm quan trọng trong kiến trúc MVC (Model-View-Controller) của Apple, đóng vai trò chủ chốt trong lập trình iOS. Chúng giúp viết code cập nhật, quản lý các thao tác liên quan đến UI, chứa các logic xử lý, trung gian giữa tầng UI và tầng model. Hơn thế nữa, các ViewController còn xử lý các phản hồi của người dùng và cung cấp các cầu nối đến các thành phần, chức năng của device như: device orientation, status bar theme...

Chính vì ViewController đảm đương quá nhiều công việc, cái gì chúng ta cũng nhét vào, nên không ngạc nhiên khi xuất hiện các "massive" ViewController. Đó là các ViewController mà chứa quá nhiều code xử lý, có thể dài đến hàng vài nghìn line of code, vài trăm function lẫn lộn code handle các view và model.

Problem

Cũng có nhiều architecture pattern mới được đưa ra để giải quyết vấn đề "massive" ViewController như là: MVP (Model-View-Presenter), MVVM (Model-View-ViewModel), VIPER (View-Interactor -Presenter -Entities -Router)...

Một trong các vấn đề mà chúng ta gặp phải khi học các architecture pattern mới đó là chúng đôi khi quá phức tạp và phân hóa, phân mảnh quá nhỏ. Ví dụ như VIPER chẳng hạn, viết code cho chỉ một màn hình thôi mà cần phải tạo quá nhiều class, struct thành phần.

Vì vậy trong bài viết này, chúng ta hãy cùng nhau tìm hiểu một cách đơn giản sử dụng các khái niệm có sẵn của Swift để tách các action ra khỏi ViewController, giảm tải, đơn giản hóa chúng, giúp viết code sáng sủa, dễ maintain hơn.

Giả sử chúng ta đang viết code cho một màn hình soạn tin nhắn đơn giản, cho phép soạn và gửi tin nhắn qua mạng. Code ViewController tạm thời như sau:

class MessageComposerViewController: UIViewController {

    private var message: Message
    private let userDatabase: UserDatabase
    private let networking: Networking

    init(recipients: [Recipient], userDatabase: UserDatabase, networking: Networking) {
        self.message = Message(recipients: recipients)
        self.userDatabase = userDatabase
        self.networking = networking
        super.init(nibName: nil, bundle: nil)
    }
    
}

Đoạn code của MessageComposerViewController trên trông cũng khá bình thường, chưa có gì gọi là "massive" cả. Tuy nhiên nếu thêm các function để handle các event như thêm người nhận, gửi và hủy tin nhắn vào ViewController trên:

private extension MessageComposerViewController {

    func handleAddRecipientButtonTap() {
        let picker = RecipientPicker(database: userDatabase)

        picker.present(in: self) { [weak self] recipient in
            self?.message.recipients.append(recipient)
            self?.renderRecipientsView()
        }
    }

    func handleCancelButtonTap() {
        if message.text.isEmpty {
            dismiss(animated: true)
        } else {
            dismissAfterAskingForConfirmation()
        }
    }

    func handleSendButtonTap() {
        let sender = MessageSender(networking: networking)

        sender.send(message) { [weak self] error in
            if let error = error {
                self?.display(error)
            } else {
                self?.dismiss(animated: true)
            }
        }
    }
    
}

Việc implement code handle các action event trong chính ViewController trông có vẻ khá tiện lợi và đơn giản. Tuy nhiên khi số lượng action event tăng lên. chúng ta có thể tổ chức lại code của các action này sao cho ViewController chỉ tập trung vào quản lý các view, setup các layout constraint và detect các thao tác của người dùng.

Actions

Các action có thể chia làm 2 loại: đồng bộ và bất đồng bộ. Các task tốn ít thời gian và tài nguyên, có thể xử lý trong một khoảng thời gian ngắn thuộc kiểu synchronous. Còn các task asynchronous thì thường tốn nhiều thời gian để thực hiện.

Để model hóa 2 loại action trên, chúng ta tạo một generic enum tên là Action. Enum này không chứa case nào mà chỉ chứa 2 type alias, một cho kiểu sync và một cho kiểu async như sau:

enum Action<I, O> {
    typealias Sync = (UIViewController, I) -> O
    typealias Async = (UIViewController, I, @escaping (O) -> Void) -> Void
}

Với việc sử dụng type alias như trên, bây giờ chúng ta có thể thể hiện tất cả các action của MessageComposerViewController dưới dạng một tuple như sau:

extension MessageComposerViewController {
    typealias Actions = (
        addRecipient: Action<Message, Message>.Async,
        finish: Action<Message, Error?>.Async,
        cancel: Action<Message, Void>.Sync
    )
}

Như vậy, ViewController đã được đơn giản hóa, lược bỏ phần networking và database, thay vào đó chỉ cần khởi tạo bằng kiểu tuple Actions.

class MessageComposerViewController: UIViewController {

    private var message: Message
    private let actions: Actions

    init(recipients: [Recipient], actions: Actions) {
        self.message = Message(recipients: recipients)
        self.actions = actions
        super.init(nibName: nil, bundle: nil)
    }
    
}

Tiếp theo, để thực sự sử dụng các action trên, hãy sửa lại code ở các function event handler. Chỉ cần gọi các action đã được define sẵn được truyền qua funtion init của ViewController:

private extension MessageComposerViewController {

    func handleAddRecipientButtonTap() {
        actions.addRecipient(self, message) { [weak self] newMessage in
            self?.message = newMessage
            self?.renderRecipientsView()
        }
    }

    func handleCancelButtonTap() {
        actions.cancel(self, message)
    }

    func handleSendButtonTap() {
        let loadingViewController = add(LoadingViewController())

        actions.finish(self, message) { [weak self] error in
            loadingViewController.remove()
            error.map { self?.display($0) }
        }
    }
    
}

Sau khi refactor code như trên, MessageComposerViewController bây giờ chỉ tập trung vào việc control các view. Và hãy implement các action ở context đã khởi tạo ra ViewController đó. Ví dụ function hiển thị màn hình soạn tin nhắn được update lại như sau:

func presentMessageComposerViewController(for recipients: [Recipient], in presentingViewController: UIViewController) {
    let composer = MessageComposerViewController(recipients: recipients,
        actions: (
            addRecipient: { viewController, message in
                // Implement detail logic for adding recipient
            },
            cancel: { viewController, message in
                 // Implement detail logic for canceling
            },
            finish: { viewController, message in
                 // Implement detail logic for finishing
            }
        )
    )

    presentingViewController.present(composer, animated: true)
}

Với việc tổ chức code như trên, code trong các ViewController giờ chỉ bao gồm các function control view, linh hoạt và dễ đọc cho người mới, dễ quản lý, dễ maintain hơn.

Một lợi ích khác của việc tách các private method action sang thành một collection tuple là khi nhìn vào, chúng ta sẽ có cái nhìn tổng quát về các action, event handler của ViewController đó. Ví dụ như ProductViewController dưới đây, có thể dễ dàng nhận ra ViewController này có bốn action async và sync.

extension ProductViewController {
    typealias Actions = (
        load: Action<Product.ID, Result<Product, Error>>.Async,
        purchase: Action<Product.ID, Error?>.Async,
        favorite: Action<Product.ID, Void>.Sync,
        share: Action<Product, Void>.Sync
    )
}

Việc thêm action mới cho ViewController cũng trở nên dễ dàng hơn bao giờ hết. Chúng ta không cần thêm phụ thuộc mới vào ViewController trong hàm khởi tạo và lưu thành các property mới. Đơn giản chỉ cần thêm action mới vào action collection đã có, các logic khác hãy implement ở ViewController context ban đầu.

Conclusion

Vấn đề "massive" ViewController thực sự không có một cách giải quyết triệt để và thực sự hoàn hảo. Có nhiều kỹ thuật cũng như architecture pattern khác nhau, mỗi kỹ thuật, kiến trúc đều có những điểm hay, phù hợp với nhiều kiểu project khác nhau.

Trong khi các architecture pattern (như MVP, MVVM, VIPER...) hướng đến cách giải quyết làm cho project trở nên phân hóa triệt để, mỗi phần đảm nhiệm một chức năng cụ thể và rành mạch. Còn cách tách các action ra khỏi ViewController trong bài viết này đơn giản hơn rất nhiều, giúp giảm tải code trong ViewController mà không thay đổi quá nhiều về cấu trúc project.

Source article: https://www.swiftbysundell.com/posts/extracting-view-controller-actions-in-swift