Month: May 2020

  • Dependency Injection trong iOS

    Dependency Injection trong iOS

    Trong lập trình OOP hay POP, thì luôn dc recommend code 1 cách tách biệt hệ thống thành những module sao cho chúng liên kết với nhau 1 cách lỏng lẻo, để những module đó có thể hoạt động 1 cách độc lập nhiều nhất có thể.
    Nếu các module liên kết quá chặt chẽ với nhau thì khi hệ thống cần nâng cấp, phát triển thì sẽ gặp nhiều vấn đề, ngoài ra việc maintain sau này hay việc viết unit test cũng gặp nhiều khó khăn.
    -> Dependency Injection (DI) là 1 design pattern để ngăn chặn sự phụ thuộc chặt chẽ này.

    Content

    • Ý tưởng của DI
    • Implement DI trong swift

    Ý tưởng của DI:

    Chữ D trong SOLID, là Dependency Inversion Principle. Nội dung của nguyên lý này được tóm gọn lại như sau:

    • Các module cấp cao không nên phụ thuộc vào các module cấp thấp.
    • Interface không nên phụ thuộc vào chi tiết, mà ngược lại.
    • Các class giao tiếp với nhau thông qua interface, chứ không phải implementation.

    Note:

    • Dependency Injection là 1 design pattern để code có thể tuân thủ nguyên lý Dependency Inversion.
    • DI là 1 kỹ thuật cho phép xóa bỏ sự phụ thuộc chặt chẽ giữa các module, làm cho ứng dụng dễ dàng hơn trong việc maintain, extend, debug và test bởi khi đó các module hoạt động độc lập.

    Implement DI trong swift:

    Trước hết hãy đến 1 ví dụ không sử dụng DI:

    Vì k sử dụng DI, ví dụ ở trên sẽ có những hạn chế như sau:
    Class Car bị phụ thuộc vào NormalEngine:

    • Trong trường hợp muốn nâng cấp bằng cách thay vào FastEngine, hay nhiều loại Engine khác thì sẽ gặp nhiều vấn đề,
    • Khi có thay đổi trong class NormalEngine, thì có thể sẽ ảnh hưởng trực tiếp đến class Car.
      Ex: Thêm nhiều thuộc tính cho NormalEngine thì constructor sẽ thay đổi.
    • Khó khăn trong việc viết unit test.

    -> Chúng ta sẽ áp dụng DI Design pattern để giải quyết điều này.

    Constructor Injection

    // 1
    protocol Engine {
        func start()
    }
    
    // 2
    class NormalEngine: Engine {
        func start() {
            print("Normal engine start running...")
        }
    }
    
    class FastEngine: Engine {
        func start() {
            print("Fast engine start running...")
        }
    }
    
    class Car {
        // 3
        var engine: Engine
        init(engine: Engine) {
            self.engine = engine
        }
    
        func run() {
            engine.start()
        }
    }
    
    // 4
    let car = Car(engine: NormalEngine())
    car.run()
    car.engine = FastEngine()
    car.run()
    1. Tạo 1 protocol(interface) để giao tiếp với class thay vì giao tiếp bằng implementation (nguyên lý Dependency Inversion).
    2. Các class NormalEngine, FastEngine implement protocol đó
    3. Khai báo 1 biến kiểu Engine trong class Car
    4. Khi khởi tạo class Car, chúng ta sẽ truyền 1 engine cụ thể vào thông qua constructor.
    Ưu điểmNhược điểm
    – Tính đóng gói cao
    – Chắc chắn rằng đối tượng đã được khởi tạo
    – Nếu giao tiếp với nhiều protocol thì sẽ làm cho constructor của class đó nhiều lên.
    – Đối với các ViewController, đặc biệt là ViewController được define trong storyboard thì sẽ không có hàm khởi tạo.

    Property Injection

    Property Injection được dùng để tránh nhược điểm của Constructor Injection,bằng cách không khởi tạo các dependencies trong constructor mà khởi tạo bằng cách set value cho property.

    class Car {
        var engine: Engine!
    
        func run() {
            engine.start()
        }
    }
    
    let car = Car()
    car.engine = NormalEngine()

    Ưu điểm:

    • Cho phép set dependencies vào thời điểm bạn muốn.

    Nhược điểm:

    • Tính đóng gói thấp.
    • Vì các dependency được khai báo theo kiểu !, nên nếu quên không set dependency trước khi sử dụng thì dễ gây ra crash; còn nếu khai báo dependency theo kiểu optional thì mỗi lần sử dụng phải unwrap.

    Interface Injection

    • Ở đây bạn tạo ra 1 hàm setter để set dependency.
    protocol HasEngine {
        func setEngine(dependency: Engine)
    }
    
    class Car: HasEngine {
        var engine: Engine!
    
        func setEngine(dependency: Engine) {
            self.engine = dependency
        }
    
        func run() {
            engine.start()
        }
    }
    
    let car = Car()
    car.setEngine(dependency: FastEngine())

    Method Injection

    • Nếu bạn chỉ cần sử dụng dependency để dùng 1 lần thì bạn không cần lưuu lại như là 1 biến. Bạn chỉ đơn giản là pass dependency như là 1 method param.
    class Car {
        func run(engine: Engine) {
            engine.start()
        }
    }
    let car = Car()
    car.run(engine: NormalEngine())

    Ps: Thật ra ví dụ này không phù hợp thực tế lắm bởi xe nào cũng cần động cơ :)) Nhưng để hiểu được thì thế là đủ hiểu r 😉

    Ambition Context:

    • Đây là 1 cách tránh DI nhưng chỉ nên được sử dụng cho các global dependency mà được chia sẻ với nhiều đối tượng khác.
    • Tuy nhiên bạn nên hạn chế sử dụng cách này, bởi nếu không quản lí đọc ghi 1 biến dùng chung 1 cách cẩn thận thì sẽ dễ dẫn tới Race Condition.
    class Car {
        static var engine: Engine = NormalEngine()
    
        func run() {
            Car.engine.start()
        }
    }
    
    let car = Car()
    car.run()

    Ưu điểm:

    • Dependency được sử dụng global.
    • Vẫn có thể thay đổi dependency trong quá trình sử dụng.

    Nhược điểm:

    • Tính đóng gói thấp.
    • Yêu cầu Thread safe.

    Kết luận:

    • Dependency Injection là 1 kỹ thuật mạnh mẽ để giúp viết code 1 cách clean và dễ maintain.
    • Ngoài ra có thể sử dụng các design pattern khác để tránh dependency như Factory, Service Locator hay Dependency Injection Container.
  • Basic CAShapeLayer iOS (P2)

    Basic CAShapeLayer iOS (P2)

    Phần 1 của loạt bài viết về CAShapeLayer đã nói về cách vẽ đường thẳng, các hình khối.
    Ở phần 2 này, mình sẽ nói về những attribute quan trọng, thường được sử dụng của CAShapeLaye.

    Content

    • Stroke
    • Fill Color
    • Line

    Stroke

    Stroke Color

    • strokeColor là thuộc tính để set màu cho đường line của 1 đường thẳng, hình khối.
    • Ex: Để vẽ 1 hình vuông có các đường thẳng màu xanh thì chỉ cần thêm dòng code dưới đây khi vẽ hình:
    shapeLayer.strokeColor = UIColor.blue.cgColor

    Code vẽ hình vuông:

    func createSquare() {
        let shapeLayer = CAShapeLayer()
            
        let path = UIBezierPath()
        path.move(to: CGPoint(x: 0.0, y: 0.0))
        path.addLine(to: CGPoint(x: 100.0, y: 0.0))
        path.addLine(to: CGPoint(x: 100.0, y: 100.0))
        path.addLine(to: CGPoint(x: 0.0, y: 100.0))
        path.close() // creating a line segment between the first point and current point
            
        shapeLayer.path = path.cgPath
        shapeLayer.fillColor = nil
        shapeLayer.strokeColor = UIColor.blue.cgColor
        layer.addSublayer(shapeLayer)
    }

    Stroke Start & Stroke End

    • Ok, vậy là bạn đã biết cách tạo 1 path và gán path đó cho shapeLayer.

    • Nếu coi việc vẽ 1 path từ đầu đến cuối là 100% công việc, vậy giả sử nếu bạn chỉ muốn vẽ được 40, 50 hay 60% của công việc đó?
      -> strokeStart và strokeEnd sinh ra là để giúp bạn làm điều đó.

    • strokeStart: Chạy trong khoảng 0 đến 1, giá trị default là 0, tức là sẽ vẽ từ đầu.

    • strokeEnd: Chạy trong khoảng 0 đến 1, giá trị default là 1, tức là sẽ vẽ cho đến cuối cùng.

    • Khi bắt đầu công việc vẽ thì sẽ bắt đầu từ strokeStart đến strokeEnd.

    Vẫn đoạn code vẽ hình vuông ở trên, giả sử bạn chỉ muốn vẽ từ đầu cho đến 70% công việc thì thêm dòng code sau:

    shapeLayer.strokeEnd = 0.7

    Còn nếu bạn muốn vẽ từ 30% công việc đến 100% công việc thì chỉ cần set strokeStart:

    shapeLayer.strokeStart = 0.3

    Bạn có thể kết hợp cả strokeStart và strokeEnd tùy ý.
    Ngoài ra, strokeStart và strokeEnd còn có thể để dùng để tạo animation. Phần animation này sẽ nói ở bài viết sau.

    Fill Color

    • fillColor là thuộc tính dùng để thay đổi màu cho phần bên trong của 1 hình khối.
    • Ở phần 1, mình đã có vẽ 1 hình tròn có đường line màu đỏ và phần bên trong không màu. Giờ thì hãy set màu cho phần bên trong hình tròn bằng cách thêm dòng code:
    shapeLayer.fillColor = UIColor.yellow.cgColor

    Code vẽ hình tròn:

    private func createCircle() {
        let shapeLayer = CAShapeLayer()
            
        shapeLayer.lineWidth = 2.0
        shapeLayer.fillColor = UIColor.yellow.cgColor
        shapeLayer.strokeColor = UIColor.red.cgColor
        let openCirclePath = UIBezierPath(arcCenter: CGPoint(x: 60.0, y: 60.0),
                                          radius: 60.0,
                                          startAngle: 0.0,
                                          endAngle: CGFloat.pi * 2,
                                          clockwise: true)
            
        shapeLayer.path = openCirclePath.cgPath
        layer.addSublayer(shapeLayer)
    }
    

    Kể cả đối với các hình không kín thì việc set fillColor vẫn cứ là OK nhé :

    Line

    Line Width

    Nghe cái tên thôi là đã biết attribute này để làm gì rồi 🙂 Đó là set width cho path dùng để vẽ đường thẳng, hình khối. Thêm dòng code sau vào đoạn code vẽ hình tròn:

    shapeLayer.lineWidth = 20.0

    Kết quả thu được:

    Line Cap

    thuộc tính lineCap sẽ quyết định xem điểm cuối của 1 open path được vẽ như thế nào: butt, round hay square.
    Hình dưới đây lần lượt là hình của shapeLayer sau khi set lineCap thành butt, round và square:

    Để set thuộc tính lineCap cho shapeLayer thì thêm dòng code:

    shapeLayer.lineCap = .round hoặc .butt hoặc .square

    Line Dash Pattern

    • Thuộc tính lineDashPattern giúp bạn vẽ những đường thẳng đứt đoạn chứ không phải là 1 đường thẳng liền mạch.
    • lineDashPattern là 1 mảng các số định nghĩa độ dài nét liền và nét đứt bạn muốn vẽ.
      Giả sử bạn muốn vẽ 1 đường thẳng đứt đọan như sau: Cứ vẽ được 1 nét dài 5 thì sẽ đứt đoạn 1 khoảng 15 thì set lineDashPattern như sau:
    shapeLayer.lineDashPattern = [5, 15]

    Hoặc nếu bạn muốn vẽ 1 nét dài 5 rồi đến 1 nét đứt 10, rồi vẽ 1 nét dài 15 và 1 nét đứt dài 20:

    shapeLayer.lineDashPattern = [5, 10, 15, 20]

    Cấu trúc lineDashPattern bạn cung cấp sẽ được lặp lại cho đến khi vẽ xong hình.

    Line Dash Phase

    "Line dash phase specifies how far into the dash pattern the line starts." – Apple

    • Có thể hiểu đơn giản là lineDashPhase là 1 thuộc tính sẽ cho bạn dịch điểm bắt đầu vẽ đi 1 khoảng bạn muốn. Độ dài của path sẽ không thay đổi.
    • lineDashPattern có default value = 0.
    let circleShapeLayer1 = ...
    circleShapeLayer1.lineWidth = 2.0
    circleShapeLayer1.lineDashPattern = [47.12]
    
    let circleShapeLayer2 = ...
    circleShapeLayer2.lineWidth = 2.0
    circleShapeLayer2.lineDashPattern = [47.12]
    circleShapeLayer2.lineDasePhase = 23.56
    
    let circleShapeLayer3 = ...
    circleShapeLayer3.lineWidth = 2.0
    circleShapeLayer3.lineDashPattern = [47.12]
    circleShapeLayer3.lineDasePhase = -23.56

    Ở bài viết tiếp theo, mình sẽ nói về animation của CAShapeLayer.

    refer: https://www.calayer.com/core-animation/2016/05/22/cashapelayer-in-depth.html