Tag: iOS Fresher Training

  • Design Pattern: Builder Pattern trong iOS

    Design Pattern: Builder Pattern trong iOS

    Design Pattern: Builder pattern trong iOS

    Nghe đến Design Pattern, chắc hẳn mỗi lập trình viên đều biết đến kỹ thuật quan trọng này và đã từng áp dụng nó ít nhất một lần. Design pattern giúp bạn giải quyết vấn đề một cách tối ưu nhất, cung cấp cho bạn các giải pháp trong lập trình hướng đối tượng (OOP). Phần lớn các ngôn ngữ lập trình đều có thể áp dụng Design pattern và Swift cũng không ngoại lệ!

    Design pattern có 3 nhóm chính:

    • Creational Pattern bao gồm: Abstract Factory, Factory Method, Singleton, Builder, Prototype.
    • Structural Pattern bao gồm: Adapter, Bridge, Composite, Decorator, Facade, Proxy và Flyweight.
    • Behavioral Pattern bao gồm: Interpreter, Template Method, Chain of Responsibility, Command, Iterator, Mediator, Memento, Observer, State, Strategy và Visitor.

    Hôm nay, chúng ta sẽ cùng tìm hiểu một design pattern trong nhóm Creational Pattern (Nhóm khởi tạo) là Builder pattern.

    Nội dung

    • Builder pattern là gì?
    • Vấn đề
    • Sử dụng nó như thế nào
    • Áp dụng trong iOS
    • Tổng kết

    Builder pattern là gì?

    Trong OOP, Builder pattern là một loại pattern thuộc nhóm Creational pattern (Tạo dựng), vì vậy nó sẽ giúp chúng ta giải quyết các vấn đề liên quan tới tạo dựng một object bằng cách:

    • Chia nhỏ các hàm khởi tạo của một object.
    • Đặt cấu hình mặc định và logic xử lý liên quan tới việc khởi tạo một object từ trong class ra bên ngoài và đẩy vào Builder class.

    Và việc chia nhỏ và đẩy vào Builder class như thế nào thì chúng ta hãy cùng tìm hiểu tiếp về vấn đề và áp dụng Builder Pattern trong một bài toán.

    Vấn đề

    Giả sử, chúng ta có một chương trình đặt bánh pizza, và mục tiêu của chúng ta là tạo ra chương trình đặt bánh để lấy yêu cầu từ khách hàng.

    Chúng ta có đối tượng Pizza, vả khởi tạo các đối tượng Pizza:

    
    class Pizza {
        
        var smell: String
        var base: String
        var size: String
        var isChiliSauce: Bool
        var isKetchup: Bool
        
        init(smell: String, base: String, size: String, isChiliSauce: Bool, isKetchup: Bool) {
            self.smell = smell
            self.base = base
            self.size = size
            self.isChiliSauce = isChiliSauce
            self.isKetchup = isKetchup
        }
    }
    
    
    
    let sizeM = Pizza(smell: "Bò", base: "Mỏng", size: "M", isChiliSauce: true, isKetchup: true)
    
    let sizeMChicken = Pizza(smell: "Gà", base: "Mỏng", size: "M", isChiliSauce: true, isKetchup: true)
    
    let sizeL = Pizza(smell: "Gà", base: "Dày", size: "L", isChiliSauce: true, isKetchup: true)
    
    

    Chúng ta có thể thấy hàm tạo khá nhiều thuộc tính, và có những thuộc tính mặc định ít thay đổi. Vì những thuộc tính đó ít thay đổi nên sẽ không tránh khỏi việc duplicate code. Do đó, người ta sử dụng Builder Pattern.

    Sử dụng nó như thế nào

    Class diagram

    Cùng phân tích một chút nhé:

    • Director: Đại diện cho module cần kết quả từ việc khởi tạo Pizza.
    • PizzaBuilder: Một class implement InterfaceBuilder, có nhiệm vụ khởi tạo ra Pizza, thông qua hàm build().
    • Pizza: Là object cần khởi tạo.

    Demo

    Một protocol nhằm khai báo các phương thức

    
    protocol PizzaBuilderProtocol {
        
        func build() -> Pizza
        
        func setSmell(_ smell: String) -> PizzaBuilder
    
        func setBase(_ base: String) -> PizzaBuilder
    
        func setSize(_ size: String) -> PizzaBuilder
        
        func withChiliSauce(_ isChiliSauce: Bool) -> PizzaBuilder
        
        func withKetchup(_ isKetchup: Bool) -> PizzaBuilder
    }
    
    

    PizzaBuilder triển khai các phương thức đã khai báo ở PizzaBuilderProtocol

    
    class PizzaBuilder: PizzaBuilderProtocol {
        
        private var pizza = Pizza()
        
        func build() -> Pizza {
            return self.pizza
        }
        
        func setSmell(_ smell: String) -> PizzaBuilder {
            pizza.smell = smell
            return self
        }
        
        func setBase(_ base: String) -> PizzaBuilder {
            pizza.base = base
            return self
        }
        
        func setSize(_ size: String) -> PizzaBuilder {
            pizza.size = size
            return self
        }
        
        func withChiliSauce(_ isChiliSauce: Bool) -> PizzaBuilder {
            pizza.isChiliSauce = isChiliSauce
            return self
        }
        
        func withKetchup(_ isKetchup: Bool) -> PizzaBuilder {
            pizza.isKetchup = isKetchup
            return self
        }
        
    }
    
    

    Cart ở đây giữ vai trò tương đương Director trong hình bên trên.

    
    class Cart {
        
        let orderId: String
        let products: [Pizza]
        
        init(orderId: String, products: [Pizza]) {
            self.orderId = orderId
            self.products = products
        }
    }
    
    let pizza1 = PizzaBuilder()
                .setSize("s")
                .setSmell("Gà")
                .setBase("Dày")
                .build()
            
    let cart1 = Cart(orderId: "OrderId1", products: [pizza1])
    
    

    Cách thực hiện thật đơn giản phải không? Bản chất của nó là chia nhỏ các properties trong Object ra thành các hàm getter, setter trong Builder và sau đó thực hiện set giá trị cho chúng và khởi tạo Object qua hàm build(). Và đó chính là cách viết theo Builder Pattern.

    Chắc hẳn khi các bạn đọc đến đây sẽ có một suy nghĩ rằng: Vậy tại sao không dùng default value? Đúng vậy, khi Builder Pattern được giới thiệu, ngôn ngữ họ sử dụng khi đó là C++. Nhưng, Swift của chúng ta đã cung cấp tính năng default value, và chúng ta có thể hoàn toàn loại bỏ Builder. Nói như vậy thì Builder Pattern sẽ trở nên vô dụng trong Swift?

    Chắc chắn là không. Những tài liệu trên mạng về Builder Pattern khá nhiều, và nó đang hướng người đọc vào vấn đề default value trong các ngôn ngữ như: C++, Java…Chúng ta hãy cùng nhìn lại về những thứ mà Builder làm, đó là giảm thiệu sự phức tạp khi khởi tạo thông qua hàm builder(). Bản chất chính là nó đang đóng gói việc khởi tạo một Object. Khi đó, chúng ta sẽ có thể xử lý nhiều hiện object, do something… trước khi trả về một Object hoàn chỉnh.

    Áp dụng trong iOS

    Chúng ta có thể áp dụng Builder Pattern ở rất nhiều trường hợp trong lập trình iOS. Mình, mình áp dụng trong việc customize một style cho Label, Button…

    Thường khi, chúng ta customize một Label theo một style nào đó, chúng ta sẽ thường khởi tạo như thế này, nhìn có vẻ khá rối và không được thú vị cho lắm.

    
    let attrString = NSMutableAttributedString(string: title)
            
    attrString.addAttributes([NSMutableAttributedString.Key.foregroundColor: UIColor.red],
                                      range: attrString.mutableString.range(of: title))
            
    attrString.addAttributes([NSMutableAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 13)],
                                      range: attrString.mutableString.range(of: title))
            
    attrString.addAttribute(NSAttributedString.Key.strikethroughStyle, value: 2,
                                         range: NSMakeRange(0, attrString.length))
            
    titleLabel.attributedText = attrString
    
    

    Chúng ta có thể áp dụng Builder pattern trong trường hợp này để nhìn code trở lên dễ đọc hơn và cũng dễ dàng phát triển trong tương lai.

    Tạo một builder và protocol

    
    protocol AttributedStringBuilderProtocol {
        
        func build() -> NSMutableAttributedString
        func setFont(_ font: UIFont, forSubString subString: String) -> AttributedStringBuilder
        func setColor(_ color: UIColor, forSubString subString: String) -> AttributedStringBuilder
        func withStrikeThrough() -> AttributedStringBuilder
        
    }
    
    class AttributedStringBuilder: AttributedStringBuilderProtocol {
        
        private var attrString: NSMutableAttributedString
        
        init(string: String) {
            self.attrString = NSMutableAttributedString(string: string)
        }
        
        func build() -> NSMutableAttributedString {
            return self.attrString
        }
        
        func setFont(_ font: UIFont, forSubString subString: String) -> AttributedStringBuilder {
            self.attrString.addAttributes([NSMutableAttributedString.Key.font: font],
                                          range: self.attrString.mutableString.range(of: subString))
            return self
        }
        
        func setColor(_ color: UIColor, forSubString subString: String) -> AttributedStringBuilder {
            self.attrString.addAttributes([NSMutableAttributedString.Key.foregroundColor: color],
                                          range: self.attrString.mutableString.range(of: subString))
            return self
        }
        
        func withStrikeThrough() -> AttributedStringBuilder {
            self.attrString.addAttribute(NSAttributedString.Key.strikethroughStyle, value: 2,
                                         range: NSMakeRange(0, self.attrString.length))
            return self
        }
    }
    
    

    Sử dụng

    
    let atrString = AttributedStringBuilder(string: title)
                .setFont(.boldSystemFont(ofSize: 13), forSubString: title)
                .setColor(.red, forSubString: title)
                .withStrikeThrough()
                .build()
    
    titleLabel.attributedText = attrString
    
    

    Như vậy, với trường hợp trên, việc sử dụng Builder Pattern sẽ giúp những dòng code của chúng ta trở nên dễ nhìn hơn và thuận lợi trong việc phát triển tiếp những properties của nó.

    Tổng kết

    Tóm lại, Design pattern: Builder Pattern là một trong những pattern dễ thực hiện. Nhưng khi sử dụng pattern này chúng ta cần phải mất thời gian để xác định được trường hợp nào cần nó, nó có thể trở nên rườm rà hơn khi sử dụng trong Swift. Nhưng chúng ta hãy đọc, tìm hiểu để áp dụng những tư duy và ý tưởng của một pattern vào một bài toán phù hợp. Đầu tư thời gian vào việc chuẩn bị xây dựng một sản phẩm sẽ luôn sẽ giúp việc bảo trì code trở nên dễ dàng hơn.

  • Coding convention – Những điều cần biết trước khi bắt tay vào code (Part 1)

    Coding convention – Những điều cần biết trước khi bắt tay vào code (Part 1)

    Table of contents

    • Đặt tên biến
    • Đặt tên hàm
    • Đặt tên class, struct, enum, protocol
    • Spacing
    • Comment
    • Access Control
    • Self & Closure

    Đặt tên biến

    • Hai quy tắc cơ bản nhất khi đặt tên biến đó là: sử dụng tiếng Anh thay vì tiếng Việt, sử dụng lowerCamelCase (kiểu lạc đà) thay vì snake_case

    Not Preferred

     private let height_normal_avatar: CGFloat = 60.0
     private let chieuRongNormalAvatar: CGFloat = 120.0
    

    Preferred

     private let heightNormalAvatar: CGFloat = 60.0
     private let widthNormalAvatar: CGFloat = 120.0
    
    • Khi đặt tên biến, hãy chú trọng đến sự rõ ràng, rành mạch hơn là sự ngắn gọn. Cố gắng làm sao khi đọc tên biến lên, ta có thể tưởng tượng được ngay biến đó có nhiệm vụ gì hoặc đang ám chỉ đến đối tượng nào. Vì vậy khi đặt tên biến không nên viết tắt và cũng không nên đặt tên giống với các đối tượng của hệ thống

    Not Preferred

    @IBOutlet private weak var tableView: UITableView!
    @IBOutlet private weak var imgAvatar: UIImageView!
    @IBOutlet private weak var lblbName: UILabel!
    
    private var bool: Bool = false
    

    Preferred

    @IBOutlet private weak var salaryTableView: UITableView!
    @IBOutlet private weak var avatarImageView: UIImageView!
    @IBOutlet private weak var nameLabel: UILabel!
    
    private var isLoadingState: Bool = false
    
    • Tên biến nên được bắt đầu bằng 1 danh từ và khi khai báo biến, nên khai báo luôn kiểu dữ liệu của biến đó (điều này có thể làm giảm được phần nào thời gian compile của app)

    Not Preferred

    private var dataSalaryArray = [Salary]()
    private var isLoadingState = false
    

    Preferred

    private var dataSalaryArray: [Salary] = [Salary]()
    private var isLoadingState: Bool = false
    
    • Với những biến cùng kiểu, nên đặt tên có sự thống nhất từ trên xuống dưới, tránh tình trạng mỗi biến 1 style đặt tên khác nhau. Ví dụ trong trường hợp với height và top constraint của đối tượng avatarImageView

    Not Preferred

    @IBOutlet private weak var heightOfAvatarImageView: NSLayoutConstraint!
    @IBOutlet private weak var topConstraintForAvatarImageView: NSLayoutConstraint!
    

    Preferred

    @IBOutlet private weak var heightConstraintAvatarImageView: NSLayoutConstraint!
    @IBOutlet private weak var topConstraintAvatarImageView: NSLayoutConstraint!
    

    Đặt tên hàm

    • Cũng giống như việc đặt tên biến, việc đặt tên hàm cũng có các quy tắc tương tự: dùng tiếng Anh, dùng kiểu lowerCamelCase (kiểu lạc đà)
    • Tên hàm thường được bắt đầu bằng động từ, tên hàm phải rõ ràng, rành mạch. Cố gắng làm sao khi đọc tên hàm lên, ta có thể tưởng tượng được ngay hàm đó làm nhiệm vụ gì.
    • Đối với những function có nhiều param, nên đặt mỗi param trên 1 dòng và căn lề cho chúng. Ngoài ra, với những param có giá trị mặc định, nên đặt chúng ở cuối list parameter. Còn những param không có giá trị mặc định thì nên đặt lên đầu

    Not Preferred

    func setAttributedString(string: String, font: UIFont, lineSpacing: CGFloat, alignment: NSTextAlignment = .left, icon: UIImage? = nil, íconRect: CGRect? = nil) -> NSAttributedString {
        // Do something
    }
    

    Preferred

    func setAttributedString(string: String, 
                            font: UIFont, 
                            lineSpacing: CGFloat, 
                            lignment: NSTextAlignment = .left, 
                            icon: UIImage? = nil, 
                            íconRect: CGRect? = nil) -> NSAttributedString {
        // Do something
    }
    
    • Bên cạnh việc sử dụng parameter 1 cách truyền thống, ta có thể sử dụng thêm specifying argument labels (thêm 1 label vào trước tên của param) hoặc omitting argument labels (thêm dấu gạch dưới _ vào trước tên của param). Điều này làm cho việc gọi tên hàm sẽ trở nên gần gũi hơn với ngôn ngữ tự nhiên. (Tham khảo thêm tại Function)

    Preferred

    func greet(person: String, from hometown: String) -> String {
        return "Hello \(person)!  Glad you could visit from \(hometown)."
    }
    
    print(greet(person: "Bill", from: "Cupertino"))
    // Prints "Hello Bill!  Glad you could visit from Cupertino."
    

    Preferred

    func welcome(_ person: String, from hometown: String) -> String {
        return "Hello \(person)!  Glad you could visit from \(hometown)."
    }
    
    print(welcome("Bill", from: "Cupertino"))
    // Prints "Hello Bill!  Glad you could visit from Cupertino."
    

    Đặt tên class, struct, enum, protocol

    • Đặt tên class, struct, enum và protocol ta cũng sử dụng tiếng Anh nhưng sẽ dùng kiểu UpperCamelCase – đây là điểm khác biệt so với function và property
    • Tên class, struct, enum, protocol thường được bắt đầu bằng danh từ và khi đặt tên cũng cần ưu tiên sự rõ ràng, rành mạch.

    Spacing

    • Các dấu ngoặc nhọn mở đầu cho các function và các dấu ngoặc sau các biểu thức if/else/switch/while… đều phải được mở trên cùng 1 dòng với câu lệnh, có thêm 1 khoảng trắng phía bên trái và đóng trên 1 dòng khác

    Not Preferred

    if user.isHappy
    {
      // Do something
    }
    else {
      // Do something else
    }
    

    Preferred

    if user.isHappy {
      // Do something
    } else {
      // Do something else
    }
    
    • Nên có 1 dòng trắng giữa các function, giữa các block code và giữa khu vực khai báo các properties với khu vực ánh xạ các outlet. Trong 1 function cũng cần có những dòng trắng để phân tách các chức năng nhỏ trong function đó.

    Not Preferred

    import UIKit
    class SettingScreen: UIViewController {
        @IBOutlet private weak var settingTitleLabel: UILabel!
        @IBOutlet private weak var stateSettingSwitch: UISwitch!
        var snapView: UIView?
        var snapTabbarView: UIView?
        override func viewDidLoad() {
            super.viewDidLoad()
        }
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
        }
    }
    extension SettingScreen {
        func loadData(state: String) {
            DispatchQueue.main.async {
                self.settingTitleLabel.text = state
            }
        }
    }
    

    Preferred

    import UIKit
    
    class SettingScreen: UIViewController {
    
        @IBOutlet private weak var settingTitleLabel: UILabel!
        @IBOutlet private weak var stateSettingSwitch: UISwitch!
    
        var snapView: UIView?
        var snapTabbarView: UIView?
    
        override func viewDidLoad() {
            super.viewDidLoad()
        }
    
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
        }
    }
    
    extension SettingScreen {
    
        func loadData(state: String) {
            DispatchQueue.main.async {
                self.settingTitleLabel.text = state
            }
        }
    }
    
    • Dấu 2 chấm luôn không có khoảng trắng ở phía bên trái và có 1 khoảng trắng ở phía bên phải. Ngoại trừ 3 trường hợp: toán tử 3 ngôi A ? B : C, empty dictionary [:] và #selector syntax addTarget(_:action:)

    Not Preferred

     class ViewController : UIViewController {
         private var data :[String:CGFloat] = ["A" : 1.2, "B":3.2]
     }
    

    Preferred

     class ViewController: UIViewController {
         private var data: [String: CGFloat] = ["A": 1.2, "B": 3.2]
     }
    
    • Không nên có khoảng trắng ở cuối mỗi dòng code nhưng nên có thêm 1 dòng trắng ở cuối mỗi file
    • Không có giới hạn nhất định cho số ký tự trên mỗi dòng code. Tuy nhiên mỗi dòng code của bạn không nên có quá 100 ký tự. Có 1 vài cách để làm giảm số lượng ký tự trên 1 dòng code:
      • Đối với function có nhiều param và các param có tên quá dài, bạn có thể xuống dòng và căn lề cho chúng (đã để cập ở trên)
      • Đối với các biểu thức tính toán, bạn có thể đặt ra các biến phụ thay vì gộp chung lại vào 1 biểu thức

    Not Preferred

    // Tính diện tích tam giác bất kỳ khi biết độ dài 3 cạnh (hệ thức Heron)
    func calculateSquareTriangleUsingHeron(firstEdge: CGFloat,
                                           secondEdge: CGFloat,
                                           thirdEdge: CGFloat) -> CGFloat {
        return sqrt(((firstEdge + secondEdge + thirdEdge) / 2) * ((firstEdge + secondEdge + thirdEdge) / 2 - firstEdge) * ((firstEdge + secondEdge + thirdEdge) / 2 - secondEdge) * ((firstEdge + secondEdge + thirdEdge) / 2 - thirdEdge))
    }
    

    Preferred

    // Tính diện tích tam giác bất kỳ khi biết độ dài 3 cạnh (hệ thức Heron)
    func calculateSquareTriangleUsingHeron(firstEdge: CGFloat,
                                           secondEdge: CGFloat,
                                           thirdEdge: CGFloat) -> CGFloat {
        let halfPerimeter: CGFloat = (firstEdge + secondEdge + thirdEdge) / 2
        let halfPerimeterMinusA: CGFloat = halfPerimeter - firstEdge
        let halfPerimeterMinusB: CGFloat = halfPerimeter - secondEdge
        let halfPerimeterMinusC: CGFloat = halfPerimeter - thirdEdge
        let doubleSquare: CGFloat = halfPerimeter * halfPerimeterMinusA * halfPerimeterMinusB * halfPerimeterMinusC
        let square: CGFloat = sqrt(doubleSquare)
        return square
    }
    

    Comment

    • Đôi khi ta cần phải add comment để chú thích cho các đoạn code, phục vụ cho quá trình maintenance sau này. Tất nhiên là khi code được thay đổi thì comment cũng cần được update theo.
    • Khi comment, không nên dùng C-style /*…*/ mà nên dùng double-slash // hoặc triple-slash ///. Cũng không nên để code và comment xuất hiện trên cùng 1 dòng

    Access Control

    • Các function và property nên mặc định để là private hoặc fileprivate để đảm bảo tính đóng gói trong lập trình. Nên hạn chế việc sử dụng open, public hoặc internal. Tham khảo thêm tại Access Control

    Not Preferred

    class SalaryCell: UITableViewCell {
        @IBOutlet weak var monthLabel: UILabel!
        @IBOutlet weak var incomeLabel: UILabel!
        
        override func awakeFromNib() {
            super.awakeFromNib()
        }
    }
    
    extension ViewController: UITableViewDataSource {
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return dataSalaryArray.count
        }
        
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            guard let cell = salaryTableView.dequeueReusableCell(withIdentifier: "SalaryCell",
                                                                for: indexPath) as? SalaryCell else {
                return UITableViewCell()
            }
            cell.monthLabel.text = dataSalaryArray[indexPath.row].month
            cell.incomeLabel.text = dataSalaryArray[indexPath.row].incomeLabel
            return cell
        }
    }
    

    Preferred

    class SalaryCell: UITableViewCell {
        @IBOutlet private weak var monthLabel: UILabel!
        @IBOutlet private weak var incomeLabel: UILabel!
        
        override func awakeFromNib() {
            super.awakeFromNib()
        }
        
        func setupData(data: Salary) {
            monthLabel.text = data.month
            incomeLabel.text = "\(data.income)"
        }
    }
    
    extension ViewController: UITableViewDataSource {
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return dataSalaryArray.count
        }
        
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            guard let cell = salaryTableView.dequeueReusableCell(withIdentifier: "SalaryCell",
                                                                for: indexPath) as? SalaryCell else {
                return UITableViewCell()
            }
            cell.setupData(data: dataSalaryArray[indexPath.row])
            return cell
        }
    }
    
    • Khi khai báo property, các từ khoá liên quan đến access control nên được đặt lên đầu. Chỉ có 1 số từ khoá được đứng trước chúng đó là: static, @IBAction, @IBOutlet, @discardableResult.

    Not Preferred

    @IBOutlet weak private var salaryTableView: UITableView!
    @IBOutlet weak private var fullNameLabel: UILabel!
    

    Preferred

    @IBOutlet private weak var salaryTableView: UITableView!
    @IBOutlet private weak var fullNameLabel: UILabel!
    

    Self & Closure

    • Không nên sử dụng từ khoá self một cách tuỳ ý. Chỉ dùng self trong 2 trường hợp sau:
      • Khi trình biên dịch yêu cầu, thường là khi đang trong biểu thức closure
      func loadData() {
          DispatchQueue.main.async {
              self.salaryTableView.reloadData()
          }
      }
      
      • Khi đang ở trong hàm init, ta cần phân biệt giữa property của object và param của hàm init
      class Salary {
          var income: Int
          var month: String
      
          init(income: Int, month: String) {
              self.income = income
              self.month = month
          }
      }
      
    • Đối với closure, ta có thể dùng trailling closure syntax trong trường hợp chỉ có duy nhất 1 biều thức closure trong list parameter. Còn nếu có nhiều hơn 1, ta phải giữ lại tên cho các closure đó.

    Not Preferred

    // Trong trường hợp này, chỉ có duy nhất 1 biểu thức closure nên không cần thiết phải để lại label "animations"
    UIView.animate(withDuration: 1, animations: {
        self.avatarImageView.alpha = 0.0
    })
            
    // Trường hợp này có 2 biểu thức closure, vì vậy nên để lại cả 2 label "animations" và "completion" để phân biệt chúng với nhau
    UIView.animate(withDuration: 1) {
        self.avatarImageView.alpha = 0.0
    } completion: { (_) in
        self.avatarImageView.removeFromSuperview()
    }
    

    Preferred

    UIView.animate(withDuration: 1) {
        self.avatarImageView.alpha = 0.0
    }
            
    UIView.animate(withDuration: 1,
                    animations: {
                        self.avatarImageView.alpha = 0.0
                    }, completion: { _ in
                        self.avatarImageView.removeFromSuperview()
                    })
    

    To be continue…

  • Coding convention – Những điều cần biết trước khi bắt tay vào code (Part 2)

    Coding convention – Những điều cần biết trước khi bắt tay vào code (Part 2)

    Table of contents

    • Magic number & Duplicate code
    • Code Organization
    • Scene Delegate
    • Computed Property
    • Optional
    • Multi-line String
    • Bonus

    Magic number & Duplicate code

    • Khi code ta không nên dùng những con số vô định, hay còn gọi là magic number, gây khó hiểu cho người khác. Điều này sẽ ảnh hưởng đến quá trình maintain sau này. Ta có thể thay thế những con số magic này bằng cách tạo ra các constant với tên gọi clear nhất có thể, làm sao để khi người khác đọc code của bạn, họ cũng có thể hiểu được vì sao bạn lại dùng đến con số đó. Hoặc ít nhất trước khi dùng magic number, ta phải thêm comment để giải thích lý do tại sao sử dụng chúng.
    • Ta cũng không nên để những đoạn code giống nhau được lặp đi lặp lại trong source code của mình. Nếu nhận thấy có những đoạn code cùng thực hiện một chức năng nhất định, hoặc cùng được apply cho 1 đối tượng nhất định, ta có thể nghĩ đến việc grouping chúng lại thành các function để tiện cho việc implement cũng như maintain sau này.

    Not Preferred

    class SignUpViewController: UIViewController {
    
        @IBOutlet private weak var firstNameTextField: UITextField!
        @IBOutlet private weak var lastNameTextField: UITextField!
        @IBOutlet private weak var accountTextField: UITextField!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            // Có thể thấy đoạn code này đang set borderWidth, borderColor, cornerRadius cho
            // lần lượt 3 TextField khác nhau. Tưởng tượng nếu sau này màn hình được update
            // thêm nhiều textfield khác nữa, hoặc trong app cũng có nhiều textfield cần phải 
            // setup các thuộc tính tương tự như trên. Mỗi textfield cần 3 dòng code x số lượng
            // textfield cả app = ...
            // -> Ta nghĩ đến việc tạo ra các function common để dùng chung cho các đối tượng 
            // UITextField, sẽ thuận tiện hơn cho việc implement và maintain sau này, sửa 1 hàm 
            // có thể apply được toàn bộ
            firstNameTextField.layer.borderColor = UIColor.orange.cgColor
            firstNameTextField.layer.borderWidth = 1.0
            firstNameTextField.layer.cornerRadius = 25
            
            lastNameTextField.layer.borderColor = UIColor.orange.cgColor
            lastNameTextField.layer.borderWidth = 1.0
            lastNameTextField.layer.cornerRadius = 25
            
            accountTextField.layer.borderColor = UIColor.orange.cgColor
            accountTextField.layer.borderWidth = 1.0
            accountTextField.layer.cornerRadius = 25
        }
    }
    

    Preferred

    class SignUpViewController: UIViewController {
    
       @IBOutlet private weak var firstNameTextField: UITextField!
       @IBOutlet private weak var lastNameTextField: UITextField!
       @IBOutlet  weak var accountTextField: UITextField!
       
       override func viewDidLoad() {
           super.viewDidLoad()
           
           firstNameTextField.setupLayer()
           lastNameTextField.setupLayer()
           accountTextField.setupLayer()
       }
    }
    // Phần extension cho các compoment như UILabel, UITextField, UIButton, ... thường 
    // được tách ra thành các file riêng. Xem thêm phần Code Organization
    extension UITextField {
       func setupLayer(borderWidth: CGFloat = 1.0,
                       borderColor: CGColor = UIColor.orange.cgColor,
                       cornerRadius: CGFloat = 25) {
           layer.borderWidth = borderWidth
           layer.borderColor = borderColor
           layer.cornerRadius = cornerRadius
       }
    }

    Code Organization

    • Ta không nên gộp tất cả các property và các function vào trong một block code ở trong một file. Vì đối với những file có số lượng dòng code lớn, việc làm này sẽ khiến ta khó hình dung được cấu trúc tổ chức của file và mục đích sử dụng của các function trong file đó. Ta nên chia nhỏ file thành nhiều block code, mỗi block code giải quyết một nhiệm vụ khác nhau và chứa các function liên quan tới nhiệm vụ đó. Ta có thể thêm từ khoá // MARK: – dosomething vào trên mỗi block code

    Not Preferred

    // 1 block code không nên adopt quá nhiều protocol, delegate như vậy
    class HomeViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    
        @IBOutlet private weak var salaryTableView: UITableView!
        @IBOutlet private weak var avatarImageView: UIImageView!
        @IBOutlet private weak var widthConstraintAvatarImageView: NSLayoutConstraint!
        
        private var dataSalaryArray: [Salary] = [Salary(income: 123456, month: "1/2021"),
                                                Salary(income: 654321, month: "2/2021")]
        
        override func viewDidLoad() {
            super.viewDidLoad()
            salaryTableView.delegate = self
            salaryTableView.dataSource = self
            salaryTableView.register(UINib(nibName: "SalaryCell", bundle: nil), forCellReuseIdentifier: "SalaryCell")
        }
        
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return dataSalaryArray.count
        }
        
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            guard let cell: SalaryCell = salaryTableView.dequeueReusableCell(withIdentifier: "SalaryCell",
                                                                             for: indexPath) as? SalaryCell else {
                return UITableViewCell()
            }
            cell.setupData(data: dataSalaryArray[indexPath.row])
            return cell
        }
        
        func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            
        }
    
        func loadData() {
            // Do something
        }
    }
    

    Preferred

    class HomeViewController: UIViewController {
    
       // MARK: - Outlet
       @IBOutlet private weak var salaryTableView: UITableView!
       @IBOutlet private weak var avatarImageView: UIImageView!
       @IBOutlet private weak var widthConstraintAvatarImageView: NSLayoutConstraint!
       
       // MARK: - Property
       private var dataSalaryArray: [Salary] = [Salary(income: 123456, month: "1/2021"),
                                               Salary(income: 654321, month: "2/2021")]
       
       override func viewDidLoad() {
           super.viewDidLoad()
           salaryTableView.delegate = self
           salaryTableView.dataSource = self
           salaryTableView.register(UINib(nibName: "SalaryCell", bundle: nil), forCellReuseIdentifier: "SalaryCell")
       }
    }
    
    // MARK: - TableView DataSource
    extension HomeViewController: UITableViewDataSource {
       func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
           return dataSalaryArray.count
       }
       
       func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
           guard let cell: SalaryCell = salaryTableView.dequeueReusableCell(withIdentifier: "SalaryCell",
                                                                            for: indexPath) as? SalaryCell else {
               return UITableViewCell()
           }
           cell.setupData(data: dataSalaryArray[indexPath.row])
           return cell
       }
    }
    
    // MARK: - TableView Delegate
    extension HomeViewController: UITableViewDelegate {
       func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
           
       }
    }
    
    // MARK: - Do something
    extension HomeViewController {
       func loadData() {
           // Do something
       }
    }
    
    • Khi thêm từ khoá // MARK: – dosomething vào trước các block code, Xcode sẽ tự gen cho ta đường line phân cách giữa các block đó. Thêm vào đó, khi tap vào thanh công cụ như trên ảnh, ta cũng có thể overview được trong file đang có những block code nào, các block đảm nhận nhiệm vụ gì, các function liên quan đến nhiệm vụ đó. Việc này rất có ích khi file của bạn có số lượng dòng code lớn và có nhiều người cùng phát triển

    • Tương tự, ta cũng nên chia project thành nhiều các folder, mỗi folder có chức năng riêng và chứa các file liên quan đến chức năng đó.
    Preferred Not Preferred

    Scene Delegate

    • Khi tạo một project mới trong Xcode, hệ thống sẽ mặc định cho rằng app của bạn sẽ chỉ support cho version iOS cao nhất mà bản Xcode đó support trở lên (ví dụ: Xcode 11.3 sẽ support cho version iOS 13.6 trở lên, Xcode 12.0 sẽ support cho version iOS 14.0 trở lên, Xcode 12.5 là iOS 14.5 trở lên, …).
    • Từ Xcode 11.0 trở về trước, khi tạo mới 1 project, sẽ có 1 vài file mặc định được tạo như: ViewController.swift, Main.storyboard, AppDelegate.swift, Info.plist… Từ Xcode 11.0 trở lên, để phục vụ cho iOS 13, ngoài những file mặc định vừa liệt kê ở trên, còn có thêm 1 file nữa là SceneDelegate.swift (Tìm hiểu thêm tại đây).

    • Vì vậy, nếu muốn project có thể được build trên device (hoặc simulator) chạy version < iOS 13, ta sẽ phải tiến hành xoá file SceneDelegate.swift và thực hiện 1 vài config như sau.
    • Ngoại trừ trường hợp là bạn muốn làm việc với SwiftUI, việc xoá file SceneDelegate.swift và giảm target version build của app dường như là 1 việc bắt buộc bởi 1 vài lý do chính sau:
      • Khi phát triển 1 ứng dụng, chắc chắn chúng ta đều muốn đông đảo người dùng có thể tiếp cận được với ứng dụng đó. Nhưng không phải người dùng nào cũng sẵn sàng update version iOS mới nhất cho device của họ. Vì vậy việc để target vesion build của app ở mức “phổ thông” như iOS 11.0, 12.0, … sẽ làm tăng tính thương mại cho app của bạn.
      • Về mặt technical, khi build app trên những device “thấp” hoặc trên những iOS version thấp, ta sẽ có cơ hội để test nhiều hơn. Vì thực tế sẽ có rất nhiều bug rất “dị”, chúng chỉ xảy ra trên những “môi trường” thấp mà không xảy ra ở trên “môi trường” cao, hoặc cũng có nhiều trường hợp ngược lại. Đối với 1 developer, ta cần phải đảm bảo những dòng code của ta phải chạy ngon trên nhiều môi trường khác nhau.
      • Cũng về mặt technical, khi bạn muốn chia sẻ source code của mình và người khác muốn clone về. Trong trường hợp bạn dùng bản Xcode mới nhất còn họ dùng bản Xcode thấp hơn, nếu bạn không giảm target version build thì người khác sẽ không thể build source của bạn được.

    Computed Property

    • Để cho ngắn gọn, nếu 1 computed property thuộc kiểu read-only, ta có thể bỏ qua mệnh đề get. Mệnh đề này chỉ cần thêm vào khi có thêm cả mệnh đề set

    Preferred

    var diameter: Double {
      return radius * 2
    }
    

    Not Preferred

    var diameter: Double {
      get {
        return radius * 2
      }
    }
    

    Optional

    • Khi truy cập giá trị optional, nếu giá trị đó chỉ được truy cập 1 lần hoặc có nhiều optional trong chuỗi, ta có thể dùng 1 chuỗi optional liên tiếp
    textContainer?.textLabel?.setNeedsDisplay()
    
    • Trong trường hợp giá trị optional được truy cập nhiều lần, nên sử dụng if…let để mở ra 1 block code rồi thao tác trong đó
    if let textContainer = textContainer {
      // do many things with textContainer
    }
    
    • Khi đặt tên cho các biến và các property optinal, không cần thiết phải đặt kiểu như optionalNameLabel hay couldAvatarImageView vì trạng thái optional (?) đã có trong khi khai báo biến rồi.
    • Khi unwrapp biến optional, cũng không cần thiết phải đặt các tên như unwrappedView hay realLabel mà hãy dùng chính tên gốc của biến đó

    Preferred

    var subview: UIView?
    var volume: Double?
    
    // later on...
    if let subview = subview, let volume = volume {
      // do something with unwrapped subview and volume
    }
    
    // another example
    resource.request().onComplete { [weak self] response in
      guard let self = self else { return }
      let model = self.updateModel(response)
      self.updateUI(model)
    }
    

    Not Preferred

    var optionalSubview: UIView?
    var volume: Double?
    
    if let unwrappedSubview = optionalSubview {
      if let realVolume = volume {
        // do something with unwrappedSubview and realVolume
      }
    }
    
    // another example
    UIView.animate(withDuration: 2.0) { [weak self] in
      guard let strongSelf = self else { return }
      strongSelf.alpha = 1.0
    }
    
    • Nếu có nhiều biến optional được unwrapp với guard let hoặc if…let, hãy ghép chúng lại với nhau thành 1 câu lệnh để giảm thiếu việc lồng điều kiện. Khi ghép, hãy đặt guard trên 1 dòng riêng, đặt các điều kiện trên từng dòng riêng và thụt lề cho chúng, cuối cùng mệnh đề else được căn lề thẳng với guard

    Preferred

    guard 
      let number1 = number1,
      let number2 = number2,
      let number3 = number3 
    else {
      fatalError("impossible")
    }
    // do something with numbers
    

    Not Preferred

    if let number1 = number1 {
      if let number2 = number2 {
        if let number3 = number3 {
          // do something with numbers
        } else {
          fatalError("impossible")
        }
      } else {
        fatalError("impossible")
      }
    } else {
      fatalError("impossible")
    }
    
    
    

    Multi-line String

    • Khi muốn viết 1 văn bản dài nhiều dòng, nên sử dụng cú pháp Multi-line String Literal. Đó là mở văn trên 1 dòng rồi bắt đầu văn bản từ dòng thứ 2 trở đi, kết hợp với việc thụt lè và căn lề thằng các dòng tiêp theo

    Preferred

    let message: String = """
      You cannot charge the flux \
      capacitor with a 9V battery.
      You must use a super-charger \
      which costs 10 credits. You currently \
      have \(credits) credits available.
      """
    

    Not Preferred

    let message: String = """You cannot charge the flux \
      capacitor with a 9V battery.
      You must use a super-charger \
      which costs 10 credits. You currently \
      have \(credits) credits available.
      """
    

    Bonus

    • Không giống như 1 số ngôn ngữ khác (C/C++, JavaScript, …), Swift không yêu cầu phải có dấu chấm phảy ở cuối mỗi dòng code

    Preferred

    let swift = "not a scripting language"
    

    Not Preferred

    let swift = "not a scripting language";
    
    • Dấu ngoặc đơn bao quanh các điều kiện là không bắt buộc và nên được bỏ qua

    Preferred

    if name == "Hello" {
      print("World")
    }
    

    Not Preferred

    if (name == "Hello") {
      print("World")
    }
    
    • Tuy nhiên với những biểu thức phức tạp, có dấu ngoặc đơn bao quanh sẽ làm code trở nên clear hơn

    Preferred

    let playerMark: String = (player == current) ? "X" : "O"

    • Ngoài ra, còn 1 lỗi nữa mà người viết thấy đa phần các bạn newbie rất hay mắc phải. Tuy rằng lỗi này liên quan đến logic nhưng cũng xin liệt kê vào đây để anh em newbie tiện theo dõi. Đó là khi ta muốn truy xuất các phần tử trong 1 collection data (set, dictionary, array, … và ở đây xin lấy ví dụ là array), việc đầu tiên ta cần làm là phải check xem array đó có phần tử hay không rồi mới tiến hành việc truy xuất. Dưới đây là 1 ví dụ:

    Preferred

    private func getDataFromPlistFile(_ name: String) -> Dictionary<String, AnyObject>? {
    if PlistManager().getPlist(withName: name)?.count == 0 {
    return nil
    }
    let data = PlistManager().getPlist(withName: name)?[0]
    return data
    }

    Chúng ta có thể viết clear hơn như sau (dĩ nhiên đây là cách được recommend):

    Preferred

    private func getDataFromPlistFile(_ name: String) -> Dictionary<String, AnyObject>? {
    if let data = PlistManager().getPlist(withName: name)?.first {
    return data
    }
    }

    Tuyệt đối không được truy xuất trực tiếp vào các phần tử của array khi chưa check điều kiện, vì ta không thể chắc chắn được là array đó luôn có phần tử hay là không. Điều này sẽ tiềm ẩn nguy cơ bug rất cao.

    Not Preferred

    private func getDataFromPlistFile(_ name: String) -> Dictionary<String, AnyObject>? {
    let data = PlistManager().getPlist(withName: name)?[0]
    return data
    }

    Reference