Thử code đẹp hơn với Dao Protocol trong swift

03 July 2019 — Written by Chính Phạm
#iOS#Swift#Extension#Dao protocol

Mở đầu

Hmm, để mở đầu câu chuyện là thế này, không biết có anh em nào hay khởi tạo view, button, label... ngay trong code mà không phải kéo IBOutlet vào không?

Bản thân mình thường dùng cách này để khởi tạo custom view hay một số màn hình đơn giản, đở phải thêm quá nhiều file *.xib không cần thiết vào, theo mình thấy thì file xib nhiều thì phần nào đó project của chúng ta sẽ ảnh hưởng đến tốc độ build đáng kể.

Ví dụ một UILabel, thông thường thì mình sẽ khởi tạo như bên dưới:

/// ViewController.swift

final class ViewController: UIViewController {

    // MARK: - Computed Properties

    private lazy var label: UILabel = {
        let label = UILabel()
        label.text = "Hello World"
        label.backgroundColor = .gray
        return label
    }()

  ...
}

Nhìn sơ qua thì anh em thấy có vẻ bình thường, nhưng xét một cách trực quan và thẩm mĩ thì UILabel được khai báo tận 2 lần, việc khai báo nhiều lần như vậy cũng không phải là xấu, vì nó là mặc định của swift rồi. 🤓

Giải quyết vấn đề

Để giải quyết vấn đề trên và giúp code của anh em trong có vẻ sạch sẽ và thơm tho hơn thì chúng ta cùng tiếp tục nhé!

Tạo giúp mình một file tên là Dao.swift và để nó ở đâu cũng được (trong project)

/// Dao.swift

import UIKit

protocol Dao {}

extension NSObject: Dao {}

extension Dao where Self: NSObject {

    typealias MultipleClosure = (Self) -> Void

    init(_ closures: MultipleClosure...) {
        self.init()
        closures.forEach { $0(self) }
    }
}

Nhìn vào file Dao.swift ta hiểu như sau: Đầu tiên là tạo một protocol Dao, sau đó ta sẽ cho mỗi Object khi được khởi tạo sẽ có Dao bên trong bằng cách extension cho NSObject.

Lúc này khi khởi tạo một Object, ví dụ UILabel() thì lập tức protocol Dao sẽ biết bản thân nó chính là UILabel.

extension Dao where Self: NSObject {
  typealias MultipleClosure = (Self) -> Void

  init(_ closures: MultipleClosure...) {
      self.init()
      closures.forEach { $0(self) }
  }
}

Ở đây mình có khai báo thêm một trường định danh đó là MultipleClosure giúp anh em có thể truyền nhiều closure vào để trông code có vẻ nguy hiểm hơn, kiểu vậy nè:

private lazy var demoButton = UIButton({
    // Common
    let viewPoint = self.view.center
    let newPoint = CGPoint(
        x: viewPoint.x - (Config.buttonSize.width / 2),
        y: viewPoint.y)
    $0.frame = CGRect(origin: newPoint, size: Config.buttonSize)
    $0.setTitle("Tap Me!", for: .normal)
    $0.backgroundColor = .blue
}, {
    // Layer
    $0.layer.cornerRadius = $0.bounds.height / 2
    $0.layer.borderWidth = Config.buttonBorderWidth
    $0.layer.borderColor = UIColor.lightGray.cgColor
}, {
    // Target action
    $0.addTarget(self, action: #selector(self.handleTapped(_:)), for: .touchUpInside)
})

Việc này sẽ chia ra thành các block code nhỏ nhìn trực quan và dễ hiểu hơn, ví dụ config layer, action...etc.

Ấy mà khoan, ví dụ Object UIButton, khi khởi tạo mà chúng ta config type cho nó là enum ButtonType thì sao nhỉ?

Việc extension cho Object này chỉ thao tác được ở lớp cha, cho nên những lớp con bên trong khi mà config thông qua closure này thì sẽ bị lỗi như sau:

Cannot assign to property: 'buttonType' is a get-only property

Để fix lỗi này thì anh em thêm cho mình đoạn code sau vào file Dao.swift nhé!

extension Dao where Self: UIButton {
    init(type: UIButton.ButtonType, closure: (Self) -> Void) {
        guard let self = UIButton(type: type) as? Self else { fatalError() }
        closure(self)
        self.init()
    }
}

🍻 Ngon lành cành đào, code chạy rồi =)))

Đây là file Dao.swift hoàn chỉnh của mình:

/// Dao.swift

import Foundation
import UIKit

protocol Dao {}

extension NSObject: Dao {}

extension Dao where Self: NSObject {

    typealias MultipleClosure = (Self) -> Void

    init(_ closures: MultipleClosure...) {
        self.init()
        closures.forEach { $0(self) }
    }
}

extension Dao where Self: UIButton {
    init(type: UIButton.ButtonType, closure: (Self) -> Void) {
        guard let self = UIButton(type: type) as? Self else { fatalError() }
        closure(self)
        self.init()
    }
}

🚀 Github Repo cho anh em tham khảo nhé!

Tổng kết

Qua bài viết này, mình hi vọng góp phần giúp cho code của anh em nhìn có vẻ ngon và thơm tho hơn 😘

⚠️ Mình chỉ là muốn code gọn và sạch hơn, không có nghĩa code cũ sai nhé!

Cảm ơn anh em đã đọc bài viết, thấy hay thì đừng quên like nhé 🤤, mọi góp ý, thắc mắc xin vui lòng để lại bình luận bên dưới 👇