Author: Hoang Anh Tuan

  • Basic Test in Swift (P1)

    Basic Test in Swift (P1)

    Trong lập trình, để có thể tự kiểm tra app để hoạt động chính xác hay chưa, thì ta phải test hết toàn bộ các case xảy ra. Tuy nhiên, nếu làm bằng 1 cách test thủ công thì rất tốn thời gian.
    Để có thể test tự động, thì có thể dùng Unit test. Vậy ở bài viết này, mình sẽ giới thiệu về cách sử dụng Unit test trong lập trình iOS.

    Nội dung bài viết

    • Unit test là gì?
    • Giới thiệu về unit test trong iOS
    • Useful Test
    • Khởi tạo Test Target
    • Run the test
    • Functional test Demo

    Unit test là gì?

    • Là phương pháp dùng để kiểm tra tính đúng đắn của một đơn vị source code. Một Unit (đơn vị) source code là phần nhỏ nhất có thể test được của chương trình, thường là một phương thức trong một lớp hoặc một tập hợp các phương thức thực hiện một mục đích thiết yếu.
    • Bạn viết các test case để Xcode tiến hành test các test case đó.

    Giới thiệu về Unit test trong iOS

    Xcode cung cấp 3 kiểu test chính:

    • Functional test: tập trung vào test các func.
    • Performance tests: tập trung vào đo lương thời gian app của bạn thực thi xong các task trên các loại thiết bị khác nhau.
    • User Interface tests (UI Tests): tập trung vào những tác vụ của người dùng trên UI.

    Note:

    • Functional test & performance test: Là những đoạn test mà bạn tự viết để test sự hoạt động của các func trong app.
    • UI Test: Ghi lại những thứ mà bạn tương tác trên UI của app.

    Useful Test

    1 test case được coi là useful khi:

    • Test case phải có khả năng fail: Nếu test đó không thể fail, thì việc test sẽ rất vô giá trị, bạn nên xốa nó đi.
    • Test case phải có khả năng success: Nếu test đó không thể success, thì việc test sẽ rất vô giá trị, tương tự ở trên, bạn nên xốa nó đi.
    • Test case phải được refactor và được viết đơn giản

    Khởi tạo 1 Test Target

    Để viết được test, trước hết cần tạo 1 Unit test target để viết test:

    Điền tên cho class rồi chọn Next.

    1. Xcode imports sẵn cho bạn frameworks XCTest và class được tạo là subclass của XCTestCase.
    2. func setUp(): dùng để thiết lập lại trạng thái trước mỗi lần test.
    3. func tearDown(): dùng để thực hiện dọn dẹp sau khi mỗi lần test kết thúc.
    4. measure: dùng để đo thời gian thực hiện xong việc test -> giúp test performance.

    Trình tự mỗi khi thực hiện test 1 test case như sau:

    Run the Tests:

    Có 3 cách để run test:

    • Product ▸ Test or Command-U: Cách này sẽ run tất cả test classes trong project.
    • Click vào button mũi tên ở Test navigator.
    • Click chọn nút hình kim cương để run 1 test case cụ thể.

    Basic Functional Test Demo

    Ở ví dụ này, mình sẽ tiến hành việc test xem dữ liệu học sinh ở file json đã chính xác chưa.

    Tạo 1 class Person:

    class Person: Decodable {
        let name: String
        let age: Int
        
        init(name: String, age: Int) {
            self.name = name
            self.age = age
        }
    }
    

    Tạo 1 class Service chứa các logic thực hiện việc lấy dữ liệu:

    class Service {
        func getListStudent() -> [Person] {
            var students: [Person] = []
            guard let url = Bundle.main.url(forResource: "config", withExtension: ".json") else {
                return students
            }
            
            
            do {
                let data = try Data(contentsOf: url)
                let results = try JSONDecoder().decode([Person].self, from: data)
                students += results
            } catch {
                print("Can get data, error: \(error)")
            }
            return students
        }
    }

    Tiến hành việc viết test thông tin cho sinh viên đầu tiên:

    1. @tesable import + tên project: Cho phép unit tests truy cập đến toàn bộ các dữ liệu kiểu internal của project.
    2. Khai báo 1 biến kiểu service.
    3. Khởi tạo sut mỗi lần bắt đầu thực hiện test 1 test case.
    4. Set sut = nil mỗi khi kết thúc việc test 1 test case.
    5. Viết func test để test thông tin học sinh 1.
    6. Viết các hàm để test.

    Note: Khi viết func để test thì phải tên func phải luôn bắt đầu với từ test để Xcode có thể biết đó là 1 test.

    Thực hiện tiến hành test func, nhận được kết quả dữ liệu cần kiểm tra đã chính xác:

    Để đảm bảo đây là 1 Userful test, thực hiện sửa đổi name của sinh viên 1 trong file json thành "Cuong1" và tiến hành test lại func, nhận được kết quả sau:

    Việc test thất bại, đồng thời XCode cũng thông báo ra kết quả để biết sai ở đâu.

    Ở phần tiếp theo, mình sẽ nói về performance test, UI Test và 1 vài thứ hay ho mà Xcode support cho việc test.

  • SQLite with FMDB in Swift (P2)

    SQLite with FMDB in Swift (P2)

    Nội dung bài viết:

    • Truy vấn insert
    • Truy vấn select
    • Truy vấn delete
    • Truy vấn update

    Truy vấn INSERT

    • Câu truy vấn này dùng để thêm bản ghi vào table
    • Có 2 cách để viết 1 câu truy vấn insert.
      Cách 1:

    Cách 2: Trong trường hợp bạn tạo 1 bản ghi mới đầy đủ các trường của bảng ghi, thì k cần liệt kê cụ thể tên các trường như cách 1:

    • Để thực hiện truy vấn INSERT vào table Book ở bài viết trước, việc đầu tiên là viết 1 câu truy vấn:
    let query = "insert into Book (name, author, price) values ('Kill the mockingbird', 'HarperLee', 85000)"

    Đây là 1 câu truy vấn thực hiện sự thay đổi đến database, nên ta vẫn sử dụng lệnh executeUpdate(…) để thực hiện câu query:
    Ở đây, vì table Book được khởi tạo với thuộc tính id tự động tăng, nên ta có thể bỏ qua việc khai báo id.

    func insertBook() {
        if openDatabaseConnectionAtPath(path: getDatabasePath()) {
            let query = "insert into Book (name, author, price) values ('Kill the mockingbird', 'HarperLee', 85000)"
            do {
               try database.executeUpdate(query, values: nil)
            } catch let err {
               print("Insert failed, error: \(err.localizedDescription)")
            }
        }
        database.close()
    }

    Ở đây, table Book có cả 4 trường đều khác nil, tuy nhiên trường id và price có thuộc tính default nên có thể không cần khai báo trong câu lệnh insert, nhưng nếu bạn insert thêm 1 bản ghi mà không có trường name hay author thì FMDB sẽ trả về lỗi và không thể insert được.

    Note: Bạn có thể thực hiện nhiều câu truy vấn cùng 1 lúc, bằng cách gộp chúng vào 1 câu truy vấn chung, mỗi câu truy vấn nhỏ ngăn cách nhau bằng 1 dấu ;

    • Ngoài ra, còn có 1 cách thực hiện câu lệnh executeUpdate thuận tiện hơn bằng cách sử dụng thuộc tính values như sau:
    • Ở đây, nameauthor không được cụ thể ngay từ câu truy vấn, mà được để 1 dấu ? có tác dụng như 1 placeholder để truyền trường name, author vào sau.
    • Khi thực hiện lệnh executeQuery, điền lần lượt các giá trị muốn truyền tương ứng vào các dấu ? vào thuộc tính values.

    Truy vấn SELECT:

    • Câu truy vấn này dùng để lấy các bản ghi từ table.
    • Cách viết câu truy vấn Select:
      + SELECT column1, column2,… FROM table_name; // Lấy 1 vài trường cụ thể
      + SELECT * FROM table_name; // Lấy tất cả các bản ghi
      + Ngoài ra có thể kết hợp thêm Where, Order by,… vào câu truy vấn để lọc các bản ghi
    • Ví dụ thực hiện câu truy vấn lấy tất cả các sách trong table Book:
    Lấy tất cả các bản ghi và sắp xếp theo giá tăng dần
    Lấy tất cả các tên tác giả mà có trường id = 2
    • Câu truy vấn select k làm thay đổi DB, nên sử dụng lệnh executeQuery(…) để thực hiện.
    • Câu truy vấn này trả về 1 tập dữ liệu FMResultSet, từ đó convert về kiểu dữ liệu mà ta mong muốn.

    Truy vấn DELETE

    • Dùng để xóa bản ghi đang có trong table.
    • Cách viết câu truy vấn delete:
      DELETE FROM table_name WHERE condition;
    • Ví dụ thực hiện câu truy vấn delete:

    Truy vấn UPDATE:

    • Dùng để thay đổi giá trị của bản ghi đang có trong table.
    • Cách viết câu truy vấn:
      UPDATE table_name SET column1 = value1, column2 = value2, … WHERE condition;
    • Ví dụ thực hiện câu truy vấn:

    Kết luận:

    • Nên define các trường thành Constant để tránh gõ nhầm khi viết câu truy vấn.
    • Sử dụng lệnh executeUpdate để thực hiện thay đổi DB, dùng lệnh executeQuery để thực hiện việc lấy các bản ghi.
    • Các câu lệnh có thể gộp vào với nhau, nhưng ngăn cách với nhau bằng dấu ;.
    • Luôn đóng liên kết tới DB sau khi thực hiện xong câu truy vấn.
  • SQLite with FMDB in Swift (P1)

    SQLite with FMDB in Swift (P1)

    Đối với việc làm ứng dụng phần mềm, đa số đều phải sử dụng đến database. Core Data của Apple cung cấp là 1 trong những sự lựa chọn phổ biến đối với ứng dụng trên iphone.
    Theo í kiến cá nhân thì Core Data thường được dùng cho các database được tạo ra trong quá trình sử dụng, còn trong trường hợp nếu bạn có 1 database có sẵn để đưa vào app thì sao? Khi đó thì cần đưa 1 database đã được nhập sẵn vào app. SQlite là 1 lựa chọn tốt Ở bài viết này, mình sẽ giới thiệu cách sử dụng SQLite trong swift bằng thư viện FMDB.

    Nội dung bài viết

    • Giới thiệu về FMDB
    • Tạo đường dẫn cho 1 database
    • Khởi tạo 1 database
    • Tạo kết nối đến database
    • Thực hiện truy vấn

    FMDB Library:

    • FMDB là 1 thư viện hỗ trợ truy vấn Sqlite và quản lí dữ liệu hiệu quả.
    • Giúp bạn xử lí việc open connection/ close connection đến DB.
    • Có thể dùng cho cả swift hoặc obj-C.
    • Dễ dàng và nhanh chóng để tích hợp vào 1 project.
    • Cách sử dụng tương đối đơn giản, cho phép người dùng tự viết câu truy vấn theo ý muốn.

    Bài viết này tập trung vào việc sử dụng SQlite bằng thư viện FMDB, không hướng dẫn về phần cài đặt FMDB. Nếu bạn chưa biết về cách cài đặt thư viện, hãy xem thêm tại:

    https://guides.cocoapods.org/using/using-cocoapods.html

    Trong bài viết này, mình sẽ lấy 1 ví dụ về việc khởi tạo và viết các câu truy vấn đến 1 DB chứa các quyển sách.
    Khởi tạo struct Book:

    struct Book {
        let id: Int
        let name: String
        let author: String
        let price: Double
        
        func toString() {
            print("Book's info: id: \(id), name: \(name), author: \(author), price: \(price)")
        }
    }
    

    Tạo 1 class DatabaseManager để thực hiện việc truy vấn DB.
    Khởi tạo 1 biến database kiểu FMDatabase – Sẽ dùng để truy cập và thực hiện truy vấn đến DB.

    import Foundation
    import FMDB
    
    class DatabaseManager {
        static let shared = DatabaseManager()
        var database: FMDatabase!
    }

    Tạo đường dẫn để lưu DB:

    Nếu chưa có 1 DB có sẵn, thì khi tạo 1 DB cần phải có đường dẫn để lưu DB. Khởi tạo 1 đường dẫn cho 1 database có tên bookDB như sau:

    func getDatabasePath() -> String {
       let directoryPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
       let path = directoryPath.first!.appendingPathComponent("musicDB.db")
       print(path)
       return path.description
    }

    Sau khi print được ra đường dẫn, để xem database vừa tạo: Vào Finder, sử dụng tổ hợp phím Cmd + Shift + G, post đường dẫn vừa lấy được vào, Finder sẽ đưa đến database vừa tạo.

    Trong trường hợp đã có 1 DB bookDB có sẵn và được kéo vào trong app, thì lấy đường dẫn như sau:

    func getPathOfExistDB() -> String? {
       if let path = Bundle.main.path(forResource: "bookDB", ofType: ".db") {
           return path
       } 
       return nil
    }

    Khởi tạo 1 database

    func createDatabase() {
        if database == nil {
            if !FileManager.default.fileExists(atPath: getDatabasePath()) {
                database = FMDatabase(path: getDatabasePath())
            }
        }
    }

    Nếu 1 DB chưa tồn tại, thì khởi tạo 1 DB mới tại 1 đường dẫn đơn giản bằng cách khởi tạo 1 object FMDatabase tại 1 đường dẫn.

    Note: Việc check xem đã tồn tại 1 DB tại 1 path cụ thể trước khi khởi tạo rất quan trọng, bởi nếu ta path đó đã có sẵn 1 DB rồi thì DB đó sẽ bị hủy đi để khởi tạo 1 DB mới.

    Tạo kết nối đến Database:

    Note: Khi muốn truy suất đến DB thì phải mở 1 liên kết với DB, khi đã hoàn thành việc truy suất thì phải đóng liên kết lại.

    • Tạo liên kết tới DB:
    database.open()
    • Đóng liên kết tới DB:
    database.close()

    2 hàm trên đều trả về giá trị Bool để biết việc mở/đóng liên kết có thành công hay không.
    Ta có thể kết hợp việc tạo DB cùng với việc mở/đóng liên kết thành 1 hàm để tiện sử dụng:

    func openDatabaseConnectionAtPath(path: String) -> Bool {
        if database == nil {
            if !FileManager.default.fileExists(atPath: path) { // don’t want to create the database file again and destroy the original database.
                database = FMDatabase(path: path)
            }
        }
            
        if database != nil {
            if database.open() {
                print("Open Success")
                return true
            }
        }
        print("Open failed")
        return false
    }

    Tạo 1 bảng trong DB:

    Câu truy vấn tạo bảng:

    Viết câu truy vấn tạo bảng trong FMDB như sau:

    let query = "create table Book (id integer primary key autoincrement not null, name text not null, author text not null, price float not null default 0)"

    Để chạy câu truy vấn:

    database.executeUpdate(query, values: nil)

    Câu lệnh executeUpdate(…) dùng để thực hiện những câu truy vấn tạo ra sự thay đổi đến DB.

    • Thuộc tính values của hàm executeUpdate sẽ được nói ở phần sau, ở đây tạm thời để nil.
    • Hàm thực hiện câu truy vấn tạo bảng:
    func createBookTable() {
        if openDatabaseConnectionAtPath(path: getDatabasePath()) {
            let query = "create table Book id integer primary key autoincrement not null, name text not null, author text not null, price float not null default 0"
            do {
                try database.executeUpdate(query, values: nil)
            } catch let err {
                print("Execute query failed. error: \(err.localizedDescription)")
            }
        }
        database.close()
    }

    Hàm createBookTable ở trên sẽ tạo ra 1 DB nếu DB chưa tồn tại, còn nếu DB đã tồn tại rồi thì sẽ tạo liên kết, thực hiện truy vấn rồi đóng liên kết.
    Thực hiện câu truy vấn createBookTable:

    Kiểm tra kết quả:

    Ở phần tiếp theo, sẽ nói về các câu truy vấn insert, delete,…

  • Passing Data with Callback Swift

    Passing Data with Callback Swift

    Một trong những vấn đề cơ bản trong lập trình ứng dụng là truyền data giữa các view controller. Có rất nhiều cách để làm điều này, như là dùng protocol, notification,… Ở bài viết này, mình sẽ giới thiệu đến 1 cách nữa, đó là sử dụng callback.
    Bài viết này yêu cầu sự hiểu biết về closure. Nếu chưa hiểu về closure, bạn có thể đọc tại đây:

    Nội dung bài viết

    • Function type
    • Callback là gì
    • Lợi ích của callback
    • Truyền data bằng cách sử dụng callback.

    Function type:

    Mọi function đều có 1 kiểu dữ liệu cụ thể, được tạo bởi kiểu dữ liệu của các tham số truyền vàokiểu dữ liệu trả về của function đó.

    Ví dụ ở func trên, không có tham số truyền vào và không có kiểu dữ liệu trả về, nên function type của func trên là () -> ().

    Đối với func không có kiểu trả về, thì cũng có thể viết theo cách khác là func đó trả về kiểu Void.

    func calculate(a: Int, b: Int) -> Int {
        return a + b
    } 
    // Function này có kiểu dữ liệu là (Int, Int) -> Int
    

    Ở bài viết lần này, mình sẽ không nói sâu về function type, mà sẽ tập trung vào chủ đề passing data bằng callback. Vậy callback là gì?

    Callback là gì?

    • Callback có thể hiểu như là 1 closure được gán cho 1 biến.
    • Để sử dụng callback truyền data, bạn khai báo callback với kiểu dữ liệu là kiểu dữ liệu của data mà bạn muốn truyền đi.
    var onCalculate: (Int, Int) -> (Int)

    Ở trên là ví dụ cách khai báo 1 callback có kiểu dữ liệu (Int, Int) -> Int. Callback này sẽ truyền data là 2 tham số kiểu Int đi, và khi 1 nơi khác nhận được data này, nó sẽ xử lí data và trả về 1 kiểu Int.

    var onPrint: (String) -> Void

    Ở ví dụ này, callback sẽ truyền data kiểu String đi, và khi 1 nơi khác nhận được dữ liệu, nó sẽ xử lí dữ liệu theo kiểu Void.

    Note: Nếu muốn truyền đi dữ liệu kiểu khác, bạn chỉ cần đơn giản sửa callback thành kiểu dữ liệu bạn mong muốn truyền đi.

    Truyền data sử dụng callback

    Giả sử có 2 view controller như sau:

    • View Controller 1 có 1 label để hiện thị kết quả, và 1 button để push sang View Controller 2.
    • View Controller 2 có nhiệm vụ thực hiện tính toán tổng của 2 số kiểu Int và trả kết quả kiểu Int về cho View Controller 1 để hiển thị. -> VC2 muốn truyền đi 1 data kiểu Int.
    • View Controller 1 sau khi nhận được data của VC2, sẽ hiển thị lên label.

    VC1:

    class FirstViewController: UIViewController {
        @IBOutlet weak var myLabel: UILabel!
        
        override func viewDidLoad() {
            super.viewDidLoad()
        }
        
        @IBAction func didTapButtonGoNext(_ sender: Any) {
            let storyboard = UIStoryboard(name: "Main", bundle: nil)        
            let secondVC = storyboard.instantiateViewController(withIdentifier: "SecondViewController") as! SecondViewController
            
            // Viết code hiển thị kết quả ở đây
            
            navigationController?.pushViewController(secondVC, animated: true)
            
        }
    }

    VC2:

    class SecondViewController: UIViewController {
        // 1
        var completionSum: ((Int) -> (Void))?
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            // 3
            let result = calculate(num1: 9, num2: 8)
            completionSum?(result)
            
            // 4
            navigationController?.popViewController(animated: true)
        }
        
        // 2
        func calculate(num1: Int, num2: Int) -> Int {
            return num1 + num2
        }
    }
    1. Khởi tạo 1 callback kiểu (Int) -> (Void), tức là callback này sẽ truyền đi data kiểu Int, và khi nhận được data sẽ xử lí theo kiểu Void.
    2. Khai báo func calculate để tính tổng 2 số và trả về kết quả kiểu Int.
    3. Tính tổng 2 số 9 và 8. Gọi completionSum?(result) để truyền đi result.
      Ở đây có dấu ? ở callback vì callback này là kiểu optional. Khi callback chưa được khởi tạo thì trình biên dịch sẽ bỏ qua mà không gọi callback. Vì vậy hãy chắc chắn rằng bạn đã khởi tạo callback trước khi gọi chúng.
    4. Back về VC1 để hiển thị kết quả.

    Giờ thì quay trở lại VC1, add thêm đoạn code sau vào phần để trống để khởi tạo completionSum cho VC2, bằng cách gán completionSum bằng 1 closure có cùng kiểu dữ liệu.

    secondVC.completionSum = { [weak self] (result) in
        self?.myLabel.text = String(result)
    }

    Ở đây bạn đã viết đoạn code để xử lí dữ liệu nhận được từ VC2. Sau khi nhận được result kiểu Int, bạn sẽ xử lí dữ liệu theo kiểu Void bởi kiểu dữ liệu của callback là (Int) -> Void.

    Khi VC2 gọi completionSum để truyền result đi, VC1 sẽ ngay lập tức nhận được và gọi hàm update label ở trên. Trình biên dịch sẽ chạy theo trình tự như sau:

    • Bấm vào button 1 -> Khởi tạo VC2, khởi tạo completionSum cho VC2 -> push sang VC2.
    • VC2 tính toán kết quả -> gọi completionSum -> VC1 nhận được kết quả và update cho label -> VC2 pop về VC1 để hiển thị kết quả.

    Ngoài ra bạn có thể khởi tạo callback bằng cách gán callback bằng 1 func có sẵn có cùng kiểu dữ liệu.
    Ở VC1, khai báo 1 func như sau:

    func showResult(result:  Int) {
        myLabel.text = String(result)
    }

    Func này có kiểu dữ liệu (Int) -> Void, cùng kiểu dữ liệu với callback completionSum.
    Sửa lại đoạn khai báo completionSum ở VC1 thành như sau:

    secondVC.completionSum = showResult

    Cách làm này cùng tác dụng với cách khai báo ở trên nhưng sẽ làm cho func ngắn gọn và clear hơn.

    Lợi ích của việc dùng callback

    • Đối với việc chỉ cần truyền những data đơn giản, xử lí đơn giản thì có thể dùng callback chứ không phải tạo protocol, notification,…
    • Tính reuseable cao, và ngắn gọn.
  • iOS: Slicing Image

    iOS: Slicing Image

    Nội dung bài viết:

    • Tổng quan
    • Cắt ảnh bằng Xcode Slicing Feature
    • Cắt ảnh bằng code.

    Tổng quan

    Trong 1 app iOS, sẽ có những buttons, views,… có kích thước khác nhau nhưng lại có cùng 1 background image.
    Nếu chỉ đơn giản dùng 1 background image cho các buttons, views,… đó thì bạn sẽ gặp phải tình trạng sau:

    Trong trường hợp này, việc sử dụng chung 1 image sẽ làm image bị co/dãn, làm xấu ảnh.
    Vậy nếu bạn chỉ muốn co/dãn phần giữa của ảnh, còn các phần ở 4 góc không bị co/dãn -> Hãy dùng slicing image.

    • Slicing image cho phép bạn có thể cắt 1 bức ảnh theo chiều dọc, chiều ngang hoặc cả ngang và dọc.
    • Nếu cắt theo chiều dọc, bạn sẽ chia bức ảnh làm 3 phần: Left, center và right. Trong trường hợp này, phần left và right là không co/dãn theo chiều ngang, phần center là phần co/dãn theo chiều ngang. (ảnh trên)
    • Nếu cắt theo chiều dọc, bạn sẽ chia bức ảnh làm 3 phần là top, center và bottom. Trong trường hợp này, phần top và bottom là không co/dãn theo chiều dọc, phần center là phần co/dãn theo chiều dọc (ảnh trên).
    • Nếu cắt theo chiều cả ngang và dọc, bạn sẽ chia bức ảnh làm 9 phần như ảnh trên và từng phần sẽ co/dãn như ảnh dưới.

    Cắt ảnh bằng Xcode Slicing Feature:

    1. Chọn vào phần Assets.xcassets trong Xcode.
    2. Chọn ảnh cần cắt.
    3. Chọn Editor -> Show Slicing hoặc chọn nút Show Slicing ở góc dưới bên phải.
    4. Chon Start Slicing
    1. Chọn cắt ảnh theo chiều ngang, chiều dọc hoặc cả ngang và dọc.
    1. Kéo các thanh để xác định phần bên phải, phần bên trái.
    • Phần 1: Phần không bị co/dãn bên trái theo chiều ngang.
    • Phần 2: Phần sẽ bị co/dãn theo chiều ngang.
    • Phần 3: Phần sẽ bị xóa khỏi ảnh mới.
    • Phần 4. Phần không bị co/dãn bên phải theo chiều ngang.
    Kết qủa: Ảnh bị co dãn phần ở giữa nhưng 2 bên không bị co dãn -> ảnh không có cảm giác bị méo

    Cắt ảnh bằng code:

    Bạn tạo ra 1 ảnh mới bằng cách cắt 1 ảnh có sẵn bằng hàm: resizableImage(withCapInsets:) hoặc resizableImage(withCapInsets:resizingMode:)

    if let img = UIImage(named: "button") {
        let resizable = img.resizableImage(withCapInsets: UIEdgeInsets(top: 0, left: 24, bottom: 0, right: 24), resizingMode: .stretch)
    }
    • Khai báo top và bottom # 0 nếu bạn muốn cắt ảnh theo chiều ngang.
    • Khai báo left và right # 0 nếu bạn muốn cắt ảnh theo chiều dọc.
    • Khai báo top, left, right và bottom # 0 nếu bạn muốn cắt ảnh theo cả ngang và dọc.

    Hi vọng qua bài viết này, bạn sẽ không gặp khó khăn khi apply 1 ảnh cho các buttons, views có size khác nhau.

    Link tham khảo: https://developer.apple.com/documentation/uikit/uiimage

  • Sự khác biệt giữa Struct và Class

    Sự khác biệt giữa Struct và Class

    Class và Struct là những thứ bất cứ 1 lập trình viên cũng thường xuyên sử dụng. Tuy nhiên, đối với những người mới thì việc phân biệt giữa class và struct là vẫn còn mơ hồ.
    Ở bài viết này, mình sẽ nói về các điểm khác nhau giữa Class và Struct.

    Nội dung bài viết:

    • Struct và Class là gì?
    • Điểm giống nhau giữa Struct và Class
    • Điểm khác nhau giữa Struct và Class
    • Khi nào nên sử dụng struct / class

    Struct và Class là gì?

    Struct và class là các cấu trúc linh hoạt, được sử dụng với nhiều mục đích khác nhau để trở thành các khối xây dựng chương trình của ban. Bạn định nghĩa các thuộc tính và phương thức để thêm vào các struct/class của bạn.

    Cách khởi tạo 1 struc và 1 class khá giống nhau.

    Điểm giống nhau giữa struct và class:

    Struct và Class đều có thể:

    • Định nghĩa, khai báo các thuộc tính và hàm.
    • Khai báo subscripts.
    class Rank {
        subscript (index: Int) -> String {
            switch index {
                case 1: return "First"
                case 2: return "Second"
                case 3: return "Three"
                default: return "Dont have rank"
            }
        }
    }
    
    let rank = Rank()
    print (rank[1]) // -> "First"
    print (rank[12]) // -> "Dont have rank"
    • Khai báo các initializers để khởi tạo.
    • Có thể mở rộng bằng extension.
    • Có thể implement các protocol để cung cấp các chức năng tiêu chuẩn.

    Điểm khác nhau giữa Struct và Class:

    Initialize:

    Khi định nghĩa 1 class, bạn bắt buộc phải khởi tạo 1 hàm init cho các thuộc tính không phải optional hoặc chưa có giá trị default.

    class Car {
        let id: Int = 1
        var color: UIColor?
        var price: Double
        
        init(price: Double) {
            self.price = price
        }
    }
    
    let car1 = Car(price: 5000)

    Còn khi định nghĩa 1 struct, bạn không cần phải khởi tạo 1 hàm init bởi khi đó Struct đã tự định nghĩa 1 hàm init default cho bạn.

    struct Car {
        let id: Int = 1
        var color: UIColor
        var price: Double
    }
    
    let car1 = Car(color: .red, price: 5000)

    Tuy nhiên, nếu bạn khai báo thêm các init khác thì hàm init default của struct sẽ bị mất.
    Vì vậy, để tránh điều này, chỉ cần khai báo các hàm init mới ở extension thì hàm init default sẽ không bị mất.

    Struct là Value types còn Class là Reference types

    Value type: 1 instance có kiểu là value type thì nó sẽ tự tạo ra các bản copy các giá trị của mình để truyền đi mỗi khi được nó đươc dùng để gán cho các instance khác, hoặc khi được dùng để truyền vào hàm. Bởi vậy, nếu bạn thay đổi giá trị các bản copy thì giá trị bản gốc cũng sẽ không bị thay đôi:

    let car1 = Car(price: 5000)
    var car2 = car1
    car2.price = 10000
    print(car1.price) // -> 5000.0
    print(car2.price) // -> 10000.0

    Reference type: Thay vì việc tạo ra các bản sao, thì 1 instance kiểu reference type sẽ tự truyền đi 1 tham chiếu tới chính nó khi được gán cho các insstance khác hoặc khi được truyền vào hàm.

    let car1 = Car(price: 5000)
    let car2 = car1
    car2.price = 10000
    print(car1.price) // -> 10000.0
    print(car2.price) // -> 10000.0
    print(car1 === car2) // -> true

    Có thể hiểu đơn giản rằng, car1 sẽ tự gán chính bản thân nó cho car2 chứ không tạo ra các bản copy như struct, bởi vậy khi thay đổi thuộc tính của car2 thì car1 cũng bị thay đổi theo.

    Note: Có thể kiểm tra 2 đối tượng có cùng trỏ tới 1 instance hay không bằng toán từ ===

    Class có thể kế thừa, còn struct thì không

    Class hỗ trợ kế thừa, có thể tạo ra các class con kế thừa từ class cha để mang những thuộc tính, phương thức của class cha. Có thể thấy class hỗ trợ lập trình OOP tốt hơn struct.

    Các phương thức trong struct nếu muốn thay đổi thuọc tính thì phải thêm mutating

    Struct là kiểu value type. Mặc định thì các thuộc tính của 1 biến kiểu value type không thể bị sửa đổi trong các hàm của biến đó.
    Tuy nhiên, nếu bạn muốn thay đổi thuộc tính của 1 Struct bằng 1 phương thức bên trong nó, bạn phải khai báo mutating vào trước phương thức để làm:

    Nếu 1 hàm thay đổi thuộc tính mà không báo mutating thì trình biên dịch sẽ báo lỗi

    Class hỗ trợ hàm deinit:

    Class cung cấp hàm deinit. Hàm này được gọi trước khi 1 class được giải phóng khỏi memory.

    Ví dụ về sự khác biệt struct và class thường dùng

    Ví dụ như trong ví dụ sau đây đối với hàm repeating rất hay được sử dụng:

    class Dog {
        var name = ""
    }
    
    let dogs = [Dog](repeating: Dog(), count: 3)
    dogs[0].name = "Green"
    dogs[1].name = "Red"
    dogs[2].name = "Blue"
    
    print("\(dogs[0].name) \(dogs[1].name) \(dogs[2].name)") // Blue Blue Blue

    Bởi Dog là 1 class, nên 3 đối tượng Dog trong mảng dogs sẽ cùng trỏ tới 1 đối tượng giống nhau -> Vì vậy nên thay đổi giá trị của dogs[3] cũng sẽ thay đổi giá trị của các dog còn lại trong array.

    Thử sửa Dog thành kiểu struct và print ra kết quả.

    Khi nào nên sử dụng struct / class

    Recommend sử dụng struct bởi:

    • Struct nhanh hơn class bởi struct sử dụng method dispatch là static dispatch, class sử dụng dynamic dispatch. Ngoài ra, struct lưu dữ liệu trong stack, còn class sử dụng stack + heap -> Xử lí trong class sẽ lâu hơn.
    • Class là 1 reference type. Do đó, nếu không cẩn thận khi truyền biến sẽ dễ gây ra lỗi ngoài ý muốn ( Xem phần value type vs reference type ở trên). -> Sử dụng struct sẽ an toàn hơn.

    Nên sử dụng class khi:

    • Cần sử dụng kế thừa.
    • Cần sử dụng reference type

    Kết luận:

    Việc phân biệt được sự khác nhau giữa class và struct vô cùng quan trọng để có thể sử dụng cho đúng cách. Hi vọng qua bài viết, các bạn sẽ hiểu rõ hơn được về sự khác biệt giữa class và struct.

  • Operation (P3)

    Operation (P3)

    Đến với phần cuối của loạt bài viết về Operation nhưng cũng không kém phần quan trọng, mình sẽ nói về Async Operation và Cancel Operation.

    Nội dung bài viết:

    • Cancel Operation
    • Async Operation
    • Demo

    Cancel Operation

    • Cách dùng khá đơn giản, chỉ cần gọi cancel() để cancel 1 operation. Tuy nhiên, 1 operation chỉ có thể bị cancel trước khi nó được bắt đầu được thực hiện.
    • Bản chất của việc gọi cancel là sẽ set state của operation thành isCancelled = true.
    operation.cancel()

    Cancel toàn bộ operation:

    Để cancel toàn bộ các operations trong 1 operation queue, chỉ cần gọi:

    operationQueue.cancelAllOperations()

    Note: Chỉ cancel những task chưa được thực hiện

    Vậy làm thế nào có thể cancel 1 operation đang thực hiện? Câu trả lời là sẽ kiểm tra state isCancelled trong thân hàm.
    Hãy xem ví dụ ở cuối bài để hiểu thêm.

    Async operation

    1 Operation nếu được khởi tạo default thì sẽ hoạt động theo kiểu synchronous. 1 vòng đời của operation khi đó theo các state là: isReady -> isExecuting -> isFinished.

    Nhưng nếu bạn muốn các operation của bạn hoạt động theo kiểu asynchoronous bởi diều đó chắc chắn sẽ khiến app của bạn họat động nhanh hơn?
    Điều đó là hoàn toàn có thể, tuy nhiên nếu operation hoạt động kiểu async, nó sẽ trả về quyền điều khiển ngay lập tức (xem lại bài GCD part 1). Vì vậy, state của operation đó sẽ trở thành isFinished ngay lập tức. Do đó, bạn sẽ phải làm thêm 1 vài việc để custom lại state của operation nếu bạn muốn operation đó chạy theo kiểu async.

    Cách làm ở đây về cơ bản là sẽ viết 1 async operation subclass để quản lí state, và giao tiếp với lớp cha Operation của nó thông qua KVO.
    Nghe có vẻ khá dài, nhưng đừng lo lắng, chỉ cần làm 1 lần thôi, lần sau bạn sẽ chỉ cần gọi và dùng.

    class AsyncOperation: Operation {
        // State enumaration
        enum State: String {
            case ready, excuting, finished
    
            // 1
            fileprivate var keyPath: String {
                return "is" + rawValue.capitalized
            }
        }
        // State property
        // 2
        var state: State = .ready {
            // 3
            willSet {
                debugPrint("New value \(newValue)")
                willChangeValue(forKey: newValue.keyPath)
                debugPrint("Will change value 1 for key \(newValue.keyPath)")
                willChangeValue(forKey: state.keyPath)
                debugPrint("Will change value 2 for key \(state.keyPath)")
            }
            didSet {
                debugPrint("old value \(oldValue)")
                didChangeValue(forKey: oldValue.keyPath)
                debugPrint("Did change value 1 for key \(oldValue.keyPath)")
                didChangeValue(forKey: state.keyPath)
                debugPrint("Did change value 2 for key \(state.keyPath)")
            }
        }
    }
    1. State của operation default sẽ là ready
    2. keyPath là 1 computed property sẽ giúp bạn lấy state hiện tại của operation.
    3. Bởi vì bạn cần gửi các notification khi bạn thay đổi state, bạn sẽ dùng didSet và willSet để hứng notification. -> Trong nhiều trường hợp có thể bạn sẽ không cần dùng đến, nhưng nên khai báo để dễ dàng quan sát, debug.

    Base Properties

    extension AsyncOperation {
        // 1
        open override var isReady: Bool {
            return super.isReady && state == .ready
        }
    
        open override var isExecuting: Bool {
            return state == .excuting
        }
    
    
        open override var isFinished: Bool {
            return state == .finished
        }
        // 2
        open override var isAsynchronous: Bool {
            return true
        }
        // 3
        open override func start() {
            // 4
            if isCancelled {
                state = .finished
                return
            }
    
            main()
        }
    
        open override func cancel() {
            super.cancel()
            state = .finished
        }
    }
    1. override lại các state vì bạn muốn tự quản lí state cho riêng mình.
    2. set thuộc tính isAsynchronous thành true để operation chạy async.
    3. override lại func start(). Khi đưa 1 operation vào queue, queue sẽ gọi start để thực hiện chạy 1 operation.
    4. Kiểm tra trước khi thực hiện xem operation này đã bị cancel chưa, nếu chưa thì sẽ bắt đầu thực hiện.

    Note: Không bao giờ gọi super.start() trong hàm start().
    Refer: https://developer.apple.com/documentation/foundation/operation/1416837-start

    Custom 1 Operation

    Mình sẽ custom 1 opeartion dùng để download ảnh, và check state isCancelled trong thân hàm để có thể dừng 1 operation đang chạy.

    1. Override lại hàm main cho operation. Đây là hàm operation sẽ chạy vào khi thực được gọi để thực hiện.
    2. set state thành .executing khi bắt đầu thực hiện operation.
    3. Luôn nhớ set state thành .finished khi operation được thực hiện xong
    1. check nếu operation đã bị cancel thì sẽ không download image.
    2. check nếu opeartion đã bị cancel thì sẽ không convert data thành image.
    3. Ở đây bạn có thể check nếu operation đã bị cancel thì không hiển thị image cũng được, tuy nhiên mình nghĩ download xong image rồi thì hiển thị cũng ok.

    Kết luận:

    • Operation thích hợp để dùng hơn GCD khi những task của bạn đòi hỏi sự kiểm soát state, hoặc những task có tính reuseable cao. Còn với những task đơn giản, hoặc không cần reuse nhiều thì có thể dùng GCD.
    • Operation là API bậc cao hơn GCD, nên cách dùng sẽ khó hiểu hơn, nên hãy cẩn trọng khi kiểm soát state của các custom Operation.

    Tham khảo: Ray wenderlich
    End

  • Operation (P2)

    Operation (P2)

    Ở phần 1 của bài viết, mình đã giới thiệu về Operation là gì. Ở phần 2 của loạt bài về Operation, mình sẽ nói về Dependency trong Operation.

    Nội dung bài viết:

    • Operation Dependencies
    • Passing Data using Dependencies

    Operation Dependencies:

    Operation cho phép bạn thiết lập các sự phụ thuộc lẫn nhau. Điều này mang lại 2 lợi ích:

    • Giả sử operation 2 phụ thuộc vào operation 1. Khi đó operation 2 chỉ được thực hiện sau khi operation 1 đã hoàn thành.
    • Cung cấp 1 cách để bạn truyền data giữa các operation.
    class DownloadImage: Operation {
        var index: Int
        
        init(ind: Int) {
            self.index = ind
        }
    
        override func main() {
            // Download image task
            print("Start downloading task \(index) at time: \(Date().timeIntervalSince1970)")
            sleep(5)
            print("Finish downloading task \(index) at time: \(Date().timeIntervalSince1970)")
        }
    }
    
    let firstOperation = DownloadImage(ind: 1)
    let secondOperation = DownloadImage(ind: 2)
    //firstOperation.addDependency(secondOperation)
    
    let operationQueue = OperationQueue()
    operationQueue.addOperation(firstOperation)
    operationQueue.addOperation(secondOperation)

    Ở trên là đoạn code khởi tạo 2 operation bình thường, giữa chúng chưa có dependency. Chạy đoạn code trên và đây là kết quả thu được:

    2 task chạy song song

    Giờ thì bỏ comment dòng code firstOperation.addDependency(secondOperation) và chạy thử:

    Task 1 phụ thuộc vào task 2. Vì vậy, task 1 chỉ chạy khi task 2 đã hoàn thành.

    Note: Bạn có thể tạo dependency cho 2 opeartion đang chạy ở 2 operation queue khác nhau.

    Đây là 1 cách khá ngắn trong khi ở GCD bạn phải khai báo 1 dispatchGroup, sau đó gọi hàm enter(), leave(), notify(), … Tuy nhiên, cách làm này rất dễ gây ra deadlock.

    Để remove 1 dependency, bạn chỉ cần gọi:

    firstOperation.removeDependency(op: secondOperation)
    Task 5 chỉ được chạy khi task 2 hoàn thành, task 2 chỉ chạy khi task 3 hoàn thành, task 3 chỉ chạy khi task 5 hoàn thành.

    Ở đoạn code mẫu ở trên, nếu bạn sửa đoạn code thành như dưới đây thì code của bạn sẽ bị deadlock và không chạy.

    firstOperation.addDependency(secondOperation)
    secondOperation.addDependency(firstOperation)

    Note: Hãy vẽ sơ đồ ra 1 tờ giấy để luôn clear về flow của bạn, tránh bị deadlock.

    Truyền data giữa các operation thông qua dependency:

    class Calculate: Operation {
        let firstNum: Int
        let secondNum: Int
        var sum: Int?
        
        init(first: Int, second: Int) {
            self.firstNum = first
            self.secondNum = second
        }
        
        override func main() {
            sum = firstNum + secondNum
        }
    }
    
    class Display: Operation {
        
        override func main() {
            // 2
            let sum = dependencies.compactMap{ ($0 as? Calculate)?.sum }.first
            
            guard let unwrappedSum = sum else {
                return
            }
            // 3
            print("Sum = \(unwrappedSum)")
        }
    }
    
    let calculateOperation = Calculate(first: 5, second: 10)
    let displayOperation = Display()
    // 1
    displayOperation.addDependency(calculateOperation)
    
    
    let operationQueue = OperationQueue()
    // 4
    operationQueue.addOperation(calculateOperation)
    operationQueue.addOperation(displayOperation)
    1. Khởi tạo 2 operation, và set dependency để task Display chỉ chạy sau khi task Calculate hoàn thành.
    2. Hàm main() là hãm sẽ chạy khi 1 operation được chạy.
      Ở đây, ta sẽ lấy ra list operations có dependency với DisplayOperation, chọn ra operation nào là Calculate và lấy ra sum.
    3. Nếu sum khác nil thì hiển thị ra.
    4. Add các operation vào queue để chạy.

    Kết quả hiển thị trên màn hình Console:

    Dependency trong Operation là 1 trong những thứ giúp Operation vượt trội hơn so với GCD.

    Ở phần tiếp, mình sẽ nói về Async Operation và xử lí cancel Opeartion.

  • Operation (P1)

    Operation (P1)

    Cũng giống như GCD, Operations giúp chúng ta có thể thực hiện các task đa luồng. Vì Operations là API bậc cao hơn GCD, do đó, Operations cung cấp nhiều tính năng hơn so với GCD như cung cấp các state để quản lí task, dependency giữa các task,… nhưng cũng vì thế mà để khơi tạo và sử dụng operation sẽ khó hơn so với GCD.

    Nếu bạn chưa hiểu về GCD, hãy đọc bài về GCD của mình tại:

    Ở phần 1 của bài viết Operation, mình sẽ giới thiệu về tính chất và các thuộc tính của Operation và Operation queue.

    Nội dung bài viết

    • Operation State
    • Block Operation
    • Subclass Operation
    • Operation Queue

    Operation States

    • Operation cũng có công dụng như là 1 DispatchWorkItem vậy. Chúng đều là 1 task, bạn khai báo thân hàm cho chúng và đưa chúng vào queue để thực hiện.
    • Tuy nhiên, điểm vượt trội của Operation so với DispatchWorkItem là mỗi Operation đều có State của riêng mình, còn workItem thì không.
    • isReady: sẵn sàng để được thực hiện
    • isExecuting: đang được thực hiện
    • isCancelled: Nếu bạn gọi phương thức cancel(), operation sẽ chuyển qua state isCancelled trước khi chuyển sang state isFinished.
    • isFinished: Nếu operation k bị cancel, nó sẽ chuyển từ state isExecuting sang isFinished khi hoàn thành.

    Note:

    • Các state của Operation là các thuộc tính read-only.
    • Bạn có thể gọi cancel() để hủy 1 operation, nhưng tương tự như workItem, chúng chỉ có thể bị cancel trước khi được thực hiện. Tuy nhiên, do Operation có state, nên bạn có thể kiểm tra state trong thân hàm để cancel hàm. Mình sẽ làm rõ hơn ở phần sau.

    Block Operation

    Có thể tạo 1 operation 1 cách nhanh chóng bằng cách sử dụng 1 block operation:

    Gọi hàm start() để thực hiện BlockOperation

    Bạn cũng có thể add thêm nhiều closures vào block operation:

    Block Operation hoạt động như một DispatchGroup. Nếu bạn cung cấp 1 completionBlock closure cho blockOperation, thì nó sẽ được thực hiên khi tất cả các closure được add vào block operation đã được thực hiện hết (tương tự hàm notify của DispatchGroup).

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

    Note: Từ kết quả thu được ở trên, ta có thể dễ dàng kết luận:

    • Task trong block operation chạy concurrent.
    • Operation Blocks chạy default trên global queue.

    Subclass Operation

    BlockOperation rất dễ sử dụng và thích hợp cho các task đơn giản. Tuy nhiên, đối với những task phức tạp cần kiểm soát state, hoặc những task sử dụng nhiều lần, thì bạn nên tự tạo 1 Operation để sử dụng.

    • Bạn phải override lại hàm main(), đây là hàm được thực hiện khi operation đó bắt đầu thực hiện.
    • Gọi start() để chạy. Tuy nhiên, lưu í rằng, khi gọi start() 1 cách trực tiếp trên 1 operation, Operation đó sẽ chạy theo kiểu sync trên current thread. -> Không nên gọi start trên main thread.

    Note: Không chỉ việc gọi start sẽ block current thread, mà nó còn dễ dẫn tới 1 exception rằng task đó chưa sẵn sàng để thực hiện. Vì vậy bạn không nên gọi start ở 1 operation.

    Vậy làm cách nào để có thể start 1 operation ? Câu trả lời cho việc này là tương tự như DispatchWorkItem, chúng ta sẽ đưa operation vào Operation Queue để chạy.

    Operation Queue:

    Bạn đưa các task vào trong operation queue, operation queue sẽ tự động thực hiện task khi thích hợp mà bạn không cần phải gọi start.

    • Operation Queue chỉ thực hiện task khi task đó đang ở state isReady.
    • Khi bạn add 1 operation vào 1 queue, task đó sẽ chạy cho đến khi hoàn thành hoặc bị hủy (cancel).
    • Không thể add 1 operation vào nhiều operation queues khác nhau.

    Block queue:

    • Operation queue có 1 thuộc tính là waitUntilAllOperationsAreFinished, có tác dụng block queue hiện tại, vì vậy bạn sẽ không nên gọi trên main thread.
    • Trong trường hợp bạn không muốn block cả 1 queue chỉ để đợi 1 task thực hiện xong mà bạn chỉ muốn block 1 vài task, bạn có thể gọi addOperations(_:waitUntilFinished:) method on OperationQueue.

    Tạm dừng queue:

    Bạn có thể tạm dừng operation queue bằng cách set isSuspend = true.
    -> Những task đang được thực hiện vẫn sẽ dc tiếp tục thực hiện, nhưng những task chưa dc thực hiện thì sẽ đợi cho đến khi isSuspend được set lại thành false.

    Set số lượng operations tối đa

    Còn nếu như bạn muốn giới hạn số lượng tối đa operations chạy trong 1 Operation Queue tại 1 thời điểm, chẳng hạn như việc chỉ load những ảnh của những visible cell của collectionView chẳng hạn?
    Khi đó, bạn chỉ cần set thuộc tính maxConcurrentOperationCount của Operation Queue thành 1 số bạn muốn.

    class DownloadImage: Operation {
        override func main() {
            // Download image task
            print("Start downloading... at time: \(Date().timeIntervalSince1970)")
            sleep(5)
            print("Finish downloading... at time: \(Date().timeIntervalSince1970)")
        }
    }
    
    let operation = DownloadImage()
    let operation2 = DownloadImage()
    
    let operationQueue = OperationQueue()
    operationQueue.maxConcurrentOperationCount = 1
    operationQueue.addOperations([operation, operation2], waitUntilFinished: true)

    Nếu set maxConcurrentOperationCount = 1, thì task 1 chạy xong, sau đó task 2 mới chạy. Đây là kết quả trên màn hình console:

    Nếu set maxConcurrentOperationCount = 2, thì 2 task sẽ chạy song song. Đây là kết quả trên màn hình console:

    Nội dung phần tiếp:

    • Dependency Operation

    Nguồn tham khảo: Ray Wenderlich

  • Grand Central Dispatch (Part 3)

    Grand Central Dispatch (Part 3)

    Nội dung bài viết:

    • DispatchBarrier
    • DispatchWorkItem
    • Thread Sanitizer

    Dispatch Barrier

    • 1 trong những vấn đề tiêu biểu của đa luồng như đã đề cập ở phần 1 của bài viết là Race Condition.
    • Có 1 cách đơn giản để tránh Race Condition, đó là dùng serial queue để thực hiện các task, khi đó chỉ được 1 task được thực hiện tại mỗi thời điểm, nhưng sẽ làm app chậm lại vì chạy trên serial queue.
    • Vậy nếu bạn muốn các task được thực hiện concurrent, nhưng khi có 1 task thay đổi tài nguyên chung thì chỉ duy nhất task đó được thực hiện tại thời điểm đó?

    Đừng lo, GCD cung cấp cho bạn DispatchBarrier để xử lí điều đó 1 cách đơn giản.

    Note: Khi 1 task tiến hành việc thay đổi share resources, sẽ có 1 barrier đến, ngăn không cho bất kì 1 task mới nào được thực hiện trong thời gian này. Khi task đó hoàn thành việc thay đổi tài nguyên, barrier mất đi, các tasks tiếp tục chạy concurrent.

    Ví dụ sau đây là cách sử dụng Dispatch Barrier để tránh Race Condition.
    Giả sử bạn có 1 class Person như sau:

    Dưới đây là đoạn code nhiều task khác nhau cùng thay đổi share resources.

    let queue = DispatchQueue(label: "01", attributes: .concurrent)
    let nameChangeGroup = DispatchGroup()
    
    let person = Person(firstName: "Hoang", lastName: "Tuan")
    let nameList = [("A","B"),("C","D"),("E","F"),("G","H"),("I","J")]
    
    for (_, name) in nameList.enumerated() {
        queue.async(group: nameChangeGroup) {
            usleep(UInt32(10000 * idx))
            person.changeName(firstName: name.0, lastName: name.1)
            print("Current name: \(person.name)")
        }
    }
    
    nameChangeGroup.notify(queue: .main) {
        print("Final name: \(person.name)")
    }

    Và đây là kết quả được hiển thị:

    Có thể dễ dàng thấy, các task thực hiện thay đổi tài nguyên cùng thời điểm gây ra sai lệch kết quả. Đồng thời, kết quả đều khác nhau sau mỗi lần Run.

    Đó là lúc chúng ta sẽ sử dụng Dispatch Barrier để xử lí việc này.

    1. Thực hiện việc get name theo sync để không cho các task khác có thể đọc tài nguyên khi có 1 task đang đọc, vì có thể task đang đọc có thể sẽ chỉnh sửa tài nguyên sau đó.
    2. Đưa toàn bộ thân hàm của func changeName vào thực hiện trên luồng isolationQueue1 với flags là 1 barrier. Vì vậy, khi excute changeName, 1 barrier sẽ xuất hiện và không để bất kì task mới nào được thực hiện cho đến khi changeName hoàn thành.

    Và đây là kết quả:

    Kết quả khi đó được thực hiện đúng

    DispatchWorkItem

    DispatchWorkItem là những block of code. Điều khác biệt khi sử dụng DispatchWorkItem là bạn có thể cancel những task đang trong queue.

    Note: Bạn chỉ có thể cancel 1 DispatchWorkItem trước khi nó đi đến đầu queue và bắt đầu execute.

    Khởi tạo 1 DispatchWorkItem:

    let workItem = DispatchWorkItem(qos: .background, flags: .inheritQoS) {
         guard let url = URL(string: self.links[i]) else {
             return
         }
         URLSession.shared.dataTask(with: url) { (data, res, err) in
             guard let imageData = data else {
                 return
             }
             let myImage = UIImage(data: imageData)
             DispatchQueue.main.async {
                 self.listImage[i].image = myImage
             }
         }.resume()
    }

    Đưa DispatchWorkItem vào queue để thực hiện:

    DispatchQueue.global().async(execute: workItem)

    Cancel 1 DispatchWorkItem:

    workItem.cancel()

    Thread Sanitizer

    Sử dụng Thread Sanitizer để phát hiện & debug race condition:

    • Chọn Product>Scheme>Edit Scheme
    • Chọn vào "Thread Sanitizer":

    Khi đó, nếu code của bạn bị race condition, xcode sẽ thông báo cho bạn:

    1 tool khá hay để phát hiện race condition

    Kết luận: GCD là 1 API đỉnh và dễ sử dụng để quản lí multitask, đa luồng, nhưng có 1 nhược điểm là không cung cấp các state của task trong trường hợp bạn muốn quản lí sâu hơn. Khi đó, bạn phải tự custom State cho riêng mình, hoặc dùng Operation.

    -End-