Sử dụng Child view controller trong Swift

Trong lập trình iOS nói riêng và các nền tảng của Apple nói chung thì chúng ta luôn đau đầu với việc gom các tính năng mà được sử dụng bởi nhiều view controller khác nhau lại một chỗ. Một mặt chúng ta muốn hạn chế tối đa code trùng lặp, mặt khác vẫn phải đảm bảo được sự tách biệt giữa các component để tránh tình trạng phình to View Controller hay còn gọi là Massive View Controller.

Một ví dụ về vấn đề trên đó là việc xử lý loading state và error state. Phần lớn các view controller trong một ứng dụng sẽ cần load dữ liệu bất đồng bộ tại một thời điểm nào đó - đây là một tác vụ có thể tốn một chút thời gian và có khả năng bị fail. Để cho người dùng biết được là app đang làm gì, chúng ta thường hiển thị một activity indicatore khi đang load dữ liệu và một thông báo error như Alert view, popup...khi có lỗi xảy ra.

Vậy chúng ta nên đặt hai tính năng này ở đâu?🤔. Có một cách rất phổ biến đó là tạo một BaseViewController :

class BaseViewController: UIViewController {
    func showActivityIndicator() {
        ...
    }

    func hideActivityIndicator() {
        ...
    }

    func handle(_ error: Error) {
        ...
    }
}

Nhìn qua thì phương pháp này khá ổn, bởi vì mọi thứ đều rất tiện lợi, các view controller chỉ cần kế thừa từ BaseViewControllers. Tuy nhiên, nó cũng tồn tại một vài vấn đề về mặt kiến trúc, đó là BaseViewController sẽ phải gánh mọi loại tính năng, tác vụ cần thiết, rất khó cho việc maintain sau này.

Một vấn đề nữa đó là dùng cách này sẽ buộc tất cả cá view controller phải kế thừa từ một single class. Điều này sẽ hạn chế tính linh hoạt cho code của bạn khi mà chúng ta chỉ cần kế thừa từ UITableViewController khi muốn implement một Table View.

Do vậy chúng ta cần một giải pháp khác để giải quyết những vấn đề tồn đọng trên, và sử dụng Child view controller dưới dạng như các "plugin" cho phép chúng ta có thể linh hoạt tùy biến các View controller con mà không cần phải sử dụng base class.

Thực ra đối với những người lập trình iOS từ iOS 5 thì Child view controllers không phải là cái gì mới mẻ cả, nhưng nó không được chú ý nhiều. Ở đây chúng ta có một khái niệm rất đơn giản - giống như chúng ta build một UIView hiearchies với subviews và superviews, bạn cũng có thể làm điều tương tự với view controller.

Điều khiến tôi thích thú khi sử dụng child view controller đó là chúng có thể truy cập tới các event giống như các parent view controller( như viewDidLoad, viewWillAppear, vv) mà không cần phải subclass lại. Đồng thời chúng cũng có thể tự xử lý các layout và logic riêng của chúng, nó làm tôi liên tưởng tới các plugin, có thể dễ dàng gắn và tháo rời tùy ý. Ví dụ, chúng ta có thể tạo một child view controller xử lý hiển thị activity indicator như sau:

class LoadingViewController: UIViewController {
    private lazy var activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)

    override func viewDidLoad() {
        super.viewDidLoad()
        activityIndicator.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(activityIndicator)
        NSLayoutConstraint.activate([
            activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
            self?.activityIndicator.startAnimating()
        }
    }
}

Lợi ích lớn nhất của phương pháp này đó là bất ky view controller nào mà cần hiển thị loading indicator đều chỉ cần add LoadingViewController, đồng thời cũng đặt toàn bộ các logic để xử lý indicator ra một chỗ riêng, đảm bảo tính nhất quán , hơn là đặt chúng với những function khác không liên quan. Sau khi implement xong thì làm cách nào để sử dụng LoadingViewController?. UIViewController có cung cấp một API để chúng ta add child view controller, đó là addChildViewController, nhưng thực tế thì mọi thứ không đơn giản chỉ dừng lại ở việc gọi đến method đó, để thêm một child view controller, chúng ta cần phải làm những bước sau:

// Add the view controller as a child
addChildViewController(child)

// Move the child view controller's view to the parent's view
view.addSubview(child.view)

// Notify the child that it was moved to a parent
child.didMove(toParentViewController: self)

tương tự với việc remove chúng:

// Notify the child that it's about to be moved away from its parent
child.willMove(toParentViewController: nil)

// Remove the child
child.removeFromParentViewController()

// Remove the child view controller's view from its parent
child.view.removeFromSuperview()

Sẽ là rất bất tiện và tốn thời gian khi mỗi lần sử dụng child view controller chúng ta lại phải khai báo lại các method trên. Do vậy chúng ta cần gom chúng lại thành một lớp abstract như sau:

extension UIViewController {
    func add(_ child: UIViewController) {
        addChildViewController(child)
        view.addSubview(child.view)
        child.didMove(toParentViewController: self)
    }

    func remove() {
        guard parent != nil else {
            return
        }

        willMove(toParentViewController: nil)
        removeFromParentViewController()
        view.removeFromSuperview()
    }
}

Từ đây thì mỗi lần sử dụng tôi chỉ cần gọi đến 2 method add() hoặc remove()

class ListViewController: UITableViewController {
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        loadItems()
    }

    private func loadItems() {
        let loadingViewController = LoadingViewController()
        add(loadingViewController)

        dataLoader.loadItems { [weak self] result in
            loadingViewController.remove()
            self?.handle(result)
        }
    }
}

Tương tự với loading state thì chúng ta cũng implement child view controller với error state, ở đây chúng ta thêm vào một Reload button chưa một reloadHandler closure:

class ErrorViewController: UIViewController {
    var reloadHandler: () -> Void = {}
}

Phần còn lại thì tương tự với Loading state ở trên:

private extension ListViewController {
    func handle(_ error: Error) {
        let errorViewController = ErrorViewController()

        errorViewController.reloadHandler = { [weak self] in
            self?.loadItems()
        }

        add(errorViewController)
    }
}

Qua các ví dụ trên thì các bạn có thể thấy sự tiện lợi của việc sử dụng child view controller, tuy nhiên không có nghĩa là chúng ta bỏ qua tính kế thừa, hãy linh hoạt trong việc kết hợp các phương pháp với nhau

Nguồn: https://medium.com/@johnsundell/using-child-view-controllers-as-plugins-in-swift-458e6b277b54


All Rights Reserved