Clean Code: Tách View ra khỏi ViewController với override loadView()

03 tháng 02, 2020 - 1439 lượt xem

Tham gia vào chương trình học Swift của Techmaster được 4 tháng, bản thân mình mới chuyển sang học code được 1 thời gian ngắn nên còn nhiều bỡ ngỡ. Rất may là mình đã được các giảng viên của Techmaster hướng dẫn rất nhiệt tình về kiến thức. Tuy nhiên, kiến thức về Swift là vô hạn mà thời gian dạy của giảng viên là hữu hạn. Do vậy, mình phải tự học và tự mày mò. Trong quá trình mày mò, kết hợp với kiến thức do giảng viên hướng dẫn, cũng vỡ ra được 1 số thứ. Nhân dịp năm mới, mình cũng muốn viết 1 vài bài để chia sẻ về kiến thức mà mình đã học được tại Techmaster.

Bất kỳ ai lập trình Swift cũng biết, xcode cho phép chúng ta sử dụng cả Storyboard và viết code để lập trình App. Bản thân mình là người thích code nên mình thường nghiêng về tự viết code hơn là dùng Storyboard. Tự viết code cũng có nhiều ưu điểm so với sử dụng Storyboard. Tuy nhiên, đây không phải là chủ đề bài này nên mình sẽ không đi sâu. Trong qúa trình viết code, mình nhận thấy rằng, việc tự viết code dễ làm ViewController phình to. Ví dụ như dưới đây:

final class MyViewController: UIViewController {
    private let myButton: UIButton = {
    	//
    }()
  
  	private let myView: UIView = {
    	//
    }()
  
  	//10 views hoặc hơn thế nữa
  
  	override func viewDidLoad() {
        super.viewDidLoad()
      	setupViews()
    }
  
  	private func setupViews() {
    	setupMyButton()
      	setupMyView()
      	//phải viết setup cho tất cả views
    }
  
  	private func setupMyButton() {
  	    view.addSubview(myButton)
    	//mỗi view lại có ít nhất 4 constrains. Kinh khủng ...
    }
  
    private func setupMyView() {
  	    view.addSubview(myView)
    	//mỗi view lại có ít nhất 4 constrains. Kinh khủng ...
    }
  
  	//tất cả setup của tất cả views
  
  	//tất cả viewModels logic
  
  	//tất cả functions của views
}

Để rút gon, chúng ta có thể gom tất cả views vào trong 1 View duy nhất:

final class MyViewController: UIViewController {
    
	let myView = MyView()
  
  	override func viewDidLoad() {
        super.viewDidLoad()
      	setupMyView()
    }
  
  	private func setupMyView() {
  	    view.addSubview(myView)
    	//vẫn phải viết constrains, hơn nữa giờ viewController có tới 2 view: view và myView
    }
}

Mình đã suy nghĩ, liệu có cách nào có thể gộp view và myView lại không? Rất may là nhờ sự trợ giúp của anh Gu-gồ nên đã có hướng giải quyết. Đó là sử dụng override loadView().

Phương thức loadView() là 1 phương thức trong ViewController. Công việc chính của loadView() là đảm bảo thuộc tính view xuất hiện đầu tiên khi ViewController được khởi tạo. Khi sử dụng Storyboard, đây là phương thức sẽ tải nib và gắn nó vào view. Tuy nhiên, khi tự viết code, phương thức loadView() chỉ trả ra 1 UIView rỗng. 

Từ phương thức loadView(), chúng ta có thể thay thế view mặc định của UIViewController bằng myView.

final class MyViewController: UIViewController {
	override func loadView() {
	    let myView = MyView()
        view = myView
    }

    override func viewDidLoad() {
        super.viewDidLoad()
	}
}

Cái hay ở đây là, view tự động constraint với viewController. Nên khi thay thế view bằng myView, chúng ta không phải viết code constraint cho myView!!!

Tuy nhiên, lúc này lại phát sinh thêm vấn đề, mặc dùng đã thay thế view = myView. Khi ta call view, view vẫn được xác định là class UIView. Tất cả function trong class MyView sẽ không call được.

Để giải quyết vấn đề này, chúng ta sử dụng protocol để xác định hành vi cho viewController:

/// Giao thức HasCustomView xác định thuộc tính customView cho UIViewControllers được sử dụng để thay đổi thuộc tính view.
public protocol HasCustomView {
    associatedtype CustomView: UIView
}

extension HasCustomView where Self: UIViewController {
    /// The UIViewController's custom view.
    public var customView: CustomView {
        guard let customView = view as? CustomView else {
            fatalError("Expected view to be of type \(CustomView.self) but got \(type(of: view)) instead")
        }
        return customView
    }
}

Và cuối cùng, điều chỉnh viewController 1 chút:

final class MyViewController: UIViewController, HasCustomView {
	typealias CustomView = MyView

	override func loadView() {
	    let customView = CustomView()
        view = customView
    }

    override func viewDidLoad() {
    	super.viewDidLoad()
    	customView.setupLayout() // khai bác các view con, constraint
	}
}

Vậy là chúng ta đã tách được view ra khỏi viewController. Bằng cách này, viewController đã được giảm tải đi tương đối. Code cũng dễ đọc hơn vì mọi khai báo view, phương thức của view sẽ tập trung hết ở class MyView.

Vì đây là bài đầu tiên nên mình chưa có nhiều kinh nghiệm viết hay diễn giải. Nếu các bạn muốn góp ý hoặc thảo luận, vui lòng để lại comment ở bên dưới.

Xin cảm ơn,

Bình luận

avatar
Trịnh Minh Cường 2020-02-05 05:54:21.540447 +0000 UTC
Rất bổ ích !
Avatar
* Vui lòng trước khi bình luận.
Ảnh đại diện
  +18 Thích
+18