Author: Hoang Anh Tuan

  • MVVM with Swift P2

    MVVM with Swift P2

    Demo MVVM với RxSwift:

    Model Layer:

    struct Category {
        let id: Int
        var name: String
    }

    Class Service để thao tác với database:

    import Foundation
    
    protocol ICategoryService {
        func getCategoryById(id: Int) -> Category?
    }
    
    class CategoryService: ICategoryService {
        func getCategoryById(id: Int) -> Category? {
            let categories = [Category(id: 3, name: "Movies"),
                              Category(id: 4, name: "Books"),
                              Category(id: 5, name: "Computer")]
            
            let category = categories.filter({
                $0.id == id
            }).first
            
            return category
        }
    }

    ViewModel Layer

    import Foundation
    import RxSwift
    
    class HomeViewModel {
        var category: Category
        var service: ICategoryService
        
        var displayName: String {
            return transformName(category.name)
        }
        
        // 1
        var valueSubject: PublishSubject<String>
        
        init(category: Category, service: ICategoryService) {
            self.category = category
            self.service = service
            valueSubject = PublishSubject()
        }
        
        func transformName(_ name: String) -> String {
            let newName = name.enumerated().map { (index, character) -> String in
                if index % 2 == 0 {
                    return character.uppercased()
                } else {
                    return character.lowercased()
                }
            }
            
            return newName.joined()
        }
        
        // 2
        func getCategory(id: Int) {
            if let newCategory = service.getCategoryById(id: id) {
                self.category = newCategory
                valueSubject.onNext(transformName(category.name))
            }
        }
    }
    1. Khởi tạo 1 subject kiểu Publish Subject để phát ra các event khi giá trị của category’s name được thay đổi.
    2. ViewModel sẽ thông qua Model Layer để truy xuất đến database. Sau khi lấy được data cần thiết, thì valueSubject sẽ phát ra 1 event có giá trị là name đã được transform của category mới.

    View Layer:

    import UIKit
    import RxSwift
    
    class HomeView: UIViewController {
        @IBOutlet private weak var categoryNameLabel: UILabel!
        
        private var viewModel: HomeViewModel?
        private let disposeBag = DisposeBag()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            let category = Category(id: 1, name: "Hollywood")
            viewModel = HomeViewModel(category: category, service: CategoryService())
            // 1
            self.categoryNameLabel.text = viewModel?.displayName
            
            // 2
            viewModel?.valueSubject
                .skip(1)
                .subscribeOn(MainScheduler.instance)
                .subscribe(onNext: { (displayName) in
                    self.categoryNameLabel.text = displayName
                })
                .disposed(by: disposeBag)
        }
        
        @IBAction func didTapButtonChangeCategory(_ sender: Any) {
            // 3
            viewModel?.getCategory(id: 3)
        }
    }
    1. Set text cho label lần đầu khi khởi tạo category.
    2. Lắng nghe khi viewModel phát ra 1 value mới.
    3. Tap button để yêu cầu viewModel lấy ra 1 category có id = 3.

    Tác dụng của MVVM:

    • Tương tự như MVP, lợi ích đầu tiên của MVVM là tách biệt phần logic ra khỏi View. Từ đó dẫn đến dễ viết unit test, dễ maintain, …
    • Dễ dàng reuse các ViewModel.
    • Bằng việc tương tác với View thông qua cơ chế data binding, vì vậy không cần tạo thêm nhiều protocol, class…

    MVVM vs MVP:

    • Trong MVVM, vì ViewModel tương tác với View bằng data binding, vì vậy ViewModel không có 1 reference nào đến View. Từ đó ViewModel sẽ dễ dàng được reuse, dễ dàng viết test hơn so với MVP.
    Presenter và View là liên kết chặt với quan hệ 1:1 trong MVP
    Quan hệ giữa ViewModel và View trong MVVM
    • Trong MVVM, 1 View có thể có nhiều ViewModel.
    • MVVM sẽ không cần phải tạo nhiều protocol như MVP.
    • Việc sử dụng cơ chế data binding cũng dẫn đến 1 hệ quả là MVVM sẽ khó để debug hơn nhiều so với việc sử dụng MVP.
    • Nếu sử dụng RxSwift kết hợp với MVVM, thì sẽ phải đòi hỏi team của bạn đều phải biết RxSwift ở mức ổn.

    Kết luận:

    • Mục tiêu chính của MVVM là tách biệt logic khỏi View. Tuy nhiên, những phần logic như truy vấn database, networking, … thì chưa được xử lí. Những logic như vậy có thể được đặt ở presenter, hoặc tạo 1 class riêng ở Model tùy theo bản thân bạn.
      Tuy nhiên, nên tách biệt các phần logic Database, networking, … ra các class riêng để tuân thủ nguyên tắc Single Responsibility Principle của SOLID.
    • Theo nguyên tắc MVVM thì ViewModel không import UIKit để tách biệt logic và View.
  • MVVM with swift P1

    MVVM with swift P1

    Bài viết này, mình sẽ nói về MVVM.
    Bài viết khá dài, nên sẽ được chia thành 2 phần.

    Content:

    Phần 1:

    • Mô hình MVVM?
    • Demo:
      • MVVM with Closure/Callback
      • Optimize MVVM with Closure/Callback

    Phần 2:

    • Demo:
      • MVVM with RxSwift
    • Tác dụng của MVVM
    • MVVM vs MVP?
    • Kết luận

    MVVM là gì?

    MVVM gồm 3 phần:

    Model:

    Tương tự Model Layer của MVC, MVP.

    View:

    Tương tự so với View Layer của MVP.

    ViewModel:

    • Đây là phần khác biệt của MVVM so với MVP.
    • ViewModel nằm giữa Model và View, có tác dụng là cầu nối để giao tiếp giữa Model và View.
    • ViewModel còn có tác dụng xử lí các logic convert, format data trước khi View hiển thị data đó cho người dùng.
    • Ngoài ra, ViewModel cũng có thể chứa các logic như update database, xử lí networking, … .Tuy nhiên, nên tách các logic này ra thành các class khác để đảm bảo nguyên tắc Single Responsibility Principle.

    Note:

    • View không tương tác trực tiếp với Model. View chỉ có thể tương tác với Model thông qua ViewModel.
    • ViewModel không biết gì về View.
    • MVVM giao tiếp giữa ViewModel và View bằng cách Observer (thường được gọi là binding data).
    • ViewModel cũng không được import UIKit để tách biệt phần logic ra khỏi phần UI.

    Demo MVVM with Closure/Callback:

    Model Layer

    import Foundation
    
    struct Category {
        let id: Int
        var name: String
    }
    

    ViewModel Layer:

    Giờ thì hãy tạo 1 ViewModel để thực hiện các logic xử lí data trước khi View hiển thị cho người dùng:

    import Foundation
    
    class HomeViewModel {
        private var category: Category
        
        // 1
        var displayName: String {
            return transformName(category.name)
        }
        
        // 2
        var onSuccess: ((String) -> Void)?
        
        init(category: Category) {
            self.category = category
        }
        
        // 3
        func transformName(_ name: String) -> String {
            let newName = name.enumerated().map { (index, character) -> String in
                if index % 2 == 0 {
                    return character.uppercased()
                } else {
                    return character.lowercased()
                }
            }
            
            return newName.joined()
        }
        
        // 4
        func changeCategoryName(_ newName: String) {
            category.name = newName
            onSuccess?(transformName(newName))
        }
    }
    
    1. Thuộc tính displayName có giá trị là tên của category sau khi đã được transform.
    2. Tạo 1 closure để phát event.
    3. func thực hiện logic transform name trước khi hiển thị cho người dùng.
    4. func thực hiện update name cho category, sau khi update thành công thì phát ra event chứa giá trị mới.

    View Layer

    import UIKit
    
    class HomeView: UIViewController {
        @IBOutlet private weak var categoryNameLabel: UILabel!
        
        // 1
        private var viewModel: HomeViewModel?
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            // 2
            let category = Category(id: 1, name: "Hollywood")
            viewModel = HomeViewModel(category: category)
            categoryNameLabel.text = viewModel?.displayName
            
            // 3
            viewModel?.onSuccess = {
                self.categoryNameLabel.text = $0
            }
            
        }
    
        @IBAction func didTapButtonChangeCategory(_ sender: Any) {
            viewModel?.changeCategoryName("vietnam")
        }
    }
    1. Khai báo 1 viewModel cho View.
    2. Khởi tạo viewModel và hiển thị name sau khi transform lên giao diện để hiển thị cho người dùng.
    3. Thực hiện observer event được phát ra từ viewModel và update UI.

    Optimize Closure?

    Với cách sử dụng closure ở trên thì bạn đã tạo ra 1 binding giữa View và Model. Nhưng nếu có nhiều thuộc tính phải transform, thì chúng ta phải tạo ra nhiều closure ở ViewModel.
    Liệu có cách nào có thể optimize chúng không?

    Câu trả lời là sử dụng Generic. Tạo ra 1 class generic để tự động phát ra 1 event khi được set 1 value mới:

    class Binding<T> {
        typealias Listener = (T) -> Void
        var listener: Listener?
        
        var value: T {
            didSet {
                listener?(value)
            }
        }
        
        init(value: T) {
            self.value = value
        }
        
        func bind(listener: Listener?) {
            self.listener = listener
            self.listener?(value)
        }
    }

    ViewModel Layer khi đó sẽ được update lại như sau:

    class HomeViewModel {
        var category: Category
        // 1
        public var displayName: Binding<String>
        
        init(category: Category) {
            self.category = category
            
            let newName = category.name.enumerated().map { (index, character) -> String in
                if index % 2 == 0 {
                    return character.uppercased()
                } else {
                    return character.lowercased()
                }
            }
            self.displayName = Binding<String>(value: newName.joined())
        }
        
        func changeCategoryName(_ newName: String) {
            category.name = newName
            // 1
            displayName.value = transformName(newName)
        }
        
        func transformName(_ name: String) -> String {
            let newName = name.enumerated().map { (index, character) -> String in
                if index % 2 == 0 {
                    return character.uppercased()
                } else {
                    return character.lowercased()
                }
            }
            
            return newName.joined()
        }
    }
    1. displayName được sửa lại thành kiểu Binding<String>
    2. Mỗi khi thay đổi tên, chỉ cần set lại value cho displayName, thì displayName sẽ tự động phát ra 1 event chứa newName.

    View Layer sau khi optimize sẽ trở thành:

    class HomeView: UIViewController {
        @IBOutlet private weak var categoryNameLabel: UILabel!
        private var viewModel: HomeViewModel?
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            let category = Category(id: 1, name: "Hollywood")
            viewModel = HomeViewModel(category: category)
            
            // 1
            viewModel?.displayName.bind(listener: {
                self.categoryNameLabel.text = $0
            })
            
        }
    
        @IBAction func didTapButtonChangeCategory(_ sender: Any) {
            viewModel?.changeCategoryName("vietnam")
        }
    }
    1. Thực hiện observer event được phát ra từ displayname của ViewModel và thực hiện update UI với giá trị mới nhận được.

    Note: Để tránh bài viết quá dài, thì mình sẽ không add ảnh kết quả run demo vào đây.

    Kết luận:

    Qua phần 1, có thể thấy 1 lợi ích của MVVM so với MVP là không cần tạo delegate để giao tiếp.
    Phần 1 sẽ chỉ dừng ở đây thôi, mình sẽ cùng tìm hiểu rõ hơn MVVM ở phần 2.

  • MVP with Swift

    MVP with Swift

    Bài viết này, mình sẽ trình bày về MVP.

    Content

    • Tổng quan về MVC
    • Những vấn đề của MVC
    • Ý tưởng của MVP
    • Áp dụng MVP

    Tổng quan về MVC:

    Chắc hẳn các bạn đã quen với MVC: 1 trong những architecture pattern phổ biến nhất.
    MVC bao gồm 3 phần chính:

    Model

    Model Layer là nơi lưu trữ data của bạn.
    Model layer bao gồm:

    • Model Objects (hiển nhiên rồi).
    • Ngoài ra còn có 1 vài class và object khác có thể trong Model như các class xử lí Network code, Persistence code(Lưu trữ data đến database, CoreData,…), Parsing Code(parsing network response to model,…), các class Helper, Extensions, …

    View

    Là nơi hiển thị giao diện cho người dùng.
    VD: Xib file, UIView class, Core Animation, … Nói chung là những thứ liênn quan đến UIKit

    Controller

    Là phần tương tác với View và Model. Controller nhận action/events từ view để update Model và View.

    Những điều cần chú í ở MVC:

    • View chỉ dùng để update UI, không chứa các logic.
    • View không được tương tác trực tiếp với Model.
    • View không được làm bất cứ điều gì mà không liên quan đến chính nó.

    Vấn đề của MVC?:

    • Controller chứa rất nhiều logic như update UI, xử lí các logic user action, logic networking, … tuy điều này có thể được giảm thiểu bởi tách các logic như Network, Lưu database,… sang các class khác.
    • Khó test các logic của controller bởi controller liên kết chặt chẽ với View.
    • Dự án càng lớn, code controller càng nhiều, dẫn đến khó maintain, bảo trì.

    Ý tưởng của MVP:

    MVP gồm 3 phần:

    View:

    Bao gồm Views và View Controller, đùng để tương tác, xử lí UI và nhận các event của user.

    Presenter:

    Chịu trách nhiệm xử lí logic, gồm các logic xử lí user action, logic networking, tương tác database…
    Các event của user sẽ được gửi đến Presenter để Presenter xử lí logic tương ứng, sau đó sẽ tương tác, yêu cầu View update UI bằng cách sử dụng delegate.

    Model:

    Tương tự như MVC.

    Note: Tầng Presenter phải không phụ thuộc vào UIKit, để tách biệt với View. Qua đó gíup dễ viết test cho phần logic hơn.

    Giờ hãy bắt tay vào demo:

    Demo:

    Model Entity:

    import Foundation
    
    struct Category {
        let id: Int
        let name: String
    }
    

    Class Service chịu trách nhiệm tương tác với Model:

    import Foundation
    
    protocol ICategoryService {
        func getCategoryById(id: Int) -> Category?
    }
    
    class CategoryService: ICategoryService {
        func getCategoryById(id: Int) -> Category? {
            let categories = [Category(id: 1, name: "Movies"),
                              Category(id: 2, name: "Books"),
                              Category(id: 3, name: "Computer")]
            
            let category = categories.filter({
                $0.id == id
            }).first
            
            return category
        }
    }

    Class Presenter: Chịu trách nhiệm xử lí logic

    import Foundation
    
    protocol IHomeViewDelegate: NSObjectProtocol {
        func updateUI(_ categoryName: String)
    }
    
    class HomeViewPresenter {
        // 1
        weak var delegate: IHomeViewDelegate?
        let categoryService: ICategoryService!
        
        init(view: IHomeViewDelegate, service: ICategoryService) {
            delegate = view
            categoryService = service
        }
        
        func searchCategoryById(id: Int) {
            let result = categoryService.getCategoryById(id: id)
            if let categoryName = result?.name {
                // 2
               delegate?.updateUI(categoryName)
            }
        }
    }
    1. Khai báo delegate để presenter có thể dùng để tương tác với View.
      Ở đây, delegate và category được khai báo kiểu protocol và được khởi tạo trong hàm init để tránh bị dependency.
    2. Sau khi thực hiện xong việc truy vấn database, presenter dùng delegate để yêu cầu View update UI.
    Class View:
    import UIKit
    
    class HomeView: UIViewController {
        @IBOutlet private weak var categoryNameLabel: UILabel!
        
        private var presenter: HomeViewPresenter!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            presenter = HomeViewPresenter(view: self, service: CategoryService())
        }
        
        // 1
        @IBAction func didTapButtonSearch(_ sender: Any) {
            presenter.searchCategoryById(id: 1)
        }
    }
    
    extension HomeView: IHomeViewDelegate {
        // 2
        func updateUI(_ categoryName: String) {
            categoryNameLabel.text = categoryName
        }
    }
    1. Khi user tap button, View sẽ chuyển action đến presenter và yêu cầu presenter thực hiện logic.
    2. Update UI khi được presenter yêu cầu.

    Lợi ích của MVP:

    MVP đã giúp tách biệt logic khỏi View, từ đó mang đến các lợi ích:

    • Dễ dàng đọc hiểu code.
    • Dễ dàng viết Unit test cho logic.
    • Dễ maintain, bảo trì.

    Kết luận:

    • MVP chỉ giải quyết việc tách biệt logic khỏi View. Tuy nhiên, những phần logic như truy vấn database, networking, … thì chưa được xử lí. Những logic như vậy có thể được đặt ở presenter, hoặc tạo 1 class riêng ở Model tùy theo bản thân bạn.
      Tuy nhiên, nên tách biệt các phần logic Database, networking, … ra các class riêng để tuân thủ nguyên tắc Single Responsibility Principle của SOLID.
    • Theo mô hình MVP, Presenter không được import UIKit để tránh logic bị liên quan đến View.
  • 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

  • Basic CAShapeLayer iOS (P1)

    Basic CAShapeLayer iOS (P1)

    Trong lập trình ứng dụng, sẽ có những lúc bạn phải làm việc với các đường thẳng, các hình khối.
    Đôi khi chúng quá đơn giản để phải cần cắt ra các image, đôi khi các ứng dụng đòi hỏi các animation, dẫn đến phức tạp thì không thể dùng image được.
    CAShapeLayer được dùng để làm việc với các đường thẳng, hình khối 1 cách đơn giản.

    Contents:

    • How to draw a line
    • Draw shape with shape layer

    How to draw a line?

    Học cách vẽ 1 đường thẳng là điều đầu tiên phải học trước khi học vẽ 1 hình khối.
    Để vẽ 1 đường thẳng thì cần tối thiểu 2 điểm: điểm đầu và điểm cuối.
    UIView có func draw() dùng để vẽ các line:

    • move(to: ): move đến 1 điểm cụ thể để bắt đầu vẽ.
    • addLine(to: ): vẽ 1 đường thẳng đến 1 điểm cụ thể.
      Kết quả thu được:

    Để vẽ 1 đường cong, thì apple cung cấp 2 phương thức addCurve() addQuadCurve():

    1. Với func addCurve() thì sẽ vẽ được 1 đường cong với 2 điểm control point là 2 điểm cong.
    2. Với func addQuadCurve() thì sẽ chỉ có 1 điểm control point để tạo điểm cong.
      Kết quả thu được:

    Draw shape with shape layer:

    Việc sử dụng draw() để vẽ có 1 nhược điểm lớn là nó sẽ bị gọi ngay khi khởi tạo view, và không thể lưu lại. Vậy trong trường hợp bạn muốn vẽ vào 1 thời điểm khác chứ không phải từ đầu? Ta sẽ sử dụng 1 cách phổ biến và đơn giản hơn, là vẽ bằng cách sử dụng CAShapeLayer.

    ShapeLayer là gì?

    • Nhìn vào hình trên có thể thấy 1 view sẽ chứa 1 root layer kiẻu CALayer và root layer đó sẽ chứa nhiều sublayer.
    • CAShapeLayer là 1 class kế thừa từ CALayer.
    • Ta dùng CAShapeLayer để tạo ra các custom layer, sau đó sẽ add đề lên root layer của 1 view như là 1 sublayer. Do đó, ta có thể tạo các custom layer tại bất kì thời điểm nào ta muốn và có thể lưu lại.
    Thứ tự hiển thị trên view khi add các sublayer

    Vẽ các hình khối:

    Giờ thì sẽ bắt đầu vẽ các hình khối trên 1 sublayer và add đè lên layer của view.
    1 vài hình đơn giản như hình vuông, hình tam giác,… đơn thuần là các nét thẳng thì có thể vẽ đơn giản bằng cách ghép các đường thẳng:

    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 = UIColor.yellow.cgColor
            shapeLayer.strokeColor = UIColor.red.cgColor
            layer.addSublayer(shapeLayer)
        }
    • Tạo ra 1 shapeLayer.
    • Tạo ra 1 path, 1 path là sự ghép lại của nhiều đường thẳng.
    • Sau khi vẽ xong path, gán path cho shapeLayer.
    • Add đè shapeLayer vừa tạo lên root layer của view.

    Đưa đoạn code trên vào hàm init của UIView và chạy thử:
    Kết quả thu được:

    Hàm close() ở trên được dùng để tạo ra 1 đường thẳng nối điểm hiện tại và điểm đầu tiên khi bắt đầu vẽ.
    -> Uncomment hàm close() ở trên và run sẽ thu được kết quả:

    Ngoài ra cũng có thể dùng 1 hàm addLine() thay cho hàm close() để nối 2 điểm.

    Hình bầu dục:

    Để vẽ hình bầu dục, sử dụng hàm UIBezierPath(ovalIn: CGRect):

    private func createOval() {
        let shapeLayer = CAShapeLayer()
            
        shapeLayer.lineWidth = 2.0
        shapeLayer.fillColor = nil
        shapeLayer.strokeColor = UIColor.red.cgColor
        let openCirclePath = UIBezierPath(ovalIn: self.bounds)
            
        shapeLayer.path = openCirclePath.cgPath        
        layer.addSublayer(shapeLayer)
    }

    Bạn cung cấp 1 frame để vẽ hình bầu dục trong đó, ở đây mình sẽ dùng view.bounds. Kết quả thu được:

    Vẽ hình tròn:

    Để vẽ 1 hình tròn thì trước hết cần phải biết các góc trong 1 hình tròn:

    Sau đó chỉ cần dùng hàm được cung cấp sẵn:

    private func createCircle() {
        let shapeLayer = CAShapeLayer()
            
        shapeLayer.lineWidth = 2.0
        shapeLayer.fillColor = nil
        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)
    }
    • arcCenter: tâm hình tròn
    • radius: Bán kính
    • startAngle, endAngle: Điểm bắt đầu và điểm kết thúc.
    • clockWise: có vẽ theo chiều kim đồng hồ không.

    Tùy vào bạn muốn vẽ 1 góc bao nhiêu độ mà thay 1 radius thích hợp.
    Kết quả thu được:

    Bài viết này đã tóm tắt về cách vẽ hình cơ bản.
    Ở bài viết sau về shapeLayer, sẽ nói về ShapeLayer Attribute và ShapeLayer Animation.

  • [Swift] Extensions

    [Swift] Extensions

    Extensions – có tác dung đúng như tên gọi của nó, là dùng để mở rộng những class, struct, enum hoặc protocol đã được define.
    Nó cho phép bạn mở rộng cả những class, struct mà bạn không có quyền truy cập để sửa source code như Int, String,.. hay UICollectionView, UITableView,…
    Bài viết này sẽ nói về extensions trong swift và những cách sử dụng căn bản.

    Content

    • Extension syntax
    • Initializes
    • Methods
    • Conform to protocol, delegate

    Extension syntax:

    Khai báo extensions với extension keyword:

    extension SomeType {
        // new functionality to add to SomeType goes here
    }

    Công dụng của extension:

    • Add thêm các property, computed property.
    • Add thêm các methods.
    • Cung cấp các initializers mới.
    • Định nghĩa subscripts
    • Định nghĩa và sử dụng các nested type
    • Extend 1 class để làm class đó conform các protocol, delegate.

    Initialize:

    • Extension có thể dùng để thêm các initialize mới cho các kiểu dữ liệu đã được define.
    • Đối với class, ở extension thì chỉ có thể thêm các convenience init mới, không được thêm các init hoặc deinit:
    designated init và deinit không được đặt tại extension của 1 class
    • Đối với struct thì có thể thêm các init, deinit vào extension:

    Methods:

    • Sử dụng extension để thêm các func mới cho 1 existing type:
    class Human {
        func eat() {
            print("Eating")
        }
    }
    
    extension Human {
        func play() {
            print("Playing")
        }
        
        func sleep() {
            print("Sleeping")
        }
    }
    
    let me = Human()
    me.eat()
    me.play()
    me.sleep()
    
    • Đối với value type như struct và enum, nếu func được thêm vào mà làm thay đổi giá trị của property của struct, enum thì func đó phải được define là mutating.
      Ở ví dụ dưới đây, vì func changeName thay đổi property name, nên func đó phải đc define là mutating bằng cách thêm mutating keyword vào đằng trước func.
    struct Dog {
        var name: String
    }
    
    extension Dog {
        mutating func changeName(newName: String) {
            self.name = newName
        }
    }
    
    • Có thể dùng extension để add thêm các func cho những source code mà bạn không được truy cập đến:

    Ở đây, bạn add thêm 1 func square để tính gía trị bình phương cho struct Int.
    Vì vậy, bất cứ 1 instance nào có kiểu dữ liệu là Int đều có func square mà bạn add thêm.

    Conform to protocol, delegate

    • 1 công dụng nữa của extension là extend 1 class để làm cho class đó conform 1 protocol, delegate:

    Thông thường, có thể làm 1 class conform protocol bằng cách viết:

    class AController: UIViewController, UICollectionViewDataSource. UICollectionViewDelegate {
    
    }

    Hoặc sử dụng extension:

    class AController: UIViewController {
    
    }
    
    // MARK: - UICollectionViewDataSource
    extension AController: UICollectionViewDataSource {
    
    }
    
    // MARK: - UICollectionViewDelegate
    extension AController: UICollectionViewDelegate {
    
    }

    Note: Nên tách ra thành nhiều extension khi extend 1 class để làm class đó conform protocol, delegate để clear và dễ dàng đọc code hơn.

    Kết luận:

    • Dùng extension để làm cho source code dễ đọc và clear hơn.
    • Dùng extension để add thêm các func cho những class như ViewController, TableView, … hoặc struct như Int, String,… để có thể tái sử dụng, tránh duplicate code.

    Ngoài ra, extension còn dùng để add thêm các nested type, các subscrip, cách làm cũng tương tự như khi add thêm method, property.

    Có thể tham khảm thêm tại: https://docs.swift.org/swift-book/LanguageGuide/Extensions.html

  • Access Control in Swift

    Access Control in Swift

    Content

    1. Giới thiệu về Acess Control
    2. Các loại access control
    3. Subclassing
    4. Getter/Setter
    5. Protocols

    Các loại access control

    Swift cung cấp 5 loại access control:

    Open access:

    • Là loại access có độ truy cập cao nhất.
    • Nếu 1 entity có access level là open, thì nó có thể được truy cập từ bất cứ nơi nào trong module mà thực thể đó được định nghĩa, và cả từ 1 module khác bên ngoài mà đang import module đó.
    • Khi truy cập từ module bên ngoài hoặc bên trong thì đều có thể kế thừa, override,… lại entity đó. > Note: Access level open thường được sử dụng cho các framework, dể bạn có thể dễ dàng kế thừa, override, sử dụng các phương thức của framework đó từ trong project của bạn.

    Public access

    • Nếu 1entity có access level là open, thì nó có thể được truy cập từ bất cứ nơi nào trong module mà thực thể đó được định nghĩa, và cả từ 1 module khác bên ngoài mà đang import module đó.
    • Level access thấp hơn open bởi khi truy cập từ module bên ngoài thì không cho phép kế thừa, override,… lại entity đó.

    Internal access

    • 1 entity có access level là internal thì sẽ chỉ được sử dụng tại bất cứ nơi nào trong module định nghĩa entity đó, nhưng không thể sử dụng bên ngoài module định nghĩa nó.
    • 1 entity nếu nếu không được set access level cụ thể thì mặc định access level là internal.
    name và func printName có access level mặc định là internal.

    Fileprivate

    • 1 entity có access level là fileprivate thì sẽ hạn chế quyền truy cập của module định nghĩa tới entity đó. Entity đó chỉ cho phép truy cập tới nó từ bên trong swift file định nghĩa nó.
    class A có property name kiểu fileprivate, property age là kiểu internal.

    class B không thể truy cập được đến property name của A bởi A và B nằm ở 2 file swift khác nhau và property name có access level là fileprivate.
    -> property name chỉ có thể truy cập được trong classA.swift file.

    Private

    • Là access level có độ truy cập thấp nhất.
    • 1 entity nếu có access level = private thì sẽ chỉ có thể truy cập được từ class khai báo entity đó, hoặc từ extension của class đó nhưng phải trong cùng 1 swift file. > Bạn nên dùng private access để giấu các phương thức, các biến cụ thể khỏi bên ngoài khi các phương thức, biến đó chỉ được dùng từ bên trong nơi định nghĩa chúng.
    classA.swift
     class A {
      private var name = ""
     }
     
     class C {
        let a = A()
     
        func printNameOfA() {
            // Không thể truy cập được property name ở đây vì C 
              không phải class định nghĩa name.
        }
     }
     
     extension A {
         func printName(){
            print(name)
         }
     }
     
     classB.swift
     extension A {
        func printName() {
           // Không thể truy cập được property name ở đây vì extension 
              của A không được khai báo cùng swift file với property name.
        }
     }

    Subclassing

    • bạn có thể subclass bất cứ class nào có thể truy cập được trong phạm vi truy cập.
    • 1 subclass thì không thể có access level cao hơn so với super class.
    • Khi override lại các phương thức, class con có thể override lại với 1 access level mới cao hơn so với access level của phương thức của super class.

    Getter/Setter

    • Getters và setter mặc định nhận access level của các biến, property,… mà chúng thuộc về.
    • Bạn có thể làm cho setter có access level thấp hơn getter bằng cách cung cấp access level riêng cho setter.
    Ở đây. bạn tạo access level private cho property name nên không thể gán 1 giá trị mới cho property name từ bên ngoài class.

    Protocols

    • Các func, properties bên trong protocol sẽ chỉ có thẻ có cùng access level cùng với protocol đó.
    • Nếu bạn tạo ra 1 protocol mới kế thừa từ 1 protocol đã có, thì protocol mới phải có access level thấp hơn so với protocol đã có.
    • 1 class có thể conform to 1 protocol có access level thấp hơn class đó.

    Hi vọng bài viết đã tổng quát lại được những kiến thức hữu ích về access control trong swift.

  • ARC in Swift

    ARC in Swift

    Việc quản lí bộ nhớ trong mỗi ứng dụng là rất quan trọng. Nếu ứng dụng của bạn sử dụng dữ liệu bừa bãi 1 cách không cần thiết thì sẽ làm memory của ứng dụng tăng cao, và trong trường hợp xấu có thể dẫn đến crash.
    Swift sử dụng ARC để hỗ trợ developer quản lí ứng dụng của app. Ở bài viết này mình sẽ nói về ARC trong swift.

    Contents:

    • ARC là gì
    • Cách ARC hoạt động
    • Strong reference
    • Xử lí strong reference
    • weak reference

    ARC là gì

    • ARC là viết tắt của Automatic Reference Counting. Swift sử dụng ARC để giúp bạn việc quản lý bộ nhớ của app.
    • ARC sẽ tự động giải phóng bộ nhớ được sử dụng bởi 1 biến khi biến đó không còn được sử dung nữa.
    • Tuy nhiên, để ARC có thể biết được khi nào biến đó không còn dc sử dụng, thì ARC phải biết về các relations giữa biến đó vs các biến khác trong code của bạn.

    Note: ARC chỉ áp dụng cho các biến kiểu class(reference type).

    Cách ARC hoạt động

    • Mỗi khi bạn tạo 1 biến, ARC sẽ tự chia sẻ 1 phần bộ nhớ để lưu trữ biến đó. Khi biến đó không còn được sử dụng, ARC giải phóng biến, lấy lại bộ nhớ để dùng cho những mục đích khác
    • Để xác định 1 biến không còn được sử dụng, ARC sẽ đếm số lượng strong reference hiện tại của biến đó đối với các biến khác. Chỉ khi số lượng strong reference = 0 thì biến đó mới được giải phóng bộ nhớ.

    Vậy strong reference là gì?

    Strong reference

    Mỗi khi bạn gán 1 biến kiểu class cho 1 property, constant hoặc variable, thì những thứ đó tạo 1 strong reference đến biến đó.

    class Car {
        var price: Double
        init(price: Double) {
            self.price = price
        }
        
        deinit {
            print("Car is being deinitialized")
        }
    }
    
    var hondaCar: Car? = Car(price: 50000.0)

    Ở đây, bạn gán biến Car(price: 50000.0) cho varialbe hondaCar
    -> hondaCar đã tạo 1 strong reference đến Car(price: 50000.0)

    Thêm dòng code sau và run thử trên playground:

    hondaCar = nil

    Kết quả thu được ở màn hình console như sau:

    Nếu bạn set honedaCar = nil -> Khi đó Car(price:50000) sẽ không còn strong reference nào trỏ đến -> ARC sẽ giải phóng bộ nhớ cho Car.
    Tiếp tục thử chạy đoạn code sau và quan sát màn hình console:

    Chạy đoạn code trên, bạn sẽ không thấy được print ra "Car is being deinitialized"…
    Vì các biến car1, car2, car3 là reference type, nên ta có sơ đồ relation ship như sau:

    Khi đó nếu chỉ set car1, car2 = nil, thì Car vẫn còn 1 strong reference từ car3 trỏ đến -> ARC sẽ không giải phóng bộ nhớ cho Car trong trường hợp này.

    Strong reference giữa các class

    Trong ví dụ trên, sẽ tạo ra 1 sơ đồ relation giữa các instance như sau:

    • Việc set property departmentowner của 2 instance tuangotham đã tạo ra 2 strong reference trỏ đến nhau.
    • Khi đó Person và Departmen, mỗi thứ sẽ có 2 strong refernce trỏ đến. Vì vậy, khi bạn chỉ set tuan = nilgotham = nil thì Person và Departmen vẫn sẽ không được xóa khỏi bộ nhớ dù không cần dùng đến nữa.
      -> Trường hợp này gọi là retain cycle -> Tạo ra rò rỉ bộ nhớ (memory leaks).

    Xử lí strong reference

    Swift cung cấp 2 cách để xử lí strong reference cycle khi bạn làm việc với các biến kiểu class:

    • weak reference
    • unowned reference

    Weak reference

    • weak refernce: thay vì tạo 1 strong reference, nó sẽ chỉ tạo 1 weak -> ARC sẽ không làm tăng số lượng strong reference trỏ tới biến đó.
    • Sử dụng weak reference khi biến còn lại có thời gian tồn tại ngắn hơn(thường bị giải phóng bộ nhớ trước)
    class Person {
        weak var department: Department?
        
        deinit {
            print("Person is being deinitialized")
        }
    }

    Sửa lại ví dụ trên bằng cách sửa department thành kiểu weak( vì department có thời gian tồn tại ngắn hơn người).
    Khi đó, sơ đồ relation ship sẽ thành:

    • set gotham = nil -> Departmen k còn strong reference nào trò đến -> được giải phóng khỏi bộ nhớ, khi đó strong reference của Departmen trò đến Person cũng sẽ bị mất.
    • set tuan = nil -> Person không còn strong refernce nào trỏ đến -> ARC giải phóng khỏi bộ nhớ.
      Kết quả thu được:

    Unowned reference:

    • Tương tự như weak refernce, nó sẽ không tạo ra 1 strong reference.
    • Khác weak reference ở chỗ, unowned dùng cho những biến có thời gian tồn tại lâu hơn.

    Note: nếu bạn truy cập đến 1 biến unowned sau khi nó đã được giải phóng khỏi bộ nhớ -> app sẽ crash. Vì vậy hãy cẩn thận khi dùng unowned.
    Sửa ví dụ trên thành như sau và sẽ vẫn thu được kết quả tương tự so với khi dùng weak:

    class Department {
        unowned var owner: Person?
        
        deinit {
            print("Department is being deinitialized")
        }
    }

    Hi vọng bài viết trên đã đem lại cho bạn cái nhìn tổng quan về ARC và cách sử dụng weak/unowned để tránh retain cycle giữa các class.

  • Deep dive into Memory Leaks Swift

    Deep dive into Memory Leaks Swift

    Đa số với mỗi lập trình viên đều đã gặp phải những vấn đề về memory leaks.
    Ở bài viết này, mình sẽ đi sâu vào memory Leaks và cách xử lí.
    Bài viết này đòi hỏi sự hiểu biết về weak/strong reference, retain cycle và ARC trong swift.

    Contents:

    • Memory leaks là gì?
    • Xử lí memory leaks bằng weak/unowned
    • Non-escaping closure vs escaping closure
    • Delay Deallocation
    • Optional self vs Unwrapped self
    • Example

    Memory leaks là gì?

    • Memory leaks là 1 phần bộ nhớ bị chiếm vĩnh viễn và không được giải phóng mặc dù không cần dùng đến -> Dẫn đến không thể tái sử dụng phần bộ nhớ này.
    • Thường xảy ra do retain cycle.

    Memory leaks gây ra những gì:

    • memory của ứng dụng tăng cao không cần thiết -> dẫn đến memory warning và có thể crash.
    • Những object bị leaks sẽ không bị hủy bỏ -> object đó sẽ luôn lắng nghe thông báo và sẽ thực hiện phản ứng mỗi khi nhận thông báo -> Dẫn đến sai lệch kết quả, có thể đặc biệt nghiêm trọng nếu ảnh hưởng đến database.

    Memory leaks demo

    Khởi tạo 2 View Controller như sau:

    • VC1 có 1 button để push sang VC2.
    • VC2 có hàm deinit để print ra "VC2 was deallocate from memory" khi VC2 được giải phóng bộ nhớ.

    Build thử, tap vào button để push sang VC2, và tap vào nút back để quay lại VC1. Quan sát màn hình console, có thể thấy không có gì được print ra.

    Ở đây, VC2 không được giải phóng bộ nhớ bởi VC2 giữ 1 strong reference đến closure completion, closure completion cũng giữ 1 strong reference đến VC2 -> Tạo ra 1 retain cycle -> Memory leaks.

    Xử lí memory leaks bằng weak/unowned

    Cách giải quyết mà mọi người thường dùng nhất là sử dụng weak/unowned.
    Vậy sự khác biệt giữa weak và unowned là gì?

    • Giống: Weak và unonwed tạo ra 1 weak reference thay vì 1 strong reference để loại bỏ retain cycle -> Bộ nhớ sẽ dc giải phóng ngay lập tức khi không cần dùng đến.
    • Khác:
    WeakUnowned
    – Có thể nilKhông thể nil

    Tuy nhiên, unowned cũng giống như việc force unwrapping self và cố gắng truy cập đến contents của nó ngay cả sau khi self đã được giải phóng -> dẫn đến crash.

    Crash do truy cập đến self trong khi self đã được xóa khỏi bộ nhớ

    Vì vậy ta thường thấy weak self được sử dụng nhiều hơn. Thay đoạn code viewDidLoad ở VC2 bằng:

    override func viewDidLoad() {
        super.viewDidLoad()
        completion = { [weak self] in
            self?.view.backgroundColor = .red
        }
    }

    Build và chạy thử. Sau khi pop từ VC2 về VC1, màn hình console đã hiện "VC2 was dellocated from memory" -> Memory leaks đã được giải quyết.

    Tuy nhiên câu hỏi ở đây là, liệu có cần sử dụng weak cho mọi closure?

    Non-escaping closure vs escaping closure

    Để trả lời câu hỏi trên, có 1 vài điều trước hết bạn cần phải biết.

    • non-escaping closure (ví dụ higher-order functions như compactMap): Được thực hiện trong 1 phạm vi thân hàm nhất định, được thực hiện ngay lập tức và sau khi thực hiện thì được giải phóng, không được lưu lại.
    • escaping closure: Được lưu lại, có thể truyền đi như 1 biến, và có thể được thực hiện lại vào 1 thời điểm khác trong tương lai.

    Closure sẽ tạo ra 1 strong reference đối với những thứ được đóng gói bên trong closure, trong ví dụ ở đây là self.

    • Đối với non-escaping closure: strong reference này sẽ chỉ tồn tại trong thời gian closure đó dc thực hiện -> Khi closure thực hiện xong, strong reference biến mất -> self không còn strong reference nào trỏ đến nên được giải phóng khỏi bộ nhớ.
    • Đối với escaping closure: Nếu escaping closure này đóng gói 1 self bên trong, thì strong reference này sẽ tồn tại mãi mãi -> tạo ra retain cycle. -> self không được giải phóng.
      Demo 2: Thay đoạn code viewDidLoad của VC2 thành và run thử.
    override func viewDidLoad() {
        super.viewDidLoad()
        let nonEscapingClosure = {
            self.view.backgroundColor = .red
        }
    }
    Tuy closure này đóng gói self bên trong nhưng khi back về VC1, VC2 vẫn được giải phóng khỏi bộ nhớ.

    Ở đây bạn tạo ra 1 non-escaping closure, nó tạo ra 1 strong reference tới self, khi kết thúc thân hàm, strong reference này sẽ biến mất -> Không bị retain cycle. Vì vậy, không cần dùng weak self trong trường hợp này.

    Delay Deallocation

    Gỉa sử có 1 func download image từ internet như sau:

    Ở đây URLSession.shared.dataTask là 1 non-escaping closure cần nhiều thời gian để chạy.

    Non-escaping closure không yêu cầu bạn phải dùng weak self để tránh retain cycle, tuy nhiên với những closure cần nhiều thời gian chạy như trên, thì sẽ tạo ra 1 khoảng thời gian delay trước khi VC2 được deallocate rất lớn -> Nếu ng dùng push và pop vào VC2 liên tục thì vẫn tạo ra tăng memory.

    Note: Cân nhắc việc sử dụng weak self đối với non-escaping closure!

    Optional self vs Unwrapped self

    Dùng weak self sẽ làm cho self thành kiểu optional. Khi đó sẽ thường có 2 kiểu giải quyết:

    • Dùng optional self: self?
    • Dùng unwrapped self

    Vậy 2 cách này có khác gì nhau?

    • Khi unwrapped self, thì sẽ chỉ kiểm tra xem self có tồn tại 1 lần duy nhất ở đầu thân hàm, nếu self khác nil thì sẽ tạo 1 strong reference tồn tại trong thời gian chạy hàm. Khi hàm kết thúc, strong reference biến mất, khi đó VC2 mới được deallocated mặc dù đã pop về VC1. -> Vẫn tạo ra 1 delay deallocated, không có retain cycle.
    • Dùng self? thì sẽ check mỗi lần gọi đến self. Nếu self đã dc giải phóng khỏi bộ nhớ thì trình biên dịch sẽ bỏ qua dòng code đó. -> Không tạo ra delay deallocated, không có retain cycle.

    Examples

    Grand Central Dispatch

    • GCD được khởi tạo và thực hiện ngay lập tức, không được lưu lại nên là non-escaping closure, không cần dùng weak self.
      -> Đó là lí do không cần dùng weak self ở main.async
    DispatchQueue.main.async {
        self.view.backgroundColor = .yellow
    }

    UIView.Animate

    UIView.animate(withDuration: 0, animations: {
        self.view.backgroundColor = .yellow
    }, completion: nil)

    Tương tự như GCD, closure trong UIView.animate cũng là 1 non-escaping closure nên không cần weak self.

    Timer

    let _ = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (_) in
         self.view.backgroundColor = .yellow
    }

    Timer sẽ tạo ra 1 strong reference và retain cycle nếu thỏa mãn 2 điều kiện:

    • Timer chạy lặp lại liên tục.
    • Timer capture lại self trong block closure

    -> Nếu thoả mãn 2 điều kiện trên thì phải dùng weak self đối với Timer.