Linh Tạ

Tạo controller từ xib và storyboard với protocol

Khởi tạo UIViewController tưởng chừng như một việc rất đơn giản nhưng lại ít khi được làm đúng cách. Không khó để bắt gặp các dòng code sau rải rác trong code base


//Khởi tạo từ xib
let customViewController = CustomViewController(nibName: "CustomViewController", bundle: nil)

//Khởi tạo từ storyboard
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let viewController = storyboard.instantiateViewController(withIdentifier: "CustomViewController")
let customViewController = viewController as! CustomViewController

Những cách làm thông dụng này thừa thãi và ẩn chứa nhiều rủi ro bởi chúng viết trực tiếp tên class dưới dạng String. Nếu CustomViewController được sử dụng ở nhiều nơi khác nhau thì việc lặp và copy code là điều không thể tránh khỏi. Hardcode tên class cũng khiến cho việc rename hay refactor sau này trở nên nguy hiểm hơn vì không có gì đảm bảo rằng bạn không bỏ sót chỗ nào.

Nếu bạn để ý kĩ ở ví dụ storyboard, việc type cast từ UIViewController sang CustomViewController cũng không cần thiết và cản trở việc đọc hiểu code. Bạn có thể thay thế việc force unwrap (as!) bằng optional binding (if let, guard let) nhưng kết quả vẫn là những dòng code thừa.

Giải pháp?

Mình đã thấy nhiều cách làm sáng tạo để tránh lặp code và đơn giản hoá việc khởi tạo ViewController như sử dụng một singleton object với static function, tạo riêng static function cho từng UIViewController class, hoặc tạo các global function để dùng chung cho cả project. Tuy nhiên các cách làm này đều cồng kềnh và không thực sự hiệu quả và gọn gàng như mình mong muốn.

Tạo view controller bằng protocol

Protocol và extension là những công cụ tuyệt vời trong Swift sẽ giúp chúng ta giải quyết bài toán này


enum Storyboard: String {
    case storyboardA
    case storyboardB
    case none
}

protocol InterfaceInitable: class {
    static var classId: String { get }
    static func initFromNib() -> Self
    static func initFromStoryboard(id: Storyboard) -> Self
}

extension InterfaceInitable where Self: UIViewController {
    static var classId: String {
        autoreleasepool {
            return String(describing: Self.self)
        }
    }

    static func initFromNib() -> Self {
        return Self(nibName: classId, bundle: nil)
    }

    static func initFromStoryboard(id: Storyboard = .none) -> Self {
        let storyboardId = id == .none ? classId : id.rawValue
        let storyboard = UIStoryboard.init(name: storyboardId, bundle: nil)
        guard let vc = storyboard.instantiateViewController(withIdentifier: classId) as? Self else {
            fatalError("classId không khớp storyboard identifier.")
        }
        return vc
    }
}

extension UIViewController: InterfaceInitable {}

Trong extension của InterfaceInitable, classId được tạo ra động bằng String(describing:), tham số truyền vào - Self.self chính là tên class của view controller cần khởi tạo. Điểm mấu chốt cần lưu ý ở đây là tên file xib, tên của storyboard và storyboard identifier phải giống tên class của view controller. Chỉ cần đạt được yêu cầu này, bạn sẽ hoàn toàn không phải lo về tên class cụ thể và được giải phóng khỏi những rắc rối được nêu ở trên.

Để có thể khởi tạo nhiều ViewController trong cùng một file storyboard mình tạo ra một enum với associated value thuộc dạng String. Lợi dụng việc tự động generate rawValue của enum mình đặt các case của enum chính là tên của các file storyboard. case none trong enum dùng trong trường hợp storyboard chỉ có một ViewController.

Giờ đây chúng ta có thể thoải mái khởi tạo ViewController mà không phải type cast tràn lan, không làm việc trực tiếp với String, và quan trọng hơn cả là không còn nỗi lo lặp code!