Category: Swift

  • Method Swizzling in Swift

    Method Swizzling in Swift

    Table of Contents

    • Problems?
    • What is method swizziling?
    • Swizzling CocoaTouch class
    • Swizzling custom Swift class
    • Note
    • References

    Problems?

    If you meet one of these situations, how will you handle?

    • Firebase SDK only provides you a function named "showLoginView()" to present a LoginViewController. The problem is all of view controllers in your app use a custom background color? So how can we set background color for LoginViewController?
    • Firebase SDK saves value to UserDataDefault, but you expect that all keys must have a prefix, for example is "FPT". How can you do this?

    Method swizzling comes to rescue

    Defination

    So what is method swizzling?

    Method swizzling is the process of changing the implementation of an existing selector at runtime.

    Speak in a easy-to-understand way, method swizzling acts like swap(a, b) function. It will takes implementation of function 1 and function 2 and swap.

    Swizzling

    Use swizzling to solve the problem:

    So with method swizzling, we can change the implementation of viewDidLoad in LoginViewController to our custom implementation that calls change backgroundColor.

    Swizzling CocoaTouch class

    To swizzle, you just need to follow some steps:

    1. Create a new method with your custom implementation.
    2. Get default method selector reference.
    3. Get new method selector reference.
    4. Use objective-C runtime to switch the selectors.

    Let’s swizzle:

    First, create a demo view controller and create new method with your custom implementation.

    class ViewController: UIViewController {
        override func viewDidLoad() {
            super.viewDidLoad()
            debugPrint("Call default view did load")
        }
    }
    
    extension UIViewController {
        // 1 
        @objc func viewDidLoadSwizzlingMethod() {
            // 2 
            self.viewDidLoadSwizzlingMethod()
            
            // 3 
            debugPrint("Swizzleeee. Call NEW view did load ")
            view.backgroundColor = .yellow
        }
    }
    
    
    1. Create new method with custom implementation
    2. If you add this line, it will call new implementation first, then call default implementation. If. you don’t add this line, it will call new implentation only.
    3. Your custom implementation

    Next, create a function where the swizzle takes place.

    extension UIViewController {
    ...
         static func startSwizzlingViewDidLoad() {
            // 1
            let defaultSelector = #selector(viewDidLoad)
            let newSelector = #selector(viewDidLoadSwizzlingMethod)
    
            // 2
            let defaultInstace = class_getInstanceMethod(UIViewController.self, defaultSelector)
            let newInstance = class_getInstanceMethod(UIViewController.self, newSelector)
            
            // 3
            if let instance1 = defaultInstace, let instance2 = newInstance {
                debugPrint("Swizzlle for all view controller success")
                method_exchangeImplementations(instance1, instance2)
            }
        }
    }
    1. Create 2 selectors of default method and new method.
    2. Create 2 references of 2 selectors by using class_getInstanceMethod.
    3. Use Objective-C runtime to “swaps implementation” of 2 selectors.

    The final step is call the function startSwizzlingViewDidLoad. We must swizzle before the viewController call it’s default viewDidLoad.
    Here, I will swizzle at AppDelegate to make all ViewControllers in apps change backgroundColor to yellow.

    class AppDelegate {
        func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
            UIViewController.startSwizzlingViewDidLoad()
            return true
        }
    
        ...
    }

    As you can see, all of our view controllers will be set backgroundColor to yellow color.

    Swizzling custom Swift class

    To use method swizzling with your Swift classes, you just need to do:

    • The methods you want to swizzle must have the dynamic attribute.
    • Flow the steps like swizzle CocoaTouch class
    class GST {
        
        @objc dynamic func workFromHome() {
            print("Working...")
        }
        
        @objc dynamic func swizzleWorkFromHome() {
            print("Playing from home...")
        }
        
        static func startSwizzling() {
            let defaultInstance = class_getInstanceMethod(GST.self, #selector(GST.workFromHome))
            let newInstance = class_getInstanceMethod(GST.self, #selector(GST.swizzleWorkFromHome))
            
            if let instance1 = defaultInstance, let instance2 = newInstance {
                method_exchangeImplementations(instance1, instance2)
            }
        }
    }

    And the results:

    Note

    1. If you swizzle multiple times default method, that default method will have the implementation of the lastest swizzle method.

    Example:

    • You swizzle viewDidLoad with your custom method in your AppDelegate.
    • Firebase swizzle viewDidLoad with its custom method when FirebaseSDK init in your app => after AppDelegate.
    • When a ViewController init => It will takes the implementation of Firebase’s custom method instead of your, because Firebase swizzle after you swizzle.
    1. If you are shipping a framework which is used by hundreds of apps, better not to use swizzling in this case. If you must use swizzling, you should added it to the framework’s document.

    References:

  • Composition over Inheritance

    Composition over Inheritance

    Content

    • Vấn đề về sử dụng Inheritance
    • Disadvantages of Inheritance
    • Composition là gì?
    • Sử dụng Composition để thay thế Inheritance.

    Vấn đề về sử dụng Inheritance

    Inheritance là 1 trong những core concept của OOP. 1 vài lợi ích mà Inheritance mang lại đó là:

    • Code reusebility: Các lớp con có các properties và functions của lớp cha -> Có thể giảm sự duplicate code giữa các lớp con bằng cách đặt các phần code bị duplicate vào lớp cha.
    • Code dễ đọc và dễ hiểu hơn.
    • Inheritance đại diện cho mối quan hệ IS-A relation ship -> Các lớp con có thể thay thế cho lớp cha

    Mặc dù Inheritance được sử dụng rộng rãi, nhưng liệu nó có phải là 1 concept mạnh mẽ nên áp dụng mọi lúc mọi nơi? Hãy cùng suy ngẫm về 1 ví dụ sau:

    • SkyBird và MountainBird là 2 class kế thừa từ class Bird. SkyBird và MountainBird có chung hành vi eat() và fly(), do đó chúng ta đặt 2 phương thức này ở lớp Bird để reuse code.
    • Hành vi jug() là khác nhau nên chúng ta sẽ tự override lại ở lớp con.

    Nghe có vẻ ổn. Bây giờ, khách hàng muốn chúng ta thêm 2 class là WildBird và CloudBird. Chúng có hành vi fly() khác so với lớp Bird, vì vậy chúng ta sẽ override lại hành vi fly(), từ đó có thiết kế:

    Nhưng vấn đề ở đây là WildBird and CloudBird có cùng hành vi fly(). Nếu chúng ta thiết kế như Diagram ở trên thì chúng ta đã tạo ra 1 sự duplicate code hành vi fly giữua lớp WildBird và CloudBird.

    Bạn có thể nghĩ 1 cách để tránh duplicate code đó là tạo 1 class cha cho WildBird và CloudBird, và đặt hành vi fly() chung vào đó, như sau:

    Khá OK. Nhưng giờ khách hàng tiếp tục muốn 1 số hành vi khác như WildBird và SkyBird có chung hành vi eat() mới – khác hành vi eat của Bird, CloudBird và MoutainBird có chung hành vi jug(), …

    Nếu bạn tiếp tục cố gắng tạo ra các lớp cha mới thì bạn sẽ tạo ra 1 kiến trúc ngày càng mở rộng, càng rối cho hệ thống của bạn. Ngoài ra bạn còn phải sửa lại kiến trúc hiện có -> Dẫn đến sửa lại rất nhiều class, code hiện có (Vi phạm nguyên tắc OCP).

    Disadvantages of inheritance

    Giờ thì hãy cùng điểm qua 1 vài nhược điểm của kế thừa:

    • 1 object chỉ có thể kế thừa từ 1 lớp cha.
    • Dễ dàng tạo ra 1 kiến trúc lớn, phức tạp
    • Thay đổi 1 function ở lớp cha -> Ảnh hưởng tới toàn bộ lớp con.
    • Thường thì chúng ta cố gắng nhét tất cả các func chung vào lớp cha -> Dẫn đến lớp cha bị phình to, đồng thời có những func của lớp cha mà lớp con này không cần dùng đến. (Điều này rất dễ thấy trong chính project hiện tại của các bạn).
    • Khi mở rộng source code thường dẫn đến phải thay đổi các code hiện có.

    Composition là gì?

    Khác với Inheritance đại diện cho quan hệ IS-A giữa 2 class, thì Composition đại diện cho quan hệ HAS-A giữa 2 class:

    • Composition có nghĩa là "thành phần". Ở ví dụ trên, Wheel là 1 thành phần của Car, nói cách khác, Car chứa 1 instance kiểu Wheel.
    • Có thể hiểu Composition như việc xếp hình. Thay vì đặt các chức năng vào đối tượng gốc, bạn sẽ tách các chức năng ra thành các đối tượng riêng lẻ, sau đó ghép các thành phần đó lại để bổ sung thêm chức năng cho đối tượng gốc của bạn.
    Thay vì đặt các login run, start,… vào đối tượng gốc Car thì ta sẽ tách ra thành các thành phần riêng lẻ

    Lợi ích của Composition:

    • Giải quyết được vấn đề tạo ra 1 big hirachy khi dùng kế thừa.
    • Dễ dàng reuse code giữa các đối tượng 1 cách linh hoạt.
      Ví dụ: Ta có thể reuse các logic turnOn, turnOff giữa các loại xe mà không cần tạo class cha.
    • Khi thêm mới/ thay đổi các hành vi của đối tượng thì không cần phải thay đổi quá nhiều code hiện có, chỉ cần viết thêm code và thay thế các hành vi mới vào hành vi hiện tại. (Đảm bảo nguyên lí SRP và OCP).
    • Để reuse code mà có liên quan đến UI kéo thả thì sử dụng Inheritance là bất khả thi, còn Composition vẫn có thể giải quyết được 😉

    Áp dụng Composition để giải quyết vấn đề ở đầu bài viết.

    1. Tạo ra các interface cho các hành vi fly và eat lần lượt là IFlyBehaviour và IEatBehaviour.
    2. Các class FlyHighBehaviour và FlyLowBehaviour conform protocol IFlyBehaviour, chúng sẽ khai báo lại phương thức fly() với các hành vi riêng. Ngoài ra có thể tạo thêm các class có phương thức fly khác tùy theo yêu cầu bài toán.
    3. Class EatLeafBehaviour conform protocol IEatBehaviour, khai báo lại phương thức eat riêng. Ngoài ra có thể tạo thêm các class có phương thức eat khác tùy theo yêu cầu bài toán.
    4. WildBird vừa có thể bay và ăn nên nó sẽ chứa 2 instance kiểu IFlyBehaviour và IEatBehaviour; Penguin không thể bay nên chỉ cần chứa 1 instance kiểu IEatBehaviour.

    Code triển khai:

    Khi cần thêm các hành vi fly mới, chỉ cần viết thêm các class conform protocol IFlyBehaviour.
    Khi cần thêm các hành vi eat mới, chỉ cần viết thêm các class conform protocol IEatBehaviour.
    Ví dụ tạo 2 đối tượng WildBird có hành vi Fly khác nhau. Các loài chim với các hành vi fly/eat khác thì chỉ cần khởi tạo class chim đó với các hành vi mong muốn
    Khởi tạo đối tượng Penguin không có hành vi bay, và hành vi ăn là ăn cá.

    Kết luận:

    • Composition đem lại nhiều lợi ích hơn, tuy nhiên không phải là luôn luôn thay thế Inheritance = Composition.
    • Sử dụng Inheritance khi bạn thật sự cần dùng đến nó, chứ không nên chỉ vì mục đích reuse code.

    Reference

    https://betterprogramming.pub/inheritance-vs-composition-2fa0cdd2f939

    Cách sử dụng composition với protocol extension: https://medium.com/commencis/reusability-and-composition-in-swift-6630fc199e16 Cách này khá hay nhưng bị 1 nhược điểm là func của protocol không thể để là private.

  • Basic CAShapeLayer iOS (P3)

    Basic CAShapeLayer iOS (P3)

    Lâu k gặp chủ đề gì hay ho để viết, ngồi viết nốt bài trong series basic CAShapeLayer đang bỏ dở từ lâu vậy =)) Bài viết này sẽ nói về cách sử animation cơ bản với CAShapeLayer.

    Content

    • Khởi tạo 1 animation
    • Các thuộc tính của animation
    • Lưu í

    Khởi tạo 1 animation

    Khởi tạo 1 animation. 1 animation có dạng CABasicAnimation.

    let animation = CABasicAnimation(keyPath: )

    Bởi vì 1 shapeLayer có rất nhiều thuộc tính khác nhau để áp dụng animation, nên khi khởi tạo 1 CABasicAnimation thì ta phải truyền keyPath vào để xác định xem sẽ thực hiện animation trên thuộc tính nào.
    Có vẻ API này đã rất lâu rồi nên Apple không define keyPath bằng enum 🙁 Nên ta phải truyền keyPath bằng 1 string 🙁
    Tìm trên mạng thì đây là các thuộc tính mà CAShapeLayer support để animate:

    Sau khi khởi tạo 1 animation, chỉ việc add animation đó vào shapeLayer của bạn.

    shapeLayer.add(animation, forKey: nil)

    Các thuộc tính của animation:

    Ta có thể cấu hình thêm cho animation 1 chút bằng các thuộc tính của CABasicAnimation như: duration, fromValue, toValue, timingFunction, fillMode, isRemovedOnCompletion, …

    duration, timingFunction, repeatCount, autoReverse

    • duration: set thời gian chạy animation
    • timingFunction: set gia tốc
    • repeatCount: số lần lặp lại animation
    • autoReverse: có reverse lại sau khi kết thúc animation hay không
    • beginTime: Thời gian bắt đầu chạy animation
    func createSquare() {
        let shapeLayer = CAShapeLayer()
            
        shapeLayer.lineWidth = 2.0
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.strokeColor = UIColor.red.cgColor
        let openCirclePath = UIBezierPath(arcCenter: CGPoint(x: 100, y: 200),
                                          radius: 60.0,
                                          startAngle: 0.0,
                                          endAngle: CGFloat.pi * 2,
                                          clockwise: true)
            
        shapeLayer.path = openCirclePath.cgPath
        view.layer.addSublayer(shapeLayer)
            
        let animation = CABasicAnimation(keyPath: "strokeEnd")
        animation.beginTime = CACurrentMediaTime() + 0.3
        animation.fromValue = 0.0
        animation.toValue = 1.0
        animation.duration = 2
        animation.repeatCount = HUGE
        animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
        shapeLayer.add(animation, forKey: nil)
    }

    fromValue, toValue

    Tương tự như giải thích ở bài trước, nó sẽ quyết định xem animation chạy % nào. Dưới đây là lần lượt 2 trường hợp chạy animation với fromValue = 0 và fromValue = 0.5

    fillMode

    Trước hết, hay thay đoạn animation ở trên bằng đoạn animation này và quan sát kết quả:

    let animation = CABasicAnimation(keyPath: "strokeColor")
    animation.beginTime = CACurrentMediaTime() + 2
    animation.fromValue = UIColor.blue.cgColor
    animation.toValue = UIColor.cyan.cgColor
    animation.duration = 2
    animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
    shapeLayer.add(animation, forKey: nil)

    Ở ví dụ trên, mình chạy animation đổi màu và bắt đầu sau 3s. Màu của layer lần lượt biến đổi như sau:

    • Đầu tiên shapeLayer có strokeColor là màu đỏ
    • Khi bắt đầu animation thì được set là màu blue, animate đến màu cyan
    • Khi kết thúc animation thì strokeColor lại thành màu đỏ

    Thuộc tính fillMode cho phép bạn kiểm soát behavior của animation tại thời điểm bắt đầu và kết thúc của animation. fillMode gồm: forward, backward, both và removed. Có các tác dụng như sau:

    • removed: giá trị default của fillMode. Effect của animation sẽ bị remove khi animation kết thúc -> Lí do đầu tiên khiến strokeColor quay về thành màu đỏ
    • backward: Hiển thị khung hình đầu tiên của animation ngay lập tức. Ở trường hợp này sẽ hiển thị màu xanh ngay lập tức.
    • forward: Giữ lại khung hình cuối cùng của animation cho đến khi bạn remove animation.
    • both: kết hợp forware và backward.

    set fillMode cho animation = .both và quan sát kết quả:

    isRemovedOnCompletion

    Wait? Tại sao set fillMode = .both hoặc .forward mà vẫn bị reset về màu đỏ ban đầu vậy?

    Việc set fillMode = forward chỉ có tác dụng giữ EFFECT cuối cùng vẫn được giữ ở animation, nhưng animation đã bị remove khỏi layer khi thực hiện xong rồi =))

    Vì vậy để giữ cho animation không bị remove thì bạn sẽ phải set đồng thời cả 2 thuộc tính:

     animation.fillMode = .forward
     animation.isRemovedOnCompletion = false
    Uy tín luôn

    Lưu ý

    #1
    Khi app xuống background thì animation sẽ bị remove khỏi layer -> Các animation sẽ bị mất. Để tránh tình trạng này thì chỉ cần set thuộc tính sau cho animation:

    animation.isRemovedOnCompletion = false

    #2

    • Có rất nhiều keyPath để set cho CABasicAnimation, bạn có thể tham khảo thêm ở đây.
    • Tuy nhiên, chỉ những thuộc tính mình để cập ở trên mới được dùng cho CAShapeLayer.
      Việc dùng các keyPath khác để dùng cho CAShapeLayer có thể đem lại những animation không được như ý muốn: ví dụ như 1 vài animation rotate, …
    • Nếu muốn kết hợp các animation không support CAShapeLayer với các shapeLayer thì có thể trick bằng cách add shapeLayer lên 1 UIView, rồi add animation lên layer của view đó.
    loadingView.layer.addSublayer(shapeLayer)
    let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
    loadingView.layer.add(rotationAnimation, forKey: nil)

    #3
    CABasicAnimation có các delegate animationDidStart và animationDidStop để handle thêm nếu muốn nhưng sẽ k đề cập ở bài viết này.

    #4
    CABasicAnimation thì có 1 vài thuộc tính để custom, và cũng chỉ animate đơn giản được bằng fromValue và endValue.
    Để có thể custom được nhiều hơn thì có thể sử dụng thêm keyFrame Animation cho ShapeLayer. // Có thể mình sẽ viết ở 1 bài khác

  • Swift – Basic to advanced Closure

    Swift – Basic to advanced Closure

    Với nhiều bài toán, không phải lúc nào cũng đơn giản. Ví dụ chỉ với 2 số nguyên, thực tế có rất nhiều công thức áp dụng được với 2 số này, từ đơn giản như cộng, trừ, nhân, chia … đến phức tạp như hàm mũ, khai căn,… Nếu chỉ sử dụng cách định nghĩa sẵn các function ta không giải quyết tất các các case của bài toán. Giải pháp ở đây là chúng ta sử dụng closure.


    Đầu mục bài viết

    Closure cơ bản

    1. Tạo closure cơ bản
    2. Tạo tham số cho closure
    3. Trả về giá trị từ closure
    4. Truyền closure như 1 tham số vào Function

    Closure nâng cao

    1. Truyền closure với nhiều tham số vào Function
    2. Trả về closure từ functiion
    3. Lưu trữ dữ liệu với closure

    Các kiểu gọi hàm với closure

    Tổng kết


    Basic closure

    1. Tạo closure cơ bản

    Swift cho phép chúng ta sử dụng function như bất kỳ kiểu dữ liệu nào,ví dụ như string, integers,… Điều này có nghĩa rằng chúng ta có thể tạo ra 1 function và gán nó cho một biến, gọi function đó bằng cách sử dụng biến đó và thậm chí có thể gán function đó vào các function khác dưới dạng tham số.

    Function mà ta sử dụng theo cách này được gọi là closure, mặc dù cách hoạt động của nó giống function tuy nhiên cách viết khác nhau 1 chút.

    Ví dụ đơn giản để print 1 đoạn text :

    Ở đây mình tạo ra một function tuy nhiên không có tên, và gán nó cho biến driving. Ta có thể gọi driving() như thể nó là 1 hàm thông thường :

    2. Tạo tham số cho closure

    Khi khởi tạo closure, ta sẽ nhận ra nó không có tên nên sẽ không có bất kỳ vị trí nào để thêm tham số như function bình thường. Tuy nhiên không có nghĩa là closure không nhận tham số input, chỉ là nó có cách làm khác so với function: các tham số được liệt kê bên trong dấu {}.

    Cách tạo ra các tham số để closure có thể “chứa chấp” rất đơn giản, chỉ cần liệt kê chúng bên trong dấu ngoặc đơn ngay sau dấu ngoặc nhọn mở, sau đó thêm keyword in để Swift biết phần nào là phần bắt đầu closure.

    Ví dụ mình sửa driving() bên trên thành closure có chứa tham số

    Và một trong những điểm khác biệt nữa giữa closure và function là bạn không cần sử dụng tên tham số khi gọi closure.

    3. Trả về giá trị từ closure

    Closure có thể trả về giá trị, và cách viết tương tự như khai báo tham số: viết nó trong closure, ngay trước keyword in.

    Với closure driving() bên trên, mình sẽ trả về đoạn string kia thay vì print thẳng ra, để làm thế ta sử dụng → String trước keyword in, sau đó sử dụng return như function bình thường

    Bây giờ có thể gọi closure này và in ra giá trị String nó trả về

    4. Truyền closure như một tham số vào function

    Vì closure có thể được sử dụng như string, integer,…, bạn có thể truyền nó vào 1 function. Syntax của nó khá là rắc rối với newbie tuy nhiên nếu bạn đã hiểu về nó thì sẽ thấy không rắc rối lắm :))

    Đây là closure driving() gốc của chúng ta

    Nếu bạn muốn truyền closure này vào trong 1 function để nó có thể thực thi bên trong function đó, bạn phải chỉ định kiểu tham số là () -> Void.

    Ví dụ mình viết 1 function travel() mà nó nhận tham số là các kiểu action khác nhau

    Ta có thể gọi hàm travel() mà sử dụng closure driving

    Bên trên là 1 ví dụ về truyền closure như 1 tham số, tuy nhiên ta đang sử dụng () → Void, nói cách khác là không truyền vào tham số và cũng không nhận giá trị trả về.

    Tuy nhiên,1 closure vẫn có thể nhận tham số của chính nó khi chính closure đó đang là 1 tham số của function khác.

    Ví dụ, mình viết lại function travel() mà nó chỉ có 1 closure là tham số duy nhất, và closure đó nhận tham số là 1 String

    Và để thực thi function travel(), ta gọi nó với 1 tham số closure


    Advanced closure

    1. Truyền closure với nhiều tham số vào function

    Mình lại xin phép viết lại hàm travel() bên trên, tuy nhiên lần này hàm travel() của chúng ta sẽ cần 1 closure mà nó chứa một vài thông tin khác thay vì 1 string như bên trên, cụ thể nó sẽ chứa thông tin về địa điểm mà một người sẽ đến và tốc độ họ đi. Lúc này chúng ta cần sử dụng (String, Int) → String cho kiểu tham số của closure

    Để thực thi function travel(), ta gọi function này với tham số closure được truyền vào

    ( Bạn có thể thấy lạ với các keyword $0 và $1, hiện tại bạn không hiểu cũng không sao cả vì mình sẽ giải thích rõ hơn ở dưới =]] )

    Closure giống với function là nó có thể nhận bao nhiêu tham số cũng được, tuy nhiên bạn có thể thấy rằng một function mà nhận quá nhiều tham số thì sẽ rất khó hiểu dẫn đến confuse, điều này còn kinh khủng hơn với closure khi bản chất closure cũng đã rất phức tạp rồi. Vì thế để mọi thứ clear bạn nên chỉ sử dụng từ 1 đến 3 tham số  mà thôi.

    2. Trả về closure từ function

    Tương tự như việc bạn truyền tham số closure vào function, bạn cũng có thể nhận về 1 closure mà được trả về từ function.

    Syntax để return closure từ function có hơi rắc rối 1 chút, bởi vì nó dùng → 2 lần: một để chỉ định giá trị trả về của function và một để chỉ định giá trị trả về từ closure.

    Mình lại viết lại hàm travel() mà không nhận tham số, tuy nhiên lại có trả về 1 closure mà closure nhận tham số là String và trả về Void

    Chúng ta gọi travel() để nhận về closure đó, sau đó gọi nó như 1 function

    Còn 1 cách gọi nữa để gọi trực tiếp giá trị trả về từ travel() – cách này không được khuyến khích sử dụng:

    3. Lưu trữ dữ liệu với closure

    Nếu bạn sử dụng bất kỳ giá trị bên ngoài nào trong closure, Swift sẽ lưu và giữ chúng cùng closure, vì thế giá trị này có thể bị thay đổi kể cả nó không còn tồn tại.

    Ví dụ, việc lưu trữ giá trị trong closure xảy ra khi ta tạo 1 giá trị trong hàm travel() mà giá trị đó được sử dụng trong closure, ở đây ví dụ như ta muốn kiểm tra xem closure được gọi trả về bao nhiêu lần

    Mặc dù biến counter được tạo bên trong travel(), nó sẽ được lưu trữ bởi closure vì thế nến nó sẽ vẫn luôn tồn tại cho closure đó.

    Vì thế nếu ta gọi result(“London”) nhiều lần, biến đếm sẽ luôn tăng lên :


    Các kiểu gọi hàm với closure

    Ví dụ, ở đây định nghĩa 1 functiontypecalculationresultcallback với tham số đầu vào là 1 kiểu int và không có giá trị trả về, chúng ta cũng có thêm 1 hàm là multiplyNumber với 2 tham số nhận vào là kiểu int, không có giá trị trả về và kèm theo 1 tham số có tên là callback có kiểu là CalculationResultCallback. Trong ngôn ngữ lập trình nói chung, callback có nghĩa là lời gọi hàm sau, tức là sau quá trình sử lý logic và ra được 1 kết quả nào đó mà chúng ta cần xử lý thêm với kết quả đó.

    Hàm multiplyNumbers có ý nghĩa là, với 2 giá trị nguyên bất kỳ, nó sẽ tính phép nhân 2 số, sau đó sẽ trả về kết quả bằng 1 hàm mà ở đó, ta có thể tùy ý sử dụng kết quả theo ý ta muốn.

    Ta có các cách gọi hàm như sau:

    • Ta có thể thấy, 2 tham số đầu tiên có kiểu int được gọi hoàn toàn bình thường, nhưng với tham số thứ 3 với tên là callback, thay vì truyền vào 1 số, hoặc 1 tên hàm như trước, thì ta truyền vào hẳn 1 hàm,mà ở đó ta in ra dòng “Tích của 2 số là …”
    • Trong closure này,chúng ta có từ khóa in phân cách 2 nửa,bên trái là khai báo tham số nằm trong () và khai báo kiểu trả về là void,bên phải là lệnh được thực thi khi gọi đến closure này,toàn bộ closure được bao trong cặp dấu {}.
    • Giống như function bình thường,ta có thể bỏ void nếu hàm không trả về kết quả,và có thể bỏ đi luôn () bao tham số.
    • Đây chính là dạng gọi cơ bản nhất của closure
    • Swift quy định,với những hàm khai báo closure là tham số nhưng ở vị trí cuối cùng trong danh sách tham số: ví dụ như trên,ta có thể gọi hàm bằng cách gọi hàm với 2 tham số int bình thường,kéo theo 1 tham số closure được bao bằng dấu {},trường hợp này được gọi là Trailing closure
    • Để closure gọn hơn nữa, có 1 tính năng gọi là Shorthands, bản chất là thay vì khai báo tham số với tên biến cụ thể thì chúng ta sử dụng cú pháp $0,$1,$2,… để thay thế cho các tham số ở vị trí 0,1,2,.. Tất nhiên,nếu hàm chỉ có 1 tham số mà ta gọi đến $1 thì hàm sẽ báo lỗi.
    • Đối với shorthands, ta không thể khai báo tham số như những trường hợp trên nữa,đổi lại sẽ ngắn gọn hết sức có thể..
    • Tuy nhiên,với hàm có nhiều tham số,ta khó xác định $0,$1,$2,… là gì.Vì thế nên chỉ sử dụng shorthands với hàm đơn giản chỉ có từ 1-3 tham số mà thôi.

    Tổng kết

    Về bản chất

    • Closure là 1 function.
    • Nhưng là function không đầy đủ tên function và thân function , mà chỉ có mỗi thân function .
    • Mục đích của nó không phải gọi function bằng tên, mà là được chèn vào tham số của 1 function khác.
  • How to write clean code (P1)

    How to write clean code (P1)

    Mặc dù lâu không viết gì vì đang bận làm đồ án, nhưng sau khi khi hoàn thành 1 task về refactor vài nghìn dòng code đã là động lực để em phải ngoi lên viết bài viết này, để chia sẻ về cách viết code mà bản thân đang áp dụng.
    Phần 1 của bài viết sẽ giới thiệu về cách cải thiện code đơn giản và dễ dàng nhất: Đặt tên

    Nội dung bài viết

    • Tầm quan trọng của clean code
    • Meaningful names

    Tầm quan trọng của clean code

    Tại sao clean code lại quan trọng?

    • Những dòng code cũng chính là 1 bản design document, vì vậy nếu code được viết 1 cách gọn gàng, dễ hiểu thì khi 1 người mới đọc code thì sẽ dễ dàng nắm được logic, flow của code – Sếp HoaND1 said.
    • Việc viết code 1 cách clean sẽ giảm thiểu thời gian dể người khác, hoặc chính bạn sau 1 thời gian đọc lại, có thể nhanh chóng hiểu được; tránh gây ra những hiểu lầm về mặt logic.
    Clean code cũng giúp tránh việc đồng nghiệp của bạn phải thốt lên “WTF” nữa

    Meaningful names

    Việc chọn 1 cái tên sao cho truyền tải đủ ý định của biến, hàm, class, … đôi khi sẽ mất nhiều thời gian, những nó sẽ tiết kiệm được nhiều thời gian hơn so với thời gian bị mất.
    Một cái tên tốt nên thể hiện tại sao biến này, hàm này, … tồn tại, nó có tác dụng thế nào, được sử dụng thế nào mà không cần tốn thêm quá nhiều thời gian để đi tìm hiểu chúng làm gì.

    let temp: Int = 0
    func check() -> Bool {}

    Ví dụ với những cái tên kiểu này, đồng nghiệp của bạn sẽ phải lặn lội mọi ngóc ngách nơi biến, hàm được gọi để có thể hình dung 1 cái nhìn mơ hồ xem chúng có tác dụng gì… Hãy biết thương đồng nghiệp của bạn.
    Ok, giờ thì đi tìm hiểu 1 vài cách đặt tên cho tốt

    Coding Conventions

    Trong swift, có 1 vài coding conventions cơ bản trong việc đặt tên như:

    • Tên biến, tên class nên là danh từ, tên hàm nên bắt đầu bằng 1 động từ
    • Sử dụng camel case (tránh dùng snakeCase)
    • Viết hoa chữ cái đầu cho các kiểu dữ liệu type, protocol, viết thường cho các thứ khác. …

    Hãy chọn những cái tên cụ thể với hành vi

    func sortListObject() {}

    Như tên hàm này không được cụ thể lắm, vì qua cái tên không thể hiện được là object sẽ sort theo kiểu gì?
    Nếu sort theo tên thì nên sửa lại thành sortListObjectById() chẳng hạn, hoặc sortListObjectByName() nếu sort theo tên.

    Nếu có điều gì quan trọng về 1 biến, 1 hàm mà người đọc nên biết, thì nên thêm thông tin đó vào tên.

    var id: String // "af84ef845cd8"
    -> var hexID: String

    Tránh những cái tên chung chung

    Những cái tên mơ hồ như temp, tmp, i, j, … trong đa số trường hợp thường không thể hiện được quá nhiều thông tin. Vì vậy, hãy chọn 1 cái tên truyền tải nhiều ý nghĩa hơn thay vì chúng.

    Cũng có 1 vài trường hợp những cái tên chung chung có thể chấp nhận. Ví dụ như:

    if left < right {
       let temp = left
       left = right
       right = left
    }

    Trong những trường hợp như thế này, việc sử dụng tên “temp” cũng ổn. Bởi mục đích của nó là lưu trữ tạm thời,với thời gian tồn tại chỉ vài dòng, nó cũng không có nhiệm vụ nào khác. Nó không được chuyển sang chức năng khác hoặc được đặt lại, hoặc sử dụng nhiều lần. Và quan trọng hơn là người khác vẫn có thể dễ dàng hiểu.

    Hãy chọn những cái tên ý nghĩa. Nếu bạn định sử dụng những cái tên chung chung như “temp”, “tmp”, … hãy chắc chắn rằng có 1 lí do hợp lí cho việc đó.

    Tránh những tên quá dài

    • Khi chọn tên, nên tránh những cái tên quá dài, bởi vì chúng rất khó đọc và khó để nhớ
    newNavigationControllerWrappingViewControllerForDataSourceOfClass
    • Đôi khi những cái tên cũng chứa những thông tin bị thừa, mà nếu bỏ đi thì cũng không ảnh hưởng gì tới ý nghĩa.
    func convertToString() 
    func toString() // Vẫn truyền tải đủ ý nghĩa

    Tránh những cái tên hiểu lầm

    • Tránh đặt những tên kiểu getXYZ(), sizeXYZ(), … nếu bên trong thân hàm xử lý những logic có độ phức tạp tính toán lớn, bởi những cái tên này thường dễ khiến người đọc hiểu lầm là hàm này "lightweight" nên dùng 1 cách thường xuyên.
    // Tên hàm gây hiểu lầm
    func getNumberOfSelectedObjects() -> Int {
       Loop hundreds time to calculate ...
    }
    • 1 vài kiểu gây hiểu lầm khác như đặt tên biến là objectIndexs nhưng lại trả về kiểu Object, …
    • Những cái tên trả về kiểu boolean thì tên biến, hàm nên bắt đầu bằng các từ như is, has, can, should, … để làm cho rõ nghĩa.
    • 1 số cái tên thông dụng khác như max, min cho giới hạn; first, last cho thứ tự,…

    Kết luận

    Hãy cố gắng chọn những cái tên truyền đạt đầy đủ ý nghĩa và dễ hiểu đối với mọi người.

    Tham khảo

    Sách "The Art of Readable Code" – O’Reilly
    Sách "Clean Code" – Uncle Bob

  • Tạo HTTP Request với URLSession

    Tạo HTTP Request với URLSession

    Alamofire là thư viện về HTTP Networking được biết đến nhiều nhất trong lập trình iOS sử dụng Swift. Vậy nếu không sử dụng Alamofire thì chúng ta thực hiện các HTTP request như thế nào? Dưới đây là một trong những cách để thực hiện các request với URL Loading System được cung cấp ở ngay thư viện cơ bản nhất Foundation của Apple.

    URL Loading System bao gồm các cấu trúc, giao thức để làm việc với URL và giao tiếp với server. Ở bài viết này chúng ta sẽ làm việc chủ yếu với lớp URLSession.

    1. Thực hiện Request đơn giản với URLSession

    Lấy ví dụ thực hiện search key word “Son Tung MTP” với iTunes Store API, chúng ta cần thực hiện một request với thông tin sau đây:

     let iTunesHostURL = "https://itunes.apple.com/search?term=Son+Tung+MTP"
     guard let url = URL(string: iTunesHostURL) else {
         print("URL Not valid")
         return
     } 

    Để thực thi request trên, ta sử dụng URLSession như sau:

     let task1 = session.dataTask(with: url, completionHandler: { data, response, error in
         if let error = error {
             print(error)
         }
         if let response = response {
             print(response)
         }
         if let data = data, let dataString = String(data: data, encoding: .utf8) {
             print(dataString)
         }
     })
     task1.resume() 

    Nhân vật chính là phương thức dataTask(with:completionHandler:) của URLSession, ở đây ta chỉ cần khởi tạo một URL với đường dẫn sẵn có, sau đó xử lý dữ liệu bên trong completionHandler block của phương thức trên. Với TH trên, thông tin in ra của response (HTTP status code, headers) hiển thị trên output log như sau:

    { Status Code: 200, Headers { "Cache-Control" = ( "max-age=86400" ); "Content-Disposition" = ( "attachment; filename=1.txt" ); "Content-Encoding" = ( gzip ); "Content-Length" = ( 7849 ); "Content-Type" = ( "text/javascript; charset=utf-8" ); Date = ( "Tue, 08 Dec 2020 08:55:43 GMT" ); "Strict-Transport-Security" = ( "max-age=31536000" ); Vary = ( "Accept-Encoding" ); "apple-originating-system" = ( MZStoreServices ); "apple-seq" = ( 0 ); "apple-timing-app" = ( "277 ms" ); "apple-tk" = ( false ); b3 = ( "ab8b7390acfee63d8fddd6db794d7776-27de63d447f98e57" ); "x-apple-application-instance" = ( 2007320 ); "x-apple-application-site" = ( ST11 ); "x-apple-jingle-correlation-key" = ( VOFXHEFM73TD3D6523NXSTLXOY ); "x-apple-orig-url" = ( "https://itunes.apple.com/search?term=Son+Tung+MTP" ); "x-apple-partner" = ( "origin.0" ); "x-apple-request-uuid" = ( "ab8b7390-acfe-e63d-8fdd-d6db794d7776" ); "x-apple-translated-wo-url" = ( "/WebObjects/MZStoreServices.woa/ws/wsSearch?term=Son+Tung+MTP&urlDesc=" ); "x-b3-spanid" = ( 27de63d447f98e57 ); "x-b3-traceid" = ( ab8b7390acfee63d8fddd6db794d7776 ); "x-cache" = ( "TCP_MISS from a113-171-230-176.deploy.akamaitechnologies.com (AkamaiGHost/10.2.2.1-31386017) (-)" ); "x-cache-remote" = ( "TCP_MISS from a23-67-57-164.deploy.akamaitechnologies.com (AkamaiGHost/10.2.2.1-31386017) (-)" ); "x-content-type-options" = ( nosniff ); "x-true-cache-key" = ( "/L/itunes.apple.com/search vcd=2897 ci2=term=Son+Tung+MTP///" ); "x-webobjects-loadaverage" = ( 0 ); } }

    Thông tin data trả về sau khi được convert thành String hiển thị lên output log như sau:

    { "resultCount":48, "results": [ {"wrapperType":"track", "kind":"song", "artistId":705007874, "collectionId":1380326325, "trackId":1380326334, "artistName":"Sơn Tùng M-TP", "collectionName":"Lạc Trôi - Single", "trackName":"Lạc Trôi", "collectionCensoredName":"Lạc Trôi - Single", "trackCensoredName":"Lạc Trôi", "artistViewUrl":"https://music.apple.com/us/artist/s%C6%A1n-t%C3%B9ng-m-tp/705007874?uo=4", "collectionViewUrl":"https://music.apple.com/us/album/l%E1%BA%A1c-tr%C3%B4i/1380326325?i=1380326334&uo=4", "trackViewUrl":"https://music.apple.com/us/album/l%E1%BA%A1c-tr%C3%B4i/1380326325?i=1380326334&uo=4", "previewUrl":"https://audio-ssl.itunes.apple.com/itunes-assets/AudioPreview118/v4/d3/ee/c7/d3eec748-a929-3ed5-4850-f79a13c34dbb/mzaf_4185904769253643682.plus.aac.p.m4a", "artworkUrl30":"https://is5-ssl.mzstatic.com/image/thumb/Music118/v4/02/81/33/028133f4-db9c-2665-bfd2-7a38389355fc/source/30x30bb.jpg", "artworkUrl60":"https://is5-ssl.mzstatic.com/image/thumb/Music118/v4/02/81/33/028133f4-db9c-2665-bfd2-7a38389355fc/source/60x60bb.jpg", "artworkUrl100":"https://is5-ssl.mzstatic.com/image/thumb/Music118/v4/02/81/33/028133f4-db9c-2665-bfd2-7a38389355fc/source/100x100bb.jpg", "collectionPrice":1.29, "trackPrice":1.29, "releaseDate":"2016-12-31T12:00:00Z", "collectionExplicitness":"notExplicit", "trackExplicitness":"notExplicit", "discCount":1, "discNumber":1, "trackCount":1, "trackNumber":1, "trackTimeMillis":232889, "country":"USA", "currency":"USD", "primaryGenreName":"Alternative", "isStreamable":true}...]}

    2. Thực hiện Request tuỳ chỉnh HTTP Request Methods

    Có tổng cộng 9 loại HTTP Request Methods, hai loại phổ biến nhất là GET và POST. Đối với các API được định nghĩa Method, chúng ta sẽ sử dụng thêm URLRequest để tuỳ chỉnh các methods trên. Tiếp tục với ví dụ ở trên, ta sẽ định nghĩa HTTP method là GET như sau:

     var request = URLRequest(url: url)
     request.httpMethod = "GET"

    Đối với các HTTP Methods khác, ta chỉ cần thay đổi giá trị httpMethod dưới dạng string như “POST”, “PUT”, …
    Để gửi request với URLRequest, ta sử dụng phương thức dataTask(with:completionHandler:), kết quả vẫn không đổi so với ví dụ request ở trên:

     let task = session.dataTask(with: request, completionHandler: { data, response, error in
         if let error = error {
             print(error)
         }
         if let response = response {
             print(response)
         }
         if let data = data, let dataString = String(data: data, encoding: .utf8) {
             print(dataString)
         }
     })
     task.resume() 

    3. Thực hiện Request tuỳ chỉnh HTTP Header

    Đối với nhiều hệ thống, việc yêu cầu thêm các thông tin của Client khi gửi request lên là bắt buộc, và sẽ được truyền lên trên HTTP Headers của Request. Ví dụ với một request yêu cầu client gửi lên các thông tin Authorization, Content-Type, Accept-Language, ta sẽ tuỳ chỉnh URLRequest như sau:

     var request = URLRequest(url: url)
     request.httpMethod = "GET"
     request.setValue("Basic bGluaG5iMTpsaW5obmIx", forHTTPHeaderField: "Authorization")
     request.setValue("application/json; charse=UTF-8", forHTTPHeaderField: "Content-Type")
     request.setValue("en-US", forHTTPHeaderField: "Accept-Language") 

    4. Thực hiện Request tuỳ chỉnh HTTP Body

    HTTP Body có rất nhiều kiểu, ví dụ như raw data (string, json), x-www-form-urlencoded, form-data, … Bài viết sẽ tập trung vào một số kiểu body phổ biến:

    4.1 HTTP Body với x-www-form-urlencoded

    x-www-form-urlencoded có dạng key-value và sẽ được mã hoá theo dạng URL Encoding (Percent Encoding) khi gửi request. Để gửi một request với body là x-www-form-urlencoded ta cần cài đặt HTTP Header như sau:

     request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")

    Đối với phần body, lấy ví dụ dữ liệu truyền lên như sau:

     let requestDictionary = ["term":"Son Tung MTP", "country":"VN", "lang":"vi_vn"]

    Chúng ta sẽ sử dụng URLComponent để chứa các thành phần key-value, sau đó encode thành URL

     var requestBodyComponent = URLComponents()
     requestBodyComponent.queryItems = [URLQueryItem(name: "term", value: "Son Tung MTP"),
                                                URLQueryItem(name: "country", value: "VN"),
                                                URLQueryItem(name: "lang", value: "vi_vn")] 
    print(requestBodyComponent.string) // print -> "?term=Son%20Tung%20MTP&country=VN&lang=vi_vn" 

    Sau đó, ta chỉ cần convert URL Encoded String thành Data và truyền vào body của request:

     request.httpBody = requestBodyComponent.query?.data(using: .utf8) 

    4.2 HTTP Body với JSON

    JSON là một kiểu dữ liệu rất phổ biến và được hỗ trợ rất tốt. Do đó, việc sử dụng request với JSON cũng rất dễ dàng. Vẫn với ví dụ dictionary ở trên, chúng ta muốn truyền đi ở dạng Json như sau “{\”country\”:\”VN\”,\”lang\”:\”vi_vn\”,\”term\”:\”Son Tung MTP\”}”:

     request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    let requestDictionary = ["term":"Son Tung MTP", "country":"VN", "lang":"vi_vn"]
     guard let jsonData = try? JSONEncoder().encode(requestDictionary) else {
         print("Error: Failed to JSON data")
         return
     } 
     request.httpBody = jsonData

    Chúng ta có thể sử dụng nhiều kiểu dữ liệu đầu vào để encode thành json Data với phương thức JSONEncoder().encode(_ value: T), chỉ cần kiểu dữ liệu đó có thuộc tính Encodable. Vì vậy, các lớp để chứa dữ liệu cho request body dạng JSon thường được kế thừa Encodable hoặc rộng hơn là Codable.

    4.3 HTTP Body với form data

    Kiểu dữ liệu multipart/form-data thường được sử dụng khi request có chứa các file đính kèm cần upload (Ví dụ: JPG, PNG, …). Lấy ví dụ với trường hợp cần upload một UIImage dạng PNG:

     let uploadImage = UIImage(named: "sample")

    Trong trường hợp này, HTTP Header và Body sẽ có dạng như sau:

    Content-Type:multipart/form-data; boundary=--26142EB6-EDB0-4F36-A4EE-079B11F200C3
    --26142EB6-EDB0-4F36-A4EE-079B11F200C3
    
    Content-Disposition: form-data; name="image_field"; filename="sample.jpg"
    Content-Type: image/jpg
    
    ￘¢\u{10}䩆䥆\u{01}\0£Œ䕸楦\0䵍*\0\u{08}\u{05}Ē\u{03}\0\u{01}\u{01}\0Ě\u{05}\0\u{01}\0Jě\u{05}\0\u{01 // Image Data
    --26142EB6-EDB0-4F36-A4EE-079B11F200C3--

    Đầu tiên, với header sẽ có format như sau:

    Content-Type:multipart/form-data; boundary=\(boundary)

    Giá trị boundary là free defined bởi người dùng và tuân theo một số ràng buộc.

    Đổi với phần body, mỗi một part sẽ có format như sau:

    --\(boundary)  // Start part 1
     Content-Disposition: form-data; name="\(partName)"
    
    \(partData)
    --\(boundary) // Start part 2
     Content-Disposition: form-data; name="\(partName)"
     Content-Type: \(contentType) // application/json
    
    \(partData)
    --\(boundary) // Start part 3
     Content-Disposition: form-data; name="\(partName)"; filename="\(fileNameOnlyForFile)"
     Content-Type: \(contentType) // image/jpg
    
    \(partData)

    Trong đó, trường filename là optional, chỉ sử dụng khi upload file, đối với dữ liệu plain thì không cần thiết. Trường contentType có một số loại như mimeType của image, application/json hoặc không cần thiết đối với plain text. partData chính là value của tham part cần truyền lên dưới dạng Data.

    Ở cuối cùng của HTTP Body, ta sẽ thêm một block nữa để đánh dấu kết thúc của phần body:

    --\(boundary)--

    Sample code sẽ trông như sau:

     let uploadImage = UIImage(named: "sample")
     guard let imageData = uploadImage?.jpegData(compressionQuality: 1) else {
         return
     }
    
     let boundary = "Boundary-\(UUID().uuidString)"
     request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
     
     let httpBody = NSMutableData()
    // Loop following 5 block for each form part
     httpBody.appendString("--\(boundary)\r\n")
     httpBody.appendString("Content-Disposition: form-data; name=\"image_field\"; filename=\"sample.jpg\"\r\n")
     httpBody.appendString("Content-Type: image/jpg\r\n\r\n")
     httpBody.append(imageData)
     httpBody.appendString("\r\n")
    
    // Add the ending block for body
     httpBody.appendString("--\(boundary)--")
     
     request.httpBody = httpBody as Data 
     extension NSMutableData {
       func appendString(_ string: String) {
         if let data = string.data(using: .utf8) {
           self.append(data)
         }
       }
     } 

    5. Thực hiện Request download file

    Để thực hiện download một file, ta có thể sử dụng downloadTask(with:completionHandler:), dưới đây là một ví dụ download file jpg và hiển thị lên UI:

     let iTunesHostURL = "https://phongvu.vn/cong-nghe/wp-content/uploads/2018/07/dota_2_traxe_drow_ranger_art_milimalism_97328_1920x1080.jpg"
     guard let url = URL(string: iTunesHostURL) else {
         print("URL Not valid")
         return
     }
     
     let task = URLSession.shared.downloadTask(with: url) { localURL, urlResponse, error in
         guard let localURL = localURL,
               let data = try? Data.init(contentsOf: localURL),
               let image = UIImage.init(data: data) else {
             print("Failed to read downloaded file to image")
             return
         }
         DispatchQueue.main.async {
             let imageview = UIImageView.init(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
             imageview.image = image
             imageview.contentMode = .scaleToFill
             self.view.addSubview(imageview)
         }
     }
     task.resume() 

    6. Một số chú ý khi sử dụng URLSession

    1. Completion Handler được thực thi ở delegate queue, do đó các cập nhật liên quan đến các thành phần thuộc UIKit (chỉ thực thi được trên main thread) cần đặc biệt chú ý khi sử dụng bên trong completionHandler.
    2. Kể từ iOS 9, mặc định các Request HTTP sẽ bị block theo chính sách AppTransportSecurity của Apple. Do đó nếu request không phải HTTPS mà là HTTP, cần cài đặt lại thông tin như sau trong info.plist
     <key>NSAppTransportSecurity</key>
     <dict>
         <key>NSAllowsArbitraryLoads</key>
         <true/>
     </dict> 

  • CoreData with MultiThreading

    CoreData with MultiThreading

    Nội dung

    • Tóm tắt CoreData
    • Sử dụng CoreData với MultiThreading
    • Debug CoreData MultiThreading
    • Kết luận

    Tóm tắt về CoreData:

    Các thành phần chính của CoreData:

    • Managed Object Model
    • Managed Object Context
    • Persistent store coordinator

    MultiThreading with CoreData:

    Khi khởi tạo managedObjectContext(MOC) thì sẽ có thể lựa chọn 1 trong 2 loại queue để khởi tạo MOC, đó là:

    • NSMainQueueConcurrencyType (main Thread)
    • NSPrivateQueueConcurrencyType (background Thread)

    NSMainQueueConcurrencyType chỉ có thể được sử dụng trên main queue.
    NSPrivateQueueConcurrencyType tạo ra 1 queue riêng để sử dụng. Vì queue này là private, nên chỉ có thể access queue thông qua hàm perform(_:)performAndWait(_:) của MOC .

    Nếu ứng dụng sử dụng nhiều thao tác data processing (parse JSON to data, …) thì việc sử dụng trên main queue sẽ gây ra block main. Khi đó, có thể khởi tạo 1 context dùng private queue và thực hiện xử lí data trên đó.

    Trước khi sử dụng CoreData với MultiThread, chú í đến điều Apple recommend:

    Hãy chắc chắn rằng MOC được sử dụng trên thread(queue) mà chúng được liên kết khi khởi tạo.

    Nếu MOC không được sử dụng trên thread(queue) mà chúng được liên kết, trong trường hợp MOC liên kết với mainQueue nhưng được sử dụng trên background thread, hoặc ngược lại, sẽ khiến app đôi lúc sẽ gặp những lỗi crash lạ.

    Vì vậy để chắc chắn MOC luôn được sử dụng trên thread mà MOC được liên kết, thì có thể sử dụng perform( _:) và performAndWait( _:) như sau:

    • perform( _:) và performAndWait( _:) sẽ tự động đưa đoạn code bên trong nó thực hiện trên queue mà context đó được khởi tạo -> Điều đó sẽ chắc chắn rằng context được sử dụng trên đúng queue.
    • perform(_:) sẽ thực hiện async hàm bên trong nó.
    • performAndWait(_:) sẽ thực hiện sync hàm bên trong nó -> Nó sẽ block thread hiện tại gọi đến hàm đó cho đến khi hàm bên trong thực hiện xong -> Không nên gọi trên main.

    Debug Concurrency:

    Để đảm bảo Context được chạy trên đúng luồng nó được liên kết khi khởi tạo, có thể bật debug CoreData Concurrency như sau:

    • Chọn Edit Scheme -> Run -> Thêm "-com.apple.CoreData.ConcurrencyDebug 1".
    • Khi bật debug này lên, nó sẽ dừng app của bạn lại tại nơi context bị dùng sai Thread.

    Ví dụ:

    • Khởi tạo 1 context bằng private queue:
    private(set) lazy var managedObjectContext: NSManagedObjectContext = {
       let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
       managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator
       return managedObjectContext
    }()
    • Sử dụng 1 context trên main:
    func createNewEntity() {
       DispatchQueue.main.async {
           let user = User(context: self.manager.managedObjectContext)
           user.name = "Hoang Anh Tuan"
                
           let account = Account(context: self.manager.managedObjectContext)
           account.username = "sunlight"
           account.password = "123"
                
           user.account = account
           account.user = user
                
           do {
               try self.manager.managedObjectContext.save()
           } catch let error {
               print("Save error: \(error.localizedDescription)")
           }
        }
    }
    • Kết quả khi bật debug:
    Có thể sử dụng perform( _:) / performAndWait( _:) để giải quyết tình huống này.

    Kết luận:

    • Nên dùng background thread cho context để tránh block main thread.
    • Nên dùng các hàm perform( _:) và performAndWait( _:) để đảm bảo context được chạy trên đúng luồng.
    • Ngoài cách ở trên thì còn 1 vài cách như sử dụng child/parent context, nhưng sẽ không được đề cập ở bài viết này.
  • Hướng dẫn tạo plugin cho dự án Cordova/Ionic

    Hướng dẫn tạo plugin cho dự án Cordova/Ionic

    Table of contents

    • Tại sao cần tạo plugin cho Cordova
    • Tạo plugin bằng plugman
    • Hoàn thiện plugin

    Tại sao cần tạo plugin cho Cordova

    Về cơ bản, thì Cordova là framework phát triển các app iOS/Android (là chính) sử dụng html/js/css làm UI, và các bộ plugin làm cầu nối để call xuống source native của platform (iOS/Android)

    Cordova bao gồm:

    • Bộ html/js/css làm UI.
    • Native webview engine làm bộ render hiển thị UI
    • Cordova framework chịu trách nhiệm cầu nối giữa function call js và funtion native.
    • Source code native làm plugin cùng các config và các pulic js method.
    Cordova project struct

    Bình thường đối với người làm Cordova thì chủ yếu họ sẽ focus vào tầng UI bằng html/js/css. Việc sử dụng các chức năng native của platform thì sẽ sử dụng các plugin được cung cấp sẵn. Vậy nên về cơ bản, một lập trình viên làm Cordova chỉ cần làm được html/js/css là đủ.

    Tuy nhiên trong một số trường hợp, các plugin có sẵn không đảm bảo giải quyết được vấn đề bài toán, lúc này việc phát triển riêng một plugin thực hiện được logic của project và support được các platform là điều cần phải làm.

    Trong một vài trường hợp khác, có thể dự án đã có sẵn source native, tuy nhiên cần chuyển sang Cordova để support multi platform và tận dụng source code native có sẵn.

    -> Đo đó, hiểu biết về cách tạo một plugin để giải quyết nhu câu bài toán sẽ nảy sinh. Bài viết này sẽ tập trung vào việc

    • Làm thế nào để tạo plugin
    • Luồng xử lý từ js xuống native source của plugin như nào
    • Install plugin vào Cordova project
    • Build và test plugin trên iOS và Androd

    Tạo plugin bằng plugman

    Tạo Cordova project

    Để tạo Cordova plugin sample thì trước tiên cần có một Cordova project để test việc add plugin và kiểm tra hoạt động của plugin trên từng platform.

    • Install Cordova CLI: sudo npm install -g cordova
    • Create Cordova project: cordova create SamplePlugin com.nhathm.samplePlugin SamplePlugin

    Install plugman và tạo plugin template

    plugman là command line tool để tạo Apache Cordova plugin. Install bằng command: npm install -g plugman

    Create plugin:

    • Command: plugman create –name pluginName –plugin_id pluginID –plugin_version version
    • Ví dụ: plugman create –name GSTPlugin –plugin_id cordova-plugin-gstplugin –plugin_version 0.0.1

    Thêm platform mà plugin sẽ hỗ trợ:

    • plugman platform add –platform_name android
    • plugman platform add –platform_name ios

    Sau khi cài đặt xong thì thư mục plugin sẽ có struct như dưới.

    .
    └── GSTPlugin
    ├── plugin.xml
    ├── src
    │ ├── android
    │ │ └── GSTPlugin.java
    │ └── ios
    │ └── GSTPlugin.m
    └── www
    └── GSTPlugin.js

    Ở đây, plugin.xml là file config cho plugin, bao gồm các thông tin như tên của plugin, các file assets, resources. Define js-module như file js của plugin, define namespace của plugin, define các plugin phụ thuộc của plugin đang phát triển…

    Hoàn thiện plugin

    Cùng view file plugin.xml của plugin mới tạo:

    <?xml version='1.0' encoding='utf-8'?>
    <plugin id="cordova-plugin-gstplugin" version="0.0.1"
    xmlns="http://apache.org/cordova/ns/plugins/1.0"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <name>GSTPlugin</name>
    <js-module name="GSTPlugin" src="www/GSTPlugin.js">
    <clobbers target="cordova.plugins.GSTPlugin" />
    </js-module>
    <platform name="android">
    <config-file parent="/*" target="res/xml/config.xml">
    <feature name="GSTPlugin">
    <param name="android-package" value="cordova-plugin-gstplugin.GSTPlugin" />
    </feature>
    </config-file>
    <config-file parent="/*" target="AndroidManifest.xml" />
    <source-file src="src/android/GSTPlugin.java" target-dir="src/cordova-plugin-gstplugin/GSTPlugin" />
    </platform>
    <platform name="ios">
    <config-file parent="/*" target="config.xml">
    <feature name="GSTPlugin">
    <param name="ios-package" value="GSTPlugin" />
    </feature>
    </config-file>
    <source-file src="src/ios/GSTPlugin.m" />
    </platform>
    </plugin>

    Trong file này có một vài điểm cần hiểu như dưới:

    • <clobbers target="cordova.plugins.GSTPlugin" /> đây là namespace phần js của plugin. Từ file js, call xuống method native của plugin thì sẽ sử dụng cordova.plugins.GSTPlugin.sampleMethod
    • <param name="android-package" value="cordova-plugin-gstplugin.GSTPlugin" /> đây là config package name của Android, cần đổi sang tên đúng => <param name="android-package" value="com.gst.gstplugin.GSTPlugin" />. Như vậy Cordova sẽ tạo ra file GSTPlugin.java trong thư mục com/gst/gstplugin.

    Trong sample này, chúng ta sẽ sử dụng Swift làm ngôn ngữ code Native logic cho iOS platform chứ không dùng Objective-C, do đó phần platform iOS cần update.

    • Trong thẻ <platform name="ios> thêm tag <dependency id="cordova-plugin-add-swift-support" version="2.0.2"/>. Đây là plugin support việc import các file source Swift vào source Objective-C. Mà bản chất Cordova sẽ generate ra source Objective-C cho platform iOS.
    • Vì sử dụng Swift nên ta thay thế <source-file src="src/ios/GSTPlugin.m" /> bằng <source-file src="src/ios/GSTPlugin.swift" />. Và đổi tên file GSTPlugin.m sang GSTPlugin.swift

    Tiếp theo, chỉnh sửa các file js và native tương ứng cho plugin.

    File GSTPlugin.js

    • File này export các public method của plugin. Update file như dưới
    var exec = require('cordova/exec');
    
    exports.helloNative = function (arg0, success, error) {
        exec(success, error, 'GSTPlugin', 'helloNative', [arg0]);
    };
    • File này sẽ export method helloNative ra js và call method helloNative của native platform tương ứng.

    File GSTPlugin.swift

    • File này chứa logic và implementation cho iOS platform. Chỉnh sửa file như dưới
    @objc(GSTPlugin) class GSTPlugin : CDVPlugin {
        @objc(helloNative:)
        func helloNative(command: CDVInvokedUrlCommand) {
            // If plugin result nil, then we should let app crash
            var pluginResult: CDVPluginResult!
    
            if let message = command.arguments.first as? String {
                let returnMessage = "GSTPlugin hello \(message) from iOS"
                pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: returnMessage)
            } else {
                pluginResult = CDVPluginResult (status: CDVCommandStatus_ERROR, messageAs: "Expected one non-empty string argument.")
            }
    
            commandDelegate.send(pluginResult, callbackId: command.callbackId)
        }
    }

    File GSTPlugin.java

    package com.gst.gstplugin;
    
    import org.apache.cordova.CordovaPlugin;
    import org.apache.cordova.CallbackContext;
    
    import org.json.JSONArray;
    import org.json.JSONException;
    import org.json.JSONObject;
    
    import android.util.Log;
    
    public class GSTPlugin extends CordovaPlugin {
    
        @Override
        public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
            if (action.equals("helloNative")) {
                String message = args.getString(0);
                this.helloNative(message, callbackContext);
                return true;
            }
            return false;
        }
    
        private void helloNative(String message, CallbackContext callbackContext) {
            if (message != null && message.length() > 0) {
                callbackContext.success("GSTPlugin hello " + message + " from Android");
            } else {
                callbackContext.error("Expected one non-empty string argument.");
            }
        }
    }

    Tại thư mục của plugin, chạy command plugman createpackagejson . và điền các câu trả lời, phần nào không có thì enter để bỏ qua

    Switch sang thư mục chứa project Cordova SamplePlugin đã tạo, run command cordova plugin add --path-to-plugin.

    -> Ví dụ cordova plugin add /Users/nhathm/Desktop/Cordova-plugin_sample/GSTPlugin/GSTPlugin

    Bây giờ, plugin đã được add vào project Cordova. Tiếp theo sẽ chỉnh sửa source của index.html và index.js để test hoạt động của project.

    File index.html

    <div class="app">
          <h1>Apache Cordova</h1>
          <div id="deviceready" class="blink">
              <p class="event listening">Connecting to Device</p>
              <p class="event received">Device is Ready</p>
          </div>
    
          <button id="testPlugin">Test Plugin</button><br/>
    </div>

    File index.js

    • Add vào onDeviceReady()
    document.getElementById("testPlugin").addEventListener("click", gstPlugin_test);

    Thêm function:

    function gstPlugin_test() {
        cordova.plugins.GSTPlugin.helloNative("NhatHM",
            function (result) {
                alert(result);
            },
            function (error) {
                alert("Error " + error);
            }
        )
    }

    Sau khi chỉnh sửa hoàn chỉnh, build source cho platform iOS và Android

    • cordova platform add ios
    • cordova platform add android

    Build project native đã được generate ra và kiểm tra kết quả

    cordova plugin sample

    Đối với việc tạo UI cho project Cordova, chúng ta nên dùng các framework support như Ionic: https://ionicframework.com/

    Note: các dự án hybrid rất hạn chế về mặt performance, do đó nên cân nhắc khi bắt đầu dự án mới bằng hybrid

  • Working with EAAcessory & NSStream

    Working with EAAcessory & NSStream

    Nội dung:

    • EAAcessory là gì?
    • Cấu hình project để làm việc với EAAcessory.
    • Lấy thông tin accessory
    • Tạo stream để gửi & nhận data
    • Gửi data đến accessory
    • Nhận data từ accessory

    EAAcessory là gì?

    • 1 đối tượng kiểu EAAcessory đại diện cho 1 thiết bị ngoại vi đang kết nối với app thông qua Lightning connector hoặc Bluetooth.
    • EAAcessory gồm các thuộc tính chứa các thông tin quan trọng về thiết bị ngoại vi: isConnected, name, manufacturer, serialNumber, protocols mà thiết bị ngoại vi dùng, firmware version, …
    • Ngoài ra, cũng có thể lấy macAddress của thiết bị ngoại vi:
     let macAddress = accessory.value(forKey: PrinterConstant.MACAddress) as? String
    • Từ những thông tin đó, bạn có thể mở 1 session tới thiết bị ngoại vi để trao đổi dữ liệu.

    Config Project:

    • Không phải cứ kết nối Bluetooth là app sẽ scan thấy hoặc lấy được thông tin của thiết bị ngoại vi đó.
    • Để app có thể giao tiếp với thiết bị ngoại vi, thì app cần khai báo các protocols mà thiết bị ngoại vi đó hỗ trợ vào Info.plist.

    Đọc thông tin accessory:

    • Sau khi config project và kết nốt với thiết bị ngoại vi thông qua Bluetooth, có thể bắt đầu đọc thông tin của thiết bị ngọai vi:

    Đầu tiên, import ExternalAccessory:

    import ExternalAccessory
    • Đọc thông tin:
    let listAccessoryAvailable = EAAccessoryManager.shared().connectedAccessories
    accessory = listAccessoryAvailable.first
    print(accessory?.name)
    print(accessory?.serialNumber)
    print(accessory?.protocolStrings)
    ...

    Tạo socket để gửi/nhận data:

    • Để gửi/nhận data đến thiết bị ngoại vi thì cần mở 1 socket đến nó.
    func openSession() {
        guard let accessory = self.accessory else {
            return
        }
            
        guard let protocolString = accessory.protocolStrings.first else {
            return
        }
            
        session = EASession(accessory: accessory, forProtocol: protocolString)
        if session != nil {
            session?.inputStream?.delegate = self
            session?.inputStream?.open()
            session?.inputStream?.schedule(in: .current, forMode: .default)
                
            session?.outputStream?.delegate = self
            session?.outputStream?.open()
            session?.outputStream?.schedule(in: .current, forMode: .default)
        }
    }

    conform to StreamDelegate để handle event của socket:

    extension ViewController: StreamDelegate {
        func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
            switch eventCode {
            case .openCompleted:
                print("Open session complete")
            case .hasBytesAvailable:
                print("Has bytes available")
            case .hasSpaceAvailable:
                print("Has space available")
            case .errorOccurred:
                let error = aStream.streamError
                print("Error occur")
            case .endEncountered:
                print("End stream")
                aStream.close()
                aStream.remove(from: .current, forMode: .default)
            default:
                return
            }
        }
    }

    Note:

    • Gửi data đến thiết bị ngoại vi thông qua outputStream, nhận data từ thiết bị ngoại vi thông qua inputStream.

    Gửi data:

    • Sau khi đã open được session, bắt đầu gửi data sang thiết bị ngoại vi thông qua outputStream:
    guard let outputStream = session?.outputStream else {
        return
    }
    
    outputStream.write(data, maxLength: 128)

    There is no firm guideline on how many bytes to write at one time. Although it may be possible to write all the data to the stream in one event, this depends on external factors, such as the behavior of the kernel and device and socket characteristics. The best approach is to use some reasonable buffer size, such as 512 bytes, one kilobyte, or a page size (four kilobytes) – Apple

    Nhận data:

    • Khi thiết bị ngoại vi gửi dữ liệu đến app của bạn, StreamDelegate sẽ trigger event hasSpaceAvailable, khi đó app sẽ đọc data thông qua inputStream:
    func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
        switch eventCode {
    ...
            case .hasBytesAvailable:
                print("Has bytes available")
                
                let dataBuffer = [UInt8](repeating: 0, count: 128)
                guard let inputStream = session?.inputStream else {
                    return
                }
                while inputStream.hasBytesAvailable {
                    inputStream.read(UnsafeMutablePointer<UInt8>(mutating: dataBuffer), maxLength: 128)
                    print("Read Data: \(dataBuffer)")
    ...
         }
    }

    Xử lí notification khi connect/disconnect với thiết bị ngoại vi:

    • Để nhận được notification, app cần phải đăng kí nhận thông báo trước:
    EAAccessoryManager.shared().registerForLocalNotifications()
    • Tiếp theo là add Observer khi 1 accessory connect/disconnect.
    NotificationCenter.default.addObserver(self,
                                           selector: #selector(self.accessoryDidConnect(notification:)),
                                           name: NSNotification.Name.EAAccessoryDidConnect,
                                            object: nil)
    NotificationCenter.default.addObserver(self,
                                           selector: #selector(self.accessoryDidDisconnect(notification:)),
                                           name: NSNotification.Name.EAAccessoryDidDisconnect,
                                           object: nil)
    • Lấy thông tin của thiết bị ngoại vi thay đổi trạng thái:
    guard let connectedAcessory = notification.userInfo?[EAAccessoryKey] as? EAAccessory else {
        return
    }

    Scan Accessory Available:

    • App có thể scan các accessory available ở gần và kết nối Bluetooth đến chúng từ trong app mà không cần phải mở System.
    • Đầu tiên, show view scan Accessory Available:
    EAAccessoryManager.shared().showBluetoothAccessoryPicker(withNameFilter: nil) { (error) in
        
    }
    • Bạn có thể tạo predicate để filter các accessory available.
    Nguồn: internet
    • Chọn 1 accessory để thực hiện kết nối, hàm sẽ trả về error nếu xảy ra lỗi; nếu không có lỗi tức là bạn đã connect thành công.
  • Debug Conflict Constraint iOS

    Debug Conflict Constraint iOS

    Có thể bạn đã từng gặp lỗi constraint bị breaking với kiểu log như thế này:

    Thường thì Xcode sẽ tự loại bỏ 1 constraint để view không bị conflict nữa. Điều này sẽ dẫn đến UI hiển thị trên màn hình đúng hoặc không, tùy thuộc vào việc Xcode loại bỏ constraint nào.

    Tuy nhiên, kể cả trong trường hợp UI hiển thị đúng, thì chúng ta vẫn nên đi fix cái lỗi này.

    Đối với những màn hình phức tạp, nhiều subview, việc biết view nào đang bị lỗi constraint là khó có thể phán đoán được.

    Vì vậy ở bài viết này, hãy cùng nhau đi đọc đống log kia để biết view nào đang bị conflict constraint 🙂

    Tìm xem view nào bị lỗi bằng cách đọc địa chỉ

    Để í đến dòng log sau:

    Đoạn log này hướng dẫn rằng code sẽ recover bằng cách loại bỏ constraint 0x60000336d7c0 của view có địa chỉ 0x7fccafe06a10.
    Giờ thì bắt đầu đi tìm view có địa chỉ 0x7fccafe06a10:

    • Chọn Show Debug Navigator (Command + 7).
    • Chọn View Memory Graph Hierachy
    • Nhập địa chỉ của View cần tìm vào phần Filter.
    • Chọn vào View có địa chỉ cần tìm, Xcode sẽ hiển thị kết quả như sau:
    • Click chuột phải vào View, chọn Quick Look để xem đó là View nào:
    • Với Quick Look, Xcode sẽ hiển thị View đang bị conflict constraint:

    Đổi màu background của View bằng lldb:

    Nếu trong trường hợp có quá nhiều View giống nhau, và bạn vẫn chưa thể xác định đó là view nào?
    -> Dùng lldb để đổi màu background view đó để có thể xác định dễ hơn.

    Đầu tiên, pause chương trình lại và thực hiện lệnh sau ở cửa sổ lldb:

    expression [(UIView*)0x7fccafe06a10 setBackgroundColor:[UIColor redColor]]
    • Với lệnh này, chọn Quick Look với View vừa rồi thì View đã chuyển sang màu đỏ rồi, nhưng vẫn chưa hiển thị trên màn hình simulator/device.
    • Để View đổi màu ngay lập tức trên simulator/device, thực hiện tiếp 1 câu lệnh sau:
    expression (void)[CATransaction flush]

    OK, khi đó thì View bị lỗi constraint đã ngay lập tức đổi màu rồi.

    Xem tất cả constraint của View:

    • Mở Debug View Hỉeachy.
    • Tìm và chọn view bị lỗi constraint.
    • Chọn Show the size inspector (Option + Command + 5)
    • Khi đó, ở mục Constraint thì sẽ hiển thị tất cả constraint đang gắn với view đó. Cuối cùng thì chỉ cần bỏ đi constraint nào không cần thiết là được.

    Vừa leading, vừa centerX, vừa set width gây ra bị conflic constraint. Bỏ width constraint hoặc leading constraint tùy vào mục đích.

    Tham khảo: https://medium.com/ios-os-x-development/dynamically-modify-ui-via-lldb-expression-1b354254e1dd