Category: Swift

  • Grand Central Dispatch (Part 2)

    Grand Central Dispatch (Part 2)

    Nội dung bài viết:

    • Dispatch Group
    • Semaphores

    Dispatch Group (cont):

    Ở phần trước, bạn đã biết về cơ chế enter-leave-notify của DispatchGroup để nhận biết khi các task trong group đã hoàn thành.
    Vậy nếu bạn muốn block luồng hiện tại cho đến khi các tasks trong group hoàn thành hoặc timeout thì sao? Ngoài cách sử dụng 1 vài logic, thì cách đơn giản hơn là dùng phương thức wait do DispatchGroup cung cấp.

    Phương thức wait của DispatchGroup là 1 sync function nên nó có tác dụng block luồng hiện tại cho đến khi mọi tasks trong group đã được thực hiện xong, hoặc timeout.

    let group = DispatchGroup()
    let queue = DispatchQueue.global(qos: .userInitiated)
    
    var formatter: DateFormatter = DateFormatter()
    formatter.dateFormat = "HH:mm:ss"
    
    // 1
    print("Start task 1 at time: \(formatter.string(from: Date()))")
    queue.async(group: group) {
        Thread.sleep(until: Date().addingTimeInterval(10))
        print("End task 1 at time: \(formatter.string(from: Date()))")
    }
    
    // 2
    print("Start task 2 at time: \(formatter.string(from: Date()))")
    queue.async(group: group) {
        Thread.sleep(until: Date().addingTimeInterval(2))
        print("End task 2 at time: \(formatter.string(from: Date()))")
    }
    
    // 3
    print("Start call time out at time: \(formatter.string(from: Date()))")
    if group.wait(timeout: .now() + 5) == .timedOut {
        print("Time out at time: \(formatter.string(from: Date()))")
    } else {
        print("All the jobs have completed at time: \(formatter.string(from: Date()))")
    }
    
    // 4
    queue.async(group: group) {
        print("Try to do task 3 at time: \(formatter.string(from: Date()))")
    }
    1. Bạn khởi tạo task đầu tiên, đưa vào queue để thực hiện theo cách async.
    2. Bạn khởi tạo task thứ 2, đưa vào queue để thực hiện theo cách async.
    3. Bạn khai báo hàm wait() với timeOut = 5s. Hàm sẽ block luồng hiện tại cho đến khi các task thực hiện xong hoặc sau 5s.
    4. Khởi tạo 1 task thứ 3, đưa vào queue để thực hiện theo cách async
    Mang đoạn code vào playground, thử sửa thời gian để hoàn thành task 1 thành 1s, xem điều gì sẽ xảy ra 😉
    • Sau 2s kể từ lúc bắt đầu, task 2 hoàn thành
    • Khi gọi wait() thì luồng hiện tại sẽ bị block -> Không thực hiện được tiếp task thứ 3 trong khoảng thời gian đó.
    • Sau 5s kể từ lúc Start time out, các task trong group chưa hoàn thành hết do task 1 cần 10s để thực hiện, vì vậy nên dispatchGroup sẽ trả quyền điều khiển về cho luồng.
    • Ngay sau khi luồng được trả quyền điều khiển, luồng thực hiện ngay lập tức task 3.
    • Sau 10s kể từ lúc bắt đầu, task 1 hoàn thành.

    Note: Vì wait() block luồng gọi nó, nên bạn không bao giờ nên gọi wait ở main queue.

    Semaphores

    • Giả sử bạn có nhiều task cùng truy cập vào 1 tài nguyên chung, và bạn muốn giới hạn số lượng task truy cập vào tài nguyên chung đó tại mỗi thời điểm. -> GCD cung cấp cho bạn Semaphore để giúp bạn xử lí việc này dễ dàng.
    • semaphore cung cấp 2 phương thức là wait()signal(). Chúng tương tự như enter() và leave() của dispatchGroup. wait() sẽ gọi khi bắt đầu 1 task và gọi signal() để thông báo rằng task đã hoàn thành.

    Giả sử bạn có 10 task cùng thực hiện 1 lúc, nhưng bạn muốn chỉ có tối đa 4 task chạy tại 1 thời điểm( ví dụ như load ảnh) -> Semaphore sẽ giúp bạn

    let group = DispatchGroup()
    let queue = DispatchQueue.global(qos: .userInitiated)
    
    // 1
    let semaphore = DispatchSemaphore(value: 4)
    
    // 2
    for i in 1...10 {
        // 3
        queue.async(group: group) {
            // 4
            semaphore.wait()
            // 5
            defer {
                semaphore.signal()
            }
            print(" Start task \(i) at time: \(Date().timeIntervalSince1970)")
    
            Thread.sleep(forTimeInterval: 3)
            print("Finish task \(i) at time: \(Date().timeIntervalSince1970)")
        }
    }
    1. Khởi tạo 1 semaphore, giá trị value biểu thị cho số lượng task tối đa chạy trong 1 thời điểm.
    2. Tạo 1 vòng lặp gồm 10 task chạy.
    3. Mỗi task sẽ chạy theo kiểu async.
    4. gọi hàm wait() để thông báo là task bắt đầu chạy.
    5. gọi signal() để thông báo là task đã hoàn thành.
    Mang đoạn code vào playground, thử không gọi signal() hoặc wait() xem điều gì sẽ xảy ra
    • Task 1, 2, 3, 4 bắt đầu được chạy đầu tiên.
    • Khi task 2, 3, 1 hoàn thành, task 5, 6, 7 ngay lập tức được thực hiện.
    • Task 4 hoàn thành, task 8 ngay lập tức được thực hiện.

      -> Có thể thấy, tại mỗi thời điểm chỉ có tối đa 4 task chạy.

    Note:

    • Luôn nhớ gọi hàm signal() khi task được thực hiện xong, nếu không thì semaphore sẽ không biết task đã hoàn thành để thực hiện task mới.
    • Gọi wait() khi bắt đầu 1 task để thông báo cho semaphore rằng 1 task mới sẽ bắt đầu thực hiện.

    Ở bài viết tiếp theo, mình sẽ giới thiệu về DispatchBarrier, DispatchWorkItem và Thread Sanitizer.

    Nguồn tham khảo: Ray Wenderlich & Lets Build that app.

  • Grand Central Dispatch (Part 1)

    Grand Central Dispatch (Part 1)

    Tổng quan:

    Để app có UI, UX mượt mà và nhanh, ta cần chia các tasks cần nhiều thời gian chạy ra các luồng khác để thực hiện -> GCD là 1 trong các cách giúp các bạn quản lí các luồng đó.

    Nội dung bài viết:

    • 1 số khái niệm cơ bản về luồng, hàng đợi.
    • Cách tạo luồng bằng GCD.
    • Các vấn đề hay gặp phải trong quản lí luồng.
    • Các basic func của GCD.

    Giới thiệu 1 số khái niệm cơ bản về luồng, hàng đợi:

    Thread

    • Luồng là nơi các task được thực hiện.
    • 1 app hoặc 1 chương trình sẽ có 1 hoặc nhiều luồng.
    • Mỗi luồng có thể thực hiện đồng thời, tuy nhiên nó tùy thuộc vào hệ thống để biết khi nào nó được thực hiện và được thực hiện thế nào.
    • GCD được xây dựng dựa trên các luồng. Với GCD, bạn đưa các task vào dispatch queue và GCD sẽ tự quyết định luồng nào được dùng để thực hiện các task.

    Queue

    • Queue là hàng đợi, hoạt động theo cơ chế FIFO.
    • Bạn đưa các task vào queue, và GCD sẽ thực hiện chúng theo cơ chế FIFO.
    • Bản thân Dispatch Queues đã là thread safe nên bạn có thể đưa task vào queue từ bất kì luồng nào.
    • Vì GCD sẽ tự động chọn luồng để thực hiện các task giúp bạn, nên việc của bạn là chỉ cần chọn loại queue phù hợp để đưa task vào.

    Có 2 loại queue là SerialConcurrent.
    Serial: hàng đợi chỉ thực hiện 1 task vào 1 thời điểm, task này xong thì thực hiện task tiếp theo.
    Concurrent: hàng đợi thực hiện nhiều task tại 1 thời điểm, các task vẫn được thực hiện theo thứ tự FIFO, nhưng bạn sẽ không biết số task thực hiện tại 1 thời điểm hay thời điểm các task hoàn thành.

    Serial Queue và Concurrent Queue

    Với GCD, bạn có thể thực hiện các task async hoặc sync.
    sync: 1 task được thực hiện kiểu synchronous sẽ trả quyền điều khiển cho hàm gọi chúng sau khi task được hoàn thành.
    async: 1 task được thực hiện kiểu asynchronus sẽ trả quyền điều khiển ngay lập tức mà không cần đợi phải hoàn thành. Vì vậy, 1 async task sẽ không block luồng hiện tại. Luồng hiện tại sẽ được trả quyền điều khiển để thực hiện task tiếp theo.

    Cách tạo queue với GCD:

    1. Khởi tạo: GCD cung cấp cách khởi tạo luồng với các quality of Service khác nhau như sau:

    Swift gồm 5 loại quality of Service được sử dụng cho các mục đích khác nhau:

    Cách gọi ra Main queue – đây là nơi để bạn thực hiện các task updates UI:

    DispatchQueue.main

    Cách tạo ra 1 private queue: (mặc định là serial queue)

    let serialQueue = DispatchQueue(label: "techOver")

    Để tạo ra 1 queue kiểu concurrent thì bạn set thuộc tính attribute kiểu concurrent:

    let serialQueue = DispatchQueue(label: "techOver", attributes: .concurrent)

    2. Thêm task vào các queue với kiểu thực hiện sync hoặc async:

    DispatchQueue.global().async {
       // do expensive non-UI task
       DispatchQueue.main.async {
          // do updates UI when task is finished
       }
    }

    Các vấn đề hay gặp về quản lí luồng:

    priority inversion: Các task với priority cao sẽ được thực hiện trước các task có priority thấp, tuy nhiên các task có priority thấp lại lock resource, dẫn đến các task với priority cao phải đợi để được thực hiện -> Làm app chậm.
    Race condition: 2 task cùng truy cập để 1 sửa 1 tài nguyên chung trong cùng 1 thời điểm, dẫn đến sai lệch kết quả.

    Deadlock: Xảy ra khi 2 tasks đang đợi lẫn nhau giải phóng tài nguyên để có thể thực hiện tiếp -> Dẫn đến việc cả 2 tasks đều không thể thực hiện -> Treo app.

    Các basic func của GCD:

    1. Dispatch Group: Giả sử bạn có nhiều task được thực hiện theo kiểu async, khi đó bạn sẽ không thể biết khi nào chúng hoàn tất bởi chúng trả về quyền điều khiển ngay lập tức -> Dispatch Group dùng để nhận biết khi 1 nhóm các task được thực hiện xong.

    // 1
    let dispatchGroup = DispatchGroup()
    // 2
    let viewContainer = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
    viewContainer.backgroundColor = .red
    view.addSubview(viewContainer)
    
    // A box move around in the view
    let box = UIView(frame: CGRect(x: 0, y: 0, width: 60, height: 60))
    box.backgroundColor = .yellow
    viewContainer.addSubview(box)
    
    // A label is appear when all the animations finish
    let label = UILabel(frame: CGRect(x: 15, y: 100, width: 360, height: 40))
    label.font = label.font.withSize(50)
    label.text = "All Done!"
    label.textColor = .yellow
    label.textAlignment = .center
    label.isHidden = true
    viewContainer.addSubview(label)
    
    // Animations
    // 3
    dispatchGroup.enter()
    UIView.animate(withDuration: 2, animations: {
        box.center = CGPoint(x: 300, y: 300)
    }, completion: { _ in
        UIView.animate(withDuration: 3, animations: {
            box.transform = CGAffineTransform(rotationAngle: .pi/4)
        }, completion: { _ in
             // 4
             self.dispatchGroup.leave()
        })
    })
    // 5        
    dispatchGroup.notify(queue: .main) {
        UIView.animate(withDuration: 2, animations: {
            viewContainer.backgroundColor = .blue
        }, completion: { _ in
            label.isHidden = false
        })
    }

    Note: Thử đưa đoạn code này vào viewDidLoad và run thử

    1. Khởi tạo 1 DispatchGroup.
    2. Khởi tạo 1 vài view để thực hiện animation.
    3. Gọi hàm enter() để đưa task cần thực hiện vào trong group.
    4. Gọi hàm leave() để báo với group rằng task đã thực hiện xong.
    5. Khai báo xem hàm notify sẽ làm gì khi các task trong group được thực hiện xong. Hàm notify sẽ được tự động gọi đến khi tất cả các task được đưa vào group đã được thực hiện xong.

    Note: Số lần gọi enter() phải bằng số lần gọi hàm leave().

    Kết luận: Thay vì phải đặt delay giữa các animation thì chúng ta có thể dùng DispatchGroup để nhận biết khi 1 animation đã thực hiện xong để thực hiện tiếp animation khác.

    Ở bài viết tiếp theo sẽ tiếp tục là về các basic func và 1 vài advance func của GCD.

  • Swift Generics (Part 2)

    Swift Generics (Part 2)

    Ở phần 1 của bài viết, bạn đã hiểu được Generic là gì, công dụng và cách sử dụng cơ bản của Generic.
    Ở phần 2 của bài viết, bạn sẽ học được:

    • Extending a generic type.
    • Subclass a generic type.
    • Cách sử dụng generic nâng cao.

    Extending a generic type.

    Giả sử bạn có 1 struct Package như sau:

    struct Package<Item> {
        var items = [Item]()
        mutating func push(item: Item) {
            items.append(item)
        }
        
        mutating func popLast() -> Item {
           return items.removeLast()
        }
    }

    Ở đây, Item là 1 generic. Khi bạn extending 1 generic type, bạn không cần khai báo lại <Item>. Item là kiểu dữ liệu generic đã được khai báo trong toàn bộ struct.

    extension Package {
        var firstItem: Item? {
           return items.isEmpty ? nil : items[0]
        }
    }

    extension with a generic where clause

    Giả sử bạn muốn khai báo func isFirstItem(:) như sau:

    Lỗi này tương tự như ở phần 1, khi không phải mọi kiểu dữ liệu trong Swift đều có thể sử dụng toán tử (==) để so sánh. Vậy ở đây, bạn có thể extend generic với 1 mệnh đề where như sau:

    Vì kiểu dữ liệu truyền vào của biến bPerson, mà Person thì không tuân theo Equatable, nên b không có func isFirstItem(:)

    Note: Việc extend 1 generic type với mệnh đề where cho phép bạn thêm 1 điều kiện mới cho extension, vì vậy func isFirstItem(:) chỉ được thêm vào extension khi Item thuộc kiểu Equatable.

    Generic subclass

    Có 2 cách để kế thừa 1 class cha là kiểu generic như sau:

    • Tạo 1 class con kế thừa những thuộc tính và func của class cha nhưng vẫn giữ cho class con là kiểu generic.
    • Tạo 1 class con nhưng là 1 class cụ thể.

    Ở trong ví dụ trên, dễ thấy:

    • Box là một generic subclass của class Package vì bạn muốn chiếc hộp này đựng được bất kì thứ gì, vì vậy Box vẫn là kiểu generic nên khi khởi tạo bạn phải truyền vào một kiểu dữ liệu cụ thể.
    • RosePackage là một subclass cụ thể của class Package, gói RosePackage này sẽ dùng chỉ để đựng hoa hồng.

    Sử dụng generic nâng cao

    Mọi table view cơ bản đều có các func sau:

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
    }
    ...

    Trong trường hợp bạn muốn tạo các tableview khác nhau, thì sẽ dẫn đến việc duplicate các đoạn code này.
    -> Sử dụng generic để giảm thiểu duplicate code.
    Example1:

    RoseTableViewController là 1 subclass cụ thể của 1 generic class.

    Ở đây, bạn sử dụng generic để giảm duplicate code như sau:

    • Tạo 1 generic class tên BaseTableViewController chứa các thuộc tính, func cơ bản, và khai báo 1 placeholder T kiểu UITableViewCell để dễ dàng truyền các kiểu cell khác nhau vào.
    • Tạo 1 class RoseCell kiểu UITableViewCell.
    • Tạo class RoseTableViewController extends BaseTableViewController, class này dùng cell kiểu RoseCell nên chúng ta truyền kiểu RoseCell vào.
    • override lại các thuộc tính -> func numberOfRows tự thay đổi theo.

    Example2 : Trong trường hợp bạn muốn mỗi cell có item kiểu dữ liệu bất kì, bạn có thể khai báo 1 class BaseCell kiểu generic, rồi khởi tạo các cell mới extends BaseCell.

    Note: U ở phần khai báo BaseTableViewController và U ở phần khai báo BaseCell là khác nhau, ở đây mình đặt đều là U cho bạn đọc đỡ confuse.

  • Swift Generics (Part 1)

    Swift Generics (Part 1)

    Generic cho phép bạn viết function và type 1 cách linh hoạt, dễ dàng tái sử dụng, có thể hoạt động với bất kì loại nào tùy theo các yêu cầu mà bạn xác định. Với generic, bạn có thể tránh sự trùng lặp của code mà vẫn thể hiện í định của nó 1 cách rõ ràng.

    Ở phần 1 của bài viết, bạn sẽ học được:

    • Generics là gì ?
    • Tại sao chúng hữu dụng ?
    • Cách sử dụng generic đơn giản.

    Getting Started

    Giả sử bạn được yêu cầu viết 1 phương thức tính tổng 2 số với kiểu dữ liệu bất kì. Thông thường, bạn sẽ viết các func riêng cho các kiểu dữ liệu riêng.

    func addInt(num1: Int, num2: Int) -> Int {
        return num1 + num2
    }
    
    func addDouble(num1: Double, num2: Double) -> Double {
        return num1 + num2
    }

    Dễ dàng nhận thấy, func addInt(::) và func addDouble(::) là 2 func khác nhau nhưng có thân hàm giống nhau. Bạn không chỉ có 2 func, mà code của bạn bị lặp lại.
    -> Generic có thể được sử dụng để giảm 2 hàm này thành 1 và loại bỏ sự trùng lặp code.

    Note

    Trong các func ở trên, kiểu dữ liệu của num1num2 là giống nhau. Nếu kiểu dữ liệu của chúng khác nhau thì không thể cộng 2 số.

    Generic Function

    Generic func có thể hoạt động với bất kì kiểu dữ liệu nào. Dưới đây là code generic của addInt(::) ở trên, tên là addTwoNumbers:

    func addTwoNumbers<T>(num1: T, num2: T) -> T {
       return num1 + num2
    }

    Tưởng tượng bạn có 1 UITextfield, UITextfield đó có 1 trường gọi là placeholder. placeholder sẽ được thay thế bằng text khi bạn nhập text vào UITextfield.
    Tương tự như vậy, func addTwoNumbers(::) sử dụng kiểu dữ liệu T như là 1 placeholder. Kiểu dữ liệu T sẽ được thay thế bằng 1 kiểu dữ liệu xác định do bạn quyết định như Int, Double, … mỗi khi func addTwoNumbers(::) dược gọi. Ta có thể thấy trong func addTwoNumbers(::) ở trên, num1 và num2 có cùng kiểu dữ liệu là T.

    Note:

    • Sự khác biệt giữa generic func và non-generic func là: Ở tên của generic func thì có 1 kiểu dữ liệu placeholder (T) được khai báo trong cặp ngoặc nhọn (<T>). Cặp ngoặc nhọn nói với Swift rằng T là kiểu dữ liệu placeholder.
    • Bạn có thể thay T bằng các các tên khác như A, B, C, Element, … tùy bạn muốn. Việc thay đổi tên chỉ có tác dụng thay đổi tên của kiểu dữ liệu placeholder, chứ không thay đổi gì về tác dụng. Nên tránh đặt tên trùng với các kiểu dữ liệu xác định như Int, String, Double, … để tránh gây nhầm lẫn.
    • Và luôn đặt tên bắt đầu bằng chữ cái in hoa để người đọc hiểu rằng đó là 1 placeholder cho 1 kiểu dữ liệu chứ không phải cho 1 giá trị.

    Tuy nhiên, khi khai báo func addTwoNumbers(::) như trên, bạn sẽ gặp lỗi như sau:

    Lỗi này có nghĩa là, bạn không thể áp dụng toán tử + cho kiểu dữ liệu T vì không phải mọi kiểu dữ liệu trong Swift đều có thể cộng với nhau bằng toán tử (+). Để áp dụng được toán tử (+) cho kiểu dữ liệu T, bạn làm như sau:

    protocol Summable {
        static func +(num1: Self, num2: Self) -> Self
    }
    
    func addTwoNumbers<T: Summable>(num1: T, num2: T) -> T {
        return num1 + num2
    }

    Ở đây, bạn khai báo 1 protocol Summable có chứa toán tử (+) và cho T extends Summable. Khi đó có nghĩa là, kiểu dữ liệu xác định mà bạn muốn đưa vào để thay T thì kiểu dữ liệu đó phải extends Summable, và hiển nhiên là chúng sẽ phải khai báo toán tử (+).

    Sử dụng generic:

    Khi bạn gọi hàm addTwoNumbers, bạn sẽ thay T bằng 1 kiểu dữ liệu xác định. Tuy nhiên, vì T là kiểu Summable, nên kiểu dữ liệu xác định mà bạn muốn truyền vào cũng phải extends Summable và khai báo toán tử + cho kiểu dữ liệu đó.

    Example 1: Gọi hàm addTwoNumbers cho 2 số kiểu Int:

    • Trước hết, phải cho Int extend Summable.
    extension Int: Summable {}

    Note: Vì bản thân các kiểu dữ liệu như Int, Double, String, … đã có chứa sẵn toán tử (+), nên không cần khai báo toán tử (+) cho chúng nữa.

    let number1: Int = 5
    let number2: Int = 10
    let total = addTwoNumbers<Int>(num1: number1, num2: number2) // 15

    Example 2: Khởi tạo và khai báo hàm addTwoNumbers(::) cho 2 số kiểu String :v

    Note: Bạn có thể gọi hàm theo cách: addTwoNumbers<String>(num1: string1, num2: string2), hoặc bỏ <String> đi, vì Swift sẽ dựa vào kiểu dữ liệu bạn truyền vào num1, num2 để tự hiểu kiểu dữ liệu xác định bạn muốn thay cho T là String.

    Ở phần tiếp theo, mình sẽ giới thiệu về:

    • Extending a generic type.
    • Subclass a generic type.
    • Cách sử dụng generic nâng cao.
  • iOS — Device orientations

    iOS — Device orientations

    Mình xin chia sẻ một vài lưu ý khi develope application support full orientations :

    • UIInterfaceOrientation.portraitUpsideDown không support cho iPhone X Family (có tai thỏ) 

    -> Link refer : https://developer.apple.com/documentation/uikit/uiviewcontroller/1621435-supportedinterfaceorientations

    • Default orientation support cho iPad là UIInterfaceOrientationMask.all (tất cả các hướng), còn cho iPhone là UIInterfaceOrientationMask.allButUpsideDown (không hỗ trợ upside down) 
    • Nếu trong app sử dụng navigation view controller hoặc tabbar view controller, muốn enable tất cả các orientation thì mình phải override lại trong Base UINavigationViewController của app (tương tự với Base UITabbarViewController của app) với 2 method delegate là shouldAutorotate và supportedInterfaceOrientations
      Example: 
        override var shouldAutorotate: Bool {
            return true
        }
        
        override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
            return .all
        }
    • Nhớ check trong setting app, tab General, section Deployment Info xem mình đã select đủ các orientation mình cần support hay chưa.
      Kĩ hơn thì check trong Info.plist với key “Supported interface orientations” và “Supported interface orientations (iPad)” xem đã đủ orientation chưa. 

    Hi vọng bài viết này sẽ giúp cho các bạn tiết kiệm được 1 ngày :v
    Have a nice day!

  • Add placeholder for UITextView (phần 2)

    Add placeholder for UITextView (phần 2)

    Hello everybody 🙂

    Như chia sẻ ở phần trước thì tại phần này mình sẽ hướng dẫn các bạn một cách để có thể add placeholder cho UITextView. Hì hì, nói hướng dẫn thì hơi quá, mình chỉ muốn chia sẻ cho các bạn cách làm của mình khi gặp phải vấn đề như này mà thôi! 🙂

    OK không dài dòng nữa chúng ta cùng bắt đầu nào!

    Ứng dụng mà chúng ta sẽ tạo ra rất đơn giản và nó có tên là PlaceholderTextView (chả có ý nghĩa gì nhỉ 🙂 ). Sơ qua thì app này chỉ có một cái màn hình, bên trong nó có một UITextView để người dùng nhập message, một UIButton để gửi đi message (send message đi đâu thì mình sẽ không implement 🙂 ). Và mục đích chính của chúng ta đó là thêm placeholder cho thanh nhập message, chính là UITextView đó. OK, bước đầu tiên đó là hãy tạo ra UI giống như thế này:

    Tạo UI đơn giản như này thì chắc ai cũng làm được nhỉ? Nhưng nếu bạn là người mới thì có thể xem code mẫu của mình ở đây nhé! Mình làm bằng storyboard chứ không code chay tốn nhiều time lắm hix hix.

    Đã done phần UI, bây giờ mình sẽ giải thích chi tiết về cách mình add placeholder cho UITextView nhé! Mình sử dụng cách tạo extension của UITextView để thêm thuộc tính placeholder vào. Bạn có thể xem file extension UITextView của mình ở đây luôn! Ý tưởng của mình cũng rất là đơn giản thôi, khi đã thêm một thuộc tính placeholder cho UITextView rồi thì lúc đó chỉ cần set thuộc tính placeholder != nil và != empty là ta sẽ có được cái placeholder mà mình mong muốn, và khi text của UITextView là != empty thì placeholder phải mất đi còn khi text của UITextView = empty thì placeholder lại phải xuất hiện.

    Mình sẽ giải thích từng thành phần một ở dưới đây:

    Đầu tiên đây là thuộc tính placeholder mình thêm vào trong extension của UITextView, vì trong extension không thể thêm một stored property nên mới phải làm phức tạp ra như này đấy 🙁

    fileprivate var placeholderKey = "placeholderKey"
    
    var placeholder: String? {
        get {
            if let value = objc_getAssociatedObject(self, &placeholderKey) as? String {
                return value
            }
            return nil
        }
        set {
            objc_setAssociatedObject(self, &placeholderKey, newValue, .OBJC_ASSOCIATION_COPY)
            if newValue?.isEmpty ?? true {
                removePlaceholder()
                removeObserverText()
            }
            else {
                addPlaceholder(newValue!)
                addObserverText()
            }
        }
    }

    Với đoạn code trên thì khi ta set thuộc tính placeholder là không nil và không empty thì sẽ gọi hàm addPlaceholder(), đây là hàm sẽ làm công việc tạo ra một UILabel với text là argument được truyền vào sau đó sẽ được addSubview vào UITextView, chính UILabel đó sẽ là placeholder của UITextView. Cùng với việc gọi hàm addPlaceholder() thì cũng gọi hàm addObserverText(), đây là một hàm sẽ thêm một đối tượng observer sự kiện thay đổi text của UITextView và khi người dùng edit text trên UITextView, mục đích của việc này chính là để mình control việc add placeholder và remove placehodler cho UITextView. Ngược lại khi ta set thuộc tính placeholder là nil hoặc là empty thì sẽ gọi hàm removePlaceholder() và hàm removeObserverText() đây là những hàm dùng để remove đi UILabel mà mình đã thêm vào UITextView tại hàm addPlaceholder() và remove đối tượng observer mà mình đã thêm tại hàm addObserverText()

    Dưới đây là hàm addPlaceholder() và hàm removePlaceholder()

    func addPlaceholder(_ text: String) {
        placeholderLabel?.removeFromSuperview()
        placeholderLabel = UILabel()
        placeholderLabel?.text = text
        if let selfFont = self.font {
            placeholderLabel?.font = UIFont.systemFont(ofSize: selfFont.pointSize, weight: .thin)
        }
        else {
            placeholderLabel?.font = UIFont.systemFont(ofSize: 12.0, weight: .thin)
        }
        placeholderLabel?.textColor = .lightGray
        placeholderLabel?.numberOfLines = 0
        let padding = self.textContainer.lineFragmentPadding
        let estimateSize = placeholderLabel!.sizeThatFits(CGSize(width: self.frame.size.width - 2*padding, height: self.frame.size.height))
        placeholderLabel?.frame = CGRect(x: padding, y: self.textContainerInset.top, width: estimateSize.width, height: estimateSize.height)
        self.addSubview(placeholderLabel!)
    }
    
    func removePlaceholder() {
        placeholderLabel?.removeFromSuperview()
    }

    Ở 2 func trên thì mình có sử dụng thuộc tính placeholderLabel, đây là thuộc tính mà mình cũng đã thêm vào phần extension của UITextView. Lý do mình thêm thuộc tính này là bởi vì để việc remove placeholder trở nên dễ dàng hơn thay vì việc phải duyệt hết các subView của UITextView, kiểm tra subView nào đang là placeholder rồi mới remove đi.

    Đây là phần implement cho thuộc tính placeholderLabel của mình

    fileprivate var placeholderLabelKey = "placeholderLabelKey"
    
    fileprivate var placeholderLabel: UILabel? {
        get {
            if let label = objc_getAssociatedObject(self, &placeholderLabelKey) as? UILabel {
                return label
            }
            return nil
        }
        set {
            objc_setAssociatedObject(self, &placeholderLabelKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }

    Dưới đây là hàm addObserverText() và hàm removeObserverText()

     fileprivate let observerKeyPath = "text"
    
     func addObserverText() {
         observerText = ObserverText()
         self.addObserver(observerText!, forKeyPath: observerKeyPath, options: .new, context: nil)
     }
        
     func removeObserverText() {
         if let observerText = observerText {
            self.removeObserver(observerText, forKeyPath: observerKeyPath)
         }
         observerText = nil
     }

    Ở 2 func trên thì có thể thấy khi mình add observer thì cũng đã add observer sự kiện thay đổi thuộc tính text của UITextView (self ở trên chính là UITextView, những hàm này đều đặt trong extension của UITextView). À còn với thuộc tính observerText mình sử dụng ở trên thì cũng là một thuộc tính mình thêm vào extension của UITextView, implement của nó đây 🙂

    fileprivate var observerTextKey = "observerTextKey"
    
    fileprivate var observerText: ObserverText? {
        get {
             if let observer = objc_getAssociatedObject(self, &observerTextKey) as? ObserverText {
                 return observer
             }
             return nil
         }
         set {
              objc_setAssociatedObject(self, &observerTextKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
         }
    }

    Class ObserverText mình sử dụng ở trên là một class mà mình tự tạo ra, nhiệm vụ của nó là xử lý hành động khi thuộc tính text của UITextView thay đổi và xử lý hành động khi người dùng edit text trên UITextView. Implement của nó ở đây:

    fileprivate class ObserverText: NSObject {
        
        override init() {
            super.init()
            NotificationCenter.default.addObserver(self, selector: #selector(textViewDidChange(notification:)), name: UITextView.textDidChangeNotification, object: nil)
        }
        
        deinit {
            NotificationCenter.default.removeObserver(self)
        }
        
        @objc func textViewDidChange(notification: Notification) {
            if let textView = notification.object as? UITextView {
                if let text = textView.placeholder, textView.text.isEmpty {
                    textView.addPlaceholder(text)
                }
                else {
                    textView.removePlaceholder()
                }
            }
        }
        
        override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
            if let keyPath = keyPath, keyPath == observerKeyPath, let textView = object as? UITextView {
                if let newText = change?[NSKeyValueChangeKey.newKey] as? String, !newText.isEmpty {
                    textView.removePlaceholder()
                }
                else {
                    if let text = textView.placeholder {
                        textView.addPlaceholder(text)
                    }
                }
            }
        }
        
    }

    Class này cũng khá là dễ hiểu phải không nào, chỉ là implement để bắt các sự kiện thôi mà 🙂.

    OK 🙂 vậy là ở trên mình đã trình bày toàn bộ cách mà mình coding để có thể thêm placeholder cho UITextView. Bây giờ, khi sử dụng UITextView, bạn chỉ cần set thuộc tính placeholder là khác nil và khác empty là bạn sẽ có placeholder của UITextView rồi, và mỗi khi người dùng edit text trên UITextView thì nó cũng sẽ tự động thêm placeholder khi mà người dùng xoá tất cả các kí tự trên UITextView và cũng tự động remove placeholder khi người dùng nhập một kí tự nào đó lên UITextView. Giống như trong file gif này nè 🙂

    Cảm ơn bạn đã dành thời gian để đọc bài viết này. Happy reading

    Source code tham khảo

  • Add placeholder for UITextView (phần 1)

    Add placeholder for UITextView (phần 1)

    Hello everybody 🙂

    Chắc hẳn mọi người điều biết trong iOS khi muốn nhập giá trị đầu vào thì sẽ sử dụng UITextFieldUITextView. Với UITextFiled thì ta chỉ có thể nhập giá trị trên một dòng còn với UITextView thì có thể nhập giá trị trên nhiều dòng, có thể scroll vì UITextView là extends từ UIScrollView mà 🙂. Tuy nhiên khi sử dụng hai View này thì ta còn thấy một điểm khác nhau nữa, đó là với UITextView Apple đang không hỗ trợ thuộc tính placeholder còn với UITextField thì có luôn placeholder ngon như này 🙁.

    Tại sao lại có sự bất công này???

    Có lẽ do Apple thích thế thôi nhỉ? 🙂. Thực ra cũng có một quan điểm cho rằng lý do mà UITextView không có placeholder là bởi vì khi lập trình các dev thường sẽ setup một header ở bên trên UITextView để giải thích rõ tại UITextView này sẽ cần ghi nội dung gì rồi nên không cần phải thêm placeholder để làm gì nữa cả, việc này cũng khá dễ hiểu tại vì khi xét tới bản chất của UITextView đó là nơi sẽ cho phép người dùng nhập một content gì đó sẽ mất nhiều dòng và ngốn nhiều khá thời gian. Thật tai hại khi người dùng đang nhập content của họ và bỗng dưng họ quên mất rằng nội dung chính phải nhập là gì 🙂 khi đó chỉ còn cách xoá hết đi và xem lại placeholder (nếu có), với những người đãng trí chắc điều này sẽ là một cơn ác mộng 🙂

    Có nên sử dụng placeholder cho UITextView?

    Một số trường hợp cũng nên dùng placeholder cho UITextView khi mà không có đủ không gian để thêm một header cho UITextView và nếu thêm vào cũng không được đẹp mắt. Chẳng hạn trong trường hợp này:

    Như hình bên trên thanh iMessage trong ứng dụng tin nhắn kia là một UITextView và có placeholder = "iMessage"

    Kết luận!

    Qua bài này, có thể thấy rằng việc sử dụng UITextView thì sẽ không được Apple hỗ trợ thuộc tính placeholder. Điều này có thể sẽ gây khó khăn cho những bạn mới lập trình về iOS 🙂. Tuy nhiên, với một lập trình viên thì không gì là không thể làm được phải không nào? Khi Apple không hỗ trợ thuộc tính placeholder cho UITextView thì ta cũng có thể tự làm điều đó hộ Apple được mà 🙂. Ở phần sau, mình sẽ hướng dẫn các bạn một cách để có thể thêm placeholder cho UITextView thông qua việc tạo ra một app đơn giản như thế này:

  • Swift—Design patterns: Multicast Delegate

    Swift—Design patterns: Multicast Delegate

    Multicast delegate

    Khái niệm

    • Chúng ta đều biết rằng delegate là mối quan hệ 1 – 1 giữa 2 objects, trong đó 1 ọbject sẽ gửi data/event và object còn lại sẽ nhận data hoặc thực thi event
    • Multicast delegate, về cơ bản chỉ là mối quan hệ 1 – n, trong đó, 1 object sẽ gửi dataevent đi, và có n class đón nhận data, event đó.

    Ứng dụng của multicase delegate, lúc nào thì dùng?

    • Dùng multicast delegate khi mà bạn muốn xây dựng một mô hình delegate có mối quan hệ 1 – nhiều.
    • Ví dụ: bạn có 1 class chuyên lấy thông tin data và các logic liên quan, và bạn muốn mỗi khi data của class được update và muốn implement các logic tương ứng của class này trên nhiều view/view controller khác nhau. Thì multicast delegate có thể dùng để xử lý bài toán này.

    So sánh với observer và notification

    • Tại sao không dùng observer: chủ yếu chúng ta dùng obverver để theo dõi data hoặc event nào đó xảy ra, và thực hiện logic sau khi nó đã xảy ra. Còn nếu dùng delegate, chúng ta còn có thể xử lý data hoặc event, hay nói cách khác, chúng ta có thể quyết định xem có cho phép event đó xảy ra hay không, ví dụ như các delegate của Table view như tableView:willSelectRowAtIndexPath:
    • Tại sao không dùng Notification thay vì multicast delegate? Về cơ bản, notification là thông tin một chiều từ 1 object gửi sang nhiều object nhận, chứ không thể có tương tác ngược lại. Ngoài ra, việc gửi data từ object gửi sang các object nhận thông qua userInfo thực sự là một điểm trừ rất lớn của Notifiction. Hơn nữa, việc quản lý Notification sẽ tốn công sức hơn là delegate, và chúng ta khá là khó khăn để nhìn ra mối quan hệ giữa object gửi và nhận khi dùng Notification.

    Implementation

    • Vì Swift không có sẵn phương pháp tạo multicast delegate nên chúng ta cần phải tạo ra 1 class helper, nhằm quản lý các object muốn nhận delegate cũng như gọi các method delegate muốn gửi.

    Đầu tiên, chúng ta tạo class helper như dưới:

    class MulticastDelegate<ProtocolType> {
        private let delegates: NSHashTable<AnyObject> = NSHashTable.weakObjects()
        
        func add(_ delegate: ProtocolType) {
            delegates.add(delegate as AnyObject)
        }
        
        func remove(_ delegateToRemove: ProtocolType) {
            for delegate in delegates.allObjects.reversed() {
                if delegate === delegateToRemove as AnyObject {
                    delegates.remove(delegate)
                }
            }
        }
        
        func invokeDelegates (_ invocation: (ProtocolType) -> Void) {
            for delegate in delegates.allObjects.reversed() {
                invocation(delegate as! ProtocolType)
            }
        }
    }
    

    Class này là helper class có các method là add:(:) dùng để add và remove các object muốn nhận delegate. Ngoài ra nó có method invokeDelegates(:)->Void để gửi method delegate sang toàn bộ các object muốn nhận delegate.

    Tiếp theo, define protocol (các delegate method) muốn implement:

    protocol SampleDelegate: class {
        func sendSampleDelegateWithoutData()
        func sendSampleDelegate(with string: String)
    }
    
    extension SampleDelegate {
        func sendSampleDelegateWithoutData() {        
        }
    }
    

    Ở đây, protocol SampleDelegate có 2 method là để ví dụ thêm rõ ràng rằng multicast delegate có thể thoải mái gửi các delegate tuỳ ý. Phần extention của SampleDelegate chỉ là để khiến cho method sendSampleDelegateWithoutData trở thành optional, không cần phải "conform" đến SampleDelegate. Đây là cách khá Swift, thay vì dùng cách sử dụng @objC và keywork optional

    Tiếp theo, define ra class sẽ gửi các method của delegate

    class SampleClass {
        var delegate = MulticastDelegate<SampleDelegate>()
        
        func didGetData() {
            delegate.invokeDelegates {
                $0.sendSampleDelegate(with: "Sample Data")
            }
        }
    }
    

    Ở đây, có thể thấy rằng delegate của class "SampleClass" thực chất là object của helpper "MulticastDelegate", và nó chỉ chấp nhận delegate là objects của các class mà conform đến protocol "SampleDelegate"

    Khai báo vài class conform đến protocol "SampleDelegate"

    class ReceivedDelegate1: SampleDelegate {
        func sendSampleDelegate(with string: String) {
            print("ReceivedDelegate === 1 === \(string)")
        }
        
        deinit {
            print("deinit ReceivedDelegate1")
        }
    }
    
    class ReceivedDelegate2: SampleDelegate {
        func sendSampleDelegate(with string: String) {
            print("ReceivedDelegate === 2 === \(string)")
        }
        
        deinit {
            print("deinit ReceivedDelegate2")
        }
    }
    

    OK, bây giờ test thử:

    let sendDelegate = SampleClass()
    let received1 = ReceivedDelegate1()
    sendDelegate.delegate.add(received1)
    
    do {
        let received2 = ReceivedDelegate2()
        sendDelegate.delegate.add(received2)
        sendDelegate.didGetData()
    }
    print("Đợi cho object received2 trong block do được release")
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        sendDelegate.didGetData()
    }
    
    

    Sau khi run đoạn source code này, ta được log như dưới:

    ReceivedDelegate === 2 === Sample Data
    ReceivedDelegate === 1 === Sample Data
    Đợi cho object received2 trong block do được release
    deinit ReceivedDelegate2
    ReceivedDelegate === 1 === Sample Data
    

    Giờ cùng phân tích:

    Trong block do thì object "sendDelegate" đã append được 2 delegate objects vào biến delegate của nó, tiến hành send delegate thì ta thấy rằng cả object của cả 2 class ReceivedDelegate1ReceivedDelegate2 đều nhận được.

    Sau block do thì object received2 sẽ được release, vì việc release sẽ tốn một chút thời gian cho nên chúng ta sẽ thử thực hiện việc send delegate sau khoảng thời gian 1s, sau khi received2 đã được release (bằng cách check log của method deinit)

    Lúc này, ta thấy rằng chỉ có object của class ReceivedDelegate1 là còn nhận được delegate, object của class ReceivedDelegate2 đã bị release nên không còn nhận được object nữa. Như vậy, cách làm này vẫn đảm bảo các delegate vẫn là weak reference, không gây ra leak memory.

    Đề làm được điều này thì ta đã sử dụng NSHashTable.weakObjects() để lưu weak reference đến các delegate được gán vào biến delegates của helper MulticastDelegate. Do đó đảm bảo được việc keep weak reference của class helper, nhằm tránh memory leak.

    Ví dụ xem file: https://github.com/nhathm/swift.sample/tree/master/DesignPatterns/MulticastDelegate.playground

  • Swift—KeyPaths (1)

    Swift—KeyPaths (1)

    Swift KeyPaths (1)

    KeyPaths là khái niệm được đưa vào Swift từ version Swift 4 (source: SE-0161)

    Table of contents

    KeyPaths basic

    • KeyPaths là cách để truy cập đến property chứ không phải truy cập đến giá trị của property.
    • Khi định nghĩa KeyPaths thì ta hoàn toàn có thể xác định/định nghĩa được kiểu biến của KeyPaths
    • Ở Objective C cũng đã có khái niệm KeyPaths, tuy nhiên KeyPaths của Objective C chỉ là string, còn KetPaths của Swift thì được định nghĩa rõ ràng kiểu dữ liệu

    KeyPaths store uninvoked reference to property

    Cùng check đoạn code phía dưới

    class Person {
        var firstName: String
        var lastName: String
        var age: Int
    
        init(_ firstName: String, _ lastName: String, _ age: Int) {
            self.firstName = firstName
            self.lastName = lastName
            self.age = age
        }
    }
    
    var firstPerson: Person? = Person("Nhat", "Hoang", 10)
    
    var nameKeyPaths = \Person.firstName
    var refVar = firstPerson?.firstName
    
    print("refVar value = \(refVar ?? "refVar nil")")
    print("KeyPaths value = \(firstPerson?[keyPath: nameKeyPaths] ?? "nil")")
    
    firstPerson = nil
    print("refVar value = \(refVar ?? "refVar nil")")
    print("KeyPaths value = \(firstPerson?[keyPath: nameKeyPaths] ?? "nil")")
    

    Log

    refVar value = Nhat
    KeyPaths value = Nhat
    refVar value = Nhat
    KeyPaths value = nil
    

    Với đoạn code trên, chúng ta dễ dàng thấy được sự khác nhau lớn giữa việc tham chiếu đến property của class khác nhau giữa việc sử dụng KeyPaths và bằng việc gán variable đến property của class.

    • Với việc tham chiếu giá trị dùng cách gán variable đến property thì ta có thể thấy rằng, mặc dù firstPerson đã được gán bằng nil, tuy nhiên do khi gán refVar đến firstName cho nên sau khi firstPerson bị gán là nil thì refVar vẫn có giá trị vì phép gán này đã ảnh hưởng đến reference count của property.
    • Đối với cách dùng KeyPaths, chúng ta vẫn có thể lấy được giá trị của property mặc cách bình thường, tuy nhiên KeyPaths không hề ảnh hưởng đến reference của property, do đó khi firstPerson được gán bằng nil, KeyPaths sẽ có value là nil.

    Với sự khác biệt trên, chúng ta hết sức lưu ý khi sử dụng phương pháp tham chiếu (reference) đến giá trị của property nếu không rất dễ nẩy sinh bug.

    KeyPaths as parameter

    Khi khai báo KeyPaths đến property nào đó của struct/class thì biến KeyPaths đó thể hiện rất rõ type của KeyPaths. Như ví dụ dưới, chúng ta có thể thấy rằng nameKeyPath là KeyPaths mô tả những property là kiểu String của type Person:

    Nếu đơn thuần chỉ gán variable đến property của instance thì ta sẽ được kiểu biến là String (hoặc loại type tương ứng với property của instance) như ảnh dưới:

    Vậy ứng dụng của việc này là gì?

    Nếu ta có một logic nào đó chỉ chấp nhận đầu vào là property của một class/struct cho trước, thì chúng ta nên sử dụng KeyPaths để giới hạn kiểu parameters cho logic đó. Ví dụ:

    func printPersonName(_ person: Person, _ path: KeyPath<Person, String>) {
        print("Person name = \(person[keyPath: path])")
    }
    
    class Person {
        var firstName: String
        var lastName: String
        var age: Int
    
        init(_ firstName: String, _ lastName: String, _ age: Int) {
            self.firstName = firstName
            self.lastName = lastName
            self.age = age
        }
    }
    class Student: Person {
        var className: String
    
        init(_ firstName: String, _ lastName: String, _ age: Int, _ className: String) {
            self.className = className
            super.init(firstName, lastName, age)
        }
    }
    
    var firstPerson = Person("Nhat", "Hoang", 10)
    var firstStudent = Student("Rio", "Vincente", 20, "Mẫu giáo lớn")
    var nameKeyPath = \Person.lastName
    
    printPersonName(firstPerson, nameKeyPath)
    

    Với ví dụ trên thì func printPersonName chỉ chấp nhận đầu vào là String và thuộc class Person, nếu chúng ta sử dụng các KeyPaths cùng class Person nhưng khác kiểu data như var agekeyPath = \Person.age hoặc dùng KeyPaths cùng kiểu data nhưng khác class như var studentNameKeyPath = \Student.lastName (Student là class kế thừa Person) thì đều bị báo lỗi.

    => Điều này giúp chúng ta hạn chế sai sót ngay từ lúc coding.

    Sorted, filter…using KeyPaths

    Một trong những ứng dụng rất hay của KeyPaths đó là làm cho các closure như sorted, map, filter trở nên linh hoạt hơn, và tính ứng dụng cao hơn.

    Đi vào bài toán thực tế như sau:

    Bạn được yêu cầu làm một ứng dụng dạng như app Contact, và ứng dụng này có 1 vài chức năng như là:

    • Sắp xếp tên người dùng theo thứ tự
    • Lọc ra những người dùng đủ 18 tuổi

    Thì ta có thể triển khai như sau: Khai báo class Person tương ứng với từng Contact:

    class Person {
        var firstName: String
        var lastName: String
        var age: Int
        var workingYear: Int
    
        init(_ firstName: String, _ lastName: String, _ age: Int) {
            self.firstName = firstName
            self.lastName = lastName
            self.age = age
            self.workingYear = 0
        }
    }
    

    Danh sách contacts:

    let listPersons: [Person] = [Person("Alex", "X", 1),
                                 Person("Bosh", "Bucus", 12),
                                 Person("David", "Lipis", 20),
                                 Person("Why", "Always Me", 69),
                                 Person("Granado", "Espada", 45),
                                 Person("Granado", "Espada", 46)]
    

    Bây giờ, nếu muốn sắp xếp danh sách người dùng theo thứ từ A->Z đối với first name, thì ta có thể làm đơn giản như sau:

    let sortedPersons = listPersons.sorted {
        $0.firstName < $1.firstName
    }
    

    Đây là cách làm không sai, tuy nhiên nếu như sau này có thêm các yêu cầu như: sắp xếp theo tứ tự first name Z->A, last name A->Z, last name Z->A, hoặc là sắp xếp theo quê quán, đất nước… thì có lẽ phải clone đoạn source code sort ra dài dài.

    Trong trường hợp này, nếu sử dụng KeyPaths để implement logic sort, chúng ta có thể rút ngắn source code đi rất nhiều, và điều kiện sort cũng có thể tuỳ biến nhiều hơn.

    Define enum thứ tự sort:

    enum Order {
        case ascending
        case descending
    }
    

    Override logic sort

    extension Sequence {
        func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>, order: Order) -> [Element] {
            return sorted { a, b in
                switch order {
                case .ascending:
                    return a[keyPath: keyPath] < b[keyPath: keyPath]
                case .descending:
                    fallthrough
                default:
                    return a[keyPath: keyPath] > b[keyPath: keyPath]
                }
            }
        }
    }
    

    Với cách làm sử dụng KeyPaths, thì ta có thể thoải mái sort listPersons dựa trên các điều kiện sort khác nhau như sort theo tên, theo họ, ascending hoặc descending… Ví dụ: var sortedPersons = listPersons.sorted(by: \.firstName, order: .descending)

    Note: ở đây dùng extension của protocol Sequencesortedfilter thực chất là được define ở Sequence protocol

    Ví dụ về cách filter các điều kiện của list persons: Xem trong file playground cuối bài.

    Tài liệu có tham khảo:

    • Swift’s document
    • nshipster.com
    • hackingwithswift.com
    • swiftbysundell.com

    Trong phần tiếp theo, chúng ta sẽ đi vào một vài ứng dụng thực tế hơn sử dụng KeyPaths.

    Sample playground: Swift_KeyPaths.playground

  • Swift—Codable

    Swift—Codable

    Codable được giới thiệu cùng với phiên bản 4.0 của Swift, đem lại sự thuận tiện cho người dùng mỗi khi cần encode/decode giữa JSON và Swift object.

    Codable là alias của 2 protocols: Decodable & Encodable

    • Decodable: chuyển data dạng string, bytes…sang instance (decoding/deserialization)
    • Encodable: chuyển instace sang string, bytes… (encoding/serialization)

    Table of contents

    Swift Codable basic

    Chúng ta sẽ đi vào ví dụ đầu tiên của Swift Codable, mục tiêu sẽ là convert đoạn JSON sau sang Swift object (struct)

    {
        "name": "NhatHM",
        "age": 29,
        "page": "https://magz.techover.io/"
    }
    

    Cách làm:

    Đối với các JSON có dạng đơn giản thế này, công việc của chúng ta chỉ là define Swift struct conform to Codable protocol cho chính xác, sau đó dùng JSONDecoder() để decode về instance là được. Note: Nếu không cần phải chuyển ngược lại thành string/bytes (không cần encode) thì chỉ cần conform protocol Decodable là đủ.

    Implementation:

    Define Swift struct:

    struct Person: Codable {
        let name: String
        let age: Int
        let page: URL
        let bio: String?
    }
    

    Convert string to instance:

    // Convert json string to data
    var data = Data(json.utf8)
    let decoder = JSONDecoder()
    // Decode json with dictionary
    let personEntity = try? decoder.decode(Person.self, from: data)
    if let personEntity = personEntity {
        print(personEntity)
    }
    

    Chú ý: đối với dạng json trả về là array như dưới:

    [{
        "name": "NhatHM",
        "age": 29,
        "page": "https://magz.techover.io/"
    },
    {
        "name": "RioV",
        "age": 19,
        "page": "https://nhathm.com/"
    }]
    

    thì chỉ cần define loại data sẽ decode cho chính xác là được:

    let personEntity = try? decoder.decode([Person].self, from: data)
    

    Ở đây ta đã định nghĩa được data decode ra sẽ là array của struct Person.

    Swift Codable manual encode decode

    Trong một vài trường hợp, data trả về mà chúng ta cần có thể nằm trong một key khác như dưới:

    {
        "person": {
            "name": "NhatHM",
            "age": 29,
            "page": "https://magz.techover.io/"
        }
    }
    

    Trong trường hợp này, nếu define Swift struct đơn giản như phần 1 chắc chắn sẽ không thể decode được. Do đó cách làm sẽ là define struct sao cho nó tương đồng nhất có thể với format của JSON. Ví dụ như đối với JSON ở trên, chúng ta có thể define struct như dưới:

    Implementation

    struct PersonData: Codable {
        struct Person: Codable {
            let name: String
            let age: Int
            let page: URL
            let bio: String?
        }
    
        let person: Person
    }
    

    Đối với trường hợp này, chúng ta vẫn sử dụng JSONDecoder() để decode string về instance như thường, tuy nhiên lúc sử dụng value của struct thì sẽ hơi bất tiện:

    let data = Data(json.utf8)
    let decoder = JSONDecoder()
    let personEntity = try? decoder.decode(PersonData.self, from: data)
    if let personEntity = personEntity {
        print(personEntity)
        print(personEntity.person.name)
    }
    

    Manual encode decode

    Đối với dạng JSON data như này, chúng ta còn có một cách khác để xử lý data cho phù hợp, dễ dùng hơn như dưới:

    Define struct (chú ý, lúc này không thể hiện struct conform to Codable nữa, mà sẽ conform to Encodable và Decodable một cách riêng biệt):

    struct Person {
        var name: String
        var age: Int
        var page: URL
        var bio: String?
    
        enum PersonKeys: String, CodingKey {
            case person
        }
    
        enum PersonDetailKeys: String, CodingKey {
            case name
            case age
            case page
            case bio
        }
    }
    

    Ở đây có một khái niệm mới là CodingKey. Về cơ bản, CodingKey chính là enum define các "key" mà chúng ta muốn Swift sử dụng để decode các value tương ứng. Ở đây key PersonKeys.person sẽ tương ứng với key "person" trong JSON string, các enum khác cũng tương tự (đọc thêm về CodingKey ở phần sau)

    Với trường hợp này, ta sử dụng nestedContainer để đọc các value ở phía sâu của JSON, sau đó gán giá trị tương ứng cho properties của Struct.

    Implementation

    extension Person: Decodable {
        init(from decoder: Decoder) throws {
            let personContainer = try decoder.container(keyedBy: PersonKeys.self)
    
            let personDetailContainer = try personContainer.nestedContainer(keyedBy: PersonDetailKeys.self, forKey: .person)
            name = try personDetailContainer.decode(String.self, forKey: .name)
            age = try personDetailContainer.decode(Int.self, forKey: .age)
            page = try personDetailContainer.decode(URL.self, forKey: .page)
            bio = try personDetailContainer.decodeIfPresent(String.self, forKey: .bio)
        }
    }
    

    Đây chính là phần implement để đọc ra các value ở tầng sâu của JSON, sau đó gán lại vào các properties tương ứng của struct. Các đoạn code trên có ý nghĩa như sau:

    • personContainer là container tương ứng với toàn bộ JSON string
    • personDetailContainer là container tương ứng với value của key person
    • Nếu có các level sâu hơn thì ta lại tiếp tục sử dụng nestedContainer để đọc sau vào trong
    • Nếu một property nào đó (key value nào đó của json) mà có thể không trả về, thì sử dụng decodeIfPresent để decode (nếu không có value thì gán bằng nil)

    Note: Đối với việc Encode thì cũng làm tương tự, tham khảo source code đi kèm (link cuối bài)

    Với cách làm này, thì khi gọi đến properties của struct, đơn giản ta chỉ cần personEntity.name là đủ.

    Swift Codable coding key

    Trong đa số các trường hợp thì client sẽ sử dụng json format mà server đã định sẵn, do đó có thể gặp các kiểu json có format như sau:

    {
        "person_detail": {
            "first_name": "Nhat",
            "last_name": "Hoang",
            "age": 29,
            "page": "https://magz.techover.io/"
        }
    }
    

    Đối với kiểu json như này, để Struct có thể codable được thì cần phải define properties dạng person_detail, first_name. Điều này vi phạm vào coding convention của Swift. Trong trường hợp này chúng ta sử dụng Coding key để mapping giữa properties của Struct và key của JSON.

    Implementation

    struct Person {
        var firstName: String
        var lastName: String
        var age: Int
        var page: URL
        var bio: String?
    
        enum PersonKeys: String, CodingKey {
            case person = "person_detail"
        }
    
        enum PersonDetailKeys: String, CodingKey {
            case firstName = "first_name"
            case lastName = "last_name"
            case age
            case page
            case bio
        }
    }
    

    Với trường hợp này, khi sử dụng đoạn code decode như

    var personDetailContainer = personContainer.nestedContainer(keyedBy: PersonDetailKeys.self, forKey: .person)

    hay

    try personDetailContainer.encode(firstName, forKey: .firstName)

    thì khi đó, Swift sẽ sử dụng các key json tương ứng là person_detail hoặc first_name.

    Cách implement cụ thể tham khảo file playground cuối bài

    Swift Codable key decoding strategy

    Nếu json format từ server trả về là snake case (example_about_snake_case) thì chúng ta không cần phải define Coding key, mà chỉ cần dùng keyDecodingStrategy của JSONDecoder là đủ. Ví dụ:

    let json = """
    {
        "first_name": "Nhat",
        "last_name": "Hoang",
        "age": 29,
        "page": "https://magz.techover.io/"
    }
    """
    
    struct Person: Codable {
        var firstName: String
        var lastName: String
        var age: Int
        var page: URL
        var bio: String?
    }
    
    let data = Data(json.utf8)
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    
    let personEntity = try? decoder.decode(Person.self, from: data)
    if let personEntity = personEntity {
        print(personEntity)
    } else {
        print("Decode entity failed")
    }
    

    Với trường hợp này, Swift decoder sẽ tự hiểu để decode key first_name thành property firstName. Điều kiện duy nhất là sử dụng keyDecodingStrategyconvertFromSnakeCase và server trả về format JSON đúng theo format của snake case.

    Custom key decoding strategy

    Ngoài ra cũng có thể define custom keyDecodingStategy bằng cách sử dụng:

    jsonDecoder.keyDecodingStrategy = .custom { keys -> CodingKey in
       let key = /* logic for custom key here */
       return CodingKey(stringValue: String(key))!
    }
    

    Swift Codable date decoding strategy

    Trong rất nhiều trường hợp thì JSON trả về từ server sẽ bao gồm cả date time string. Và JSONDecoder cũng cung cấp phương pháp để decode date time từ string một cách nhanh gọn bằng dateDecodingStrategy. Ví dụ, với date time string đúng chuẩn 8601 thì chỉ cần define:

    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .iso8601
    

    là có thể convert date time dạng 1990-01-01T14:12:41+0700 sang Swift date time 1990-01-01 07:12:41 +0000 một cách đơn giản.

    Trong trường hợp muốn decode một vài string date time có format khác đi, thì có thể làm bằng cách:

    func dateFormatter() -> DateFormatter {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy/MM/dd HH:mm:ss"
        return formatter
    }
    
    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .formatted(dateFormatter())
    

    Với ví dụ này, thì chúng ta có thể tự define date formatter và sử dụng cho dateDecodingStrategy của JSONDecoder

    Swift Codable nested unkeyed container

    {
        "persons": [
            {
                "name": "NhatHM",
                "age": 29,
                "page": "https://magz.techover.io/"
            },
            {
                "name": "RioV",
                "age": 19,
                "page": "https://nhathm.com/"
            }
        ]
    }
    

    Với dạng JSON như trên thì values của persons là một array chứa các thông tin của person. Và các item trong array thì không có key tương ứng. Do đó để Decode được trường hợp này thì ta dùng nestedUnkeyedContainer.

    Implementation

    extension ListPerson: Decodable {
        init(from decoder: Decoder) throws {
            let personContainer = try decoder.container(keyedBy: PersonKeys.self)
            listPerson = [Person]()
    
            var personDetailContainer = try personContainer.nestedUnkeyedContainer(forKey: .persons)
            while (!personDetailContainer.isAtEnd) {
                if let person = try? personDetailContainer.decode(Person.self) {
                    listPerson.append(person)
                }
            }
        }
    }
    

    Sample playground: Swift_Codable.playground