Category: iOS

  • Design Pattern: Builder Pattern trong iOS

    Design Pattern: Builder Pattern trong iOS

    Design Pattern: Builder pattern trong iOS

    Nghe đến Design Pattern, chắc hẳn mỗi lập trình viên đều biết đến kỹ thuật quan trọng này và đã từng áp dụng nó ít nhất một lần. Design pattern giúp bạn giải quyết vấn đề một cách tối ưu nhất, cung cấp cho bạn các giải pháp trong lập trình hướng đối tượng (OOP). Phần lớn các ngôn ngữ lập trình đều có thể áp dụng Design pattern và Swift cũng không ngoại lệ!

    Design pattern có 3 nhóm chính:

    • Creational Pattern bao gồm: Abstract Factory, Factory Method, Singleton, Builder, Prototype.
    • Structural Pattern bao gồm: Adapter, Bridge, Composite, Decorator, Facade, Proxy và Flyweight.
    • Behavioral Pattern bao gồm: Interpreter, Template Method, Chain of Responsibility, Command, Iterator, Mediator, Memento, Observer, State, Strategy và Visitor.

    Hôm nay, chúng ta sẽ cùng tìm hiểu một design pattern trong nhóm Creational Pattern (Nhóm khởi tạo) là Builder pattern.

    Nội dung

    • Builder pattern là gì?
    • Vấn đề
    • Sử dụng nó như thế nào
    • Áp dụng trong iOS
    • Tổng kết

    Builder pattern là gì?

    Trong OOP, Builder pattern là một loại pattern thuộc nhóm Creational pattern (Tạo dựng), vì vậy nó sẽ giúp chúng ta giải quyết các vấn đề liên quan tới tạo dựng một object bằng cách:

    • Chia nhỏ các hàm khởi tạo của một object.
    • Đặt cấu hình mặc định và logic xử lý liên quan tới việc khởi tạo một object từ trong class ra bên ngoài và đẩy vào Builder class.

    Và việc chia nhỏ và đẩy vào Builder class như thế nào thì chúng ta hãy cùng tìm hiểu tiếp về vấn đề và áp dụng Builder Pattern trong một bài toán.

    Vấn đề

    Giả sử, chúng ta có một chương trình đặt bánh pizza, và mục tiêu của chúng ta là tạo ra chương trình đặt bánh để lấy yêu cầu từ khách hàng.

    Chúng ta có đối tượng Pizza, vả khởi tạo các đối tượng Pizza:

    
    class Pizza {
        
        var smell: String
        var base: String
        var size: String
        var isChiliSauce: Bool
        var isKetchup: Bool
        
        init(smell: String, base: String, size: String, isChiliSauce: Bool, isKetchup: Bool) {
            self.smell = smell
            self.base = base
            self.size = size
            self.isChiliSauce = isChiliSauce
            self.isKetchup = isKetchup
        }
    }
    
    
    
    let sizeM = Pizza(smell: "Bò", base: "Mỏng", size: "M", isChiliSauce: true, isKetchup: true)
    
    let sizeMChicken = Pizza(smell: "Gà", base: "Mỏng", size: "M", isChiliSauce: true, isKetchup: true)
    
    let sizeL = Pizza(smell: "Gà", base: "Dày", size: "L", isChiliSauce: true, isKetchup: true)
    
    

    Chúng ta có thể thấy hàm tạo khá nhiều thuộc tính, và có những thuộc tính mặc định ít thay đổi. Vì những thuộc tính đó ít thay đổi nên sẽ không tránh khỏi việc duplicate code. Do đó, người ta sử dụng Builder Pattern.

    Sử dụng nó như thế nào

    Class diagram

    Cùng phân tích một chút nhé:

    • Director: Đại diện cho module cần kết quả từ việc khởi tạo Pizza.
    • PizzaBuilder: Một class implement InterfaceBuilder, có nhiệm vụ khởi tạo ra Pizza, thông qua hàm build().
    • Pizza: Là object cần khởi tạo.

    Demo

    Một protocol nhằm khai báo các phương thức

    
    protocol PizzaBuilderProtocol {
        
        func build() -> Pizza
        
        func setSmell(_ smell: String) -> PizzaBuilder
    
        func setBase(_ base: String) -> PizzaBuilder
    
        func setSize(_ size: String) -> PizzaBuilder
        
        func withChiliSauce(_ isChiliSauce: Bool) -> PizzaBuilder
        
        func withKetchup(_ isKetchup: Bool) -> PizzaBuilder
    }
    
    

    PizzaBuilder triển khai các phương thức đã khai báo ở PizzaBuilderProtocol

    
    class PizzaBuilder: PizzaBuilderProtocol {
        
        private var pizza = Pizza()
        
        func build() -> Pizza {
            return self.pizza
        }
        
        func setSmell(_ smell: String) -> PizzaBuilder {
            pizza.smell = smell
            return self
        }
        
        func setBase(_ base: String) -> PizzaBuilder {
            pizza.base = base
            return self
        }
        
        func setSize(_ size: String) -> PizzaBuilder {
            pizza.size = size
            return self
        }
        
        func withChiliSauce(_ isChiliSauce: Bool) -> PizzaBuilder {
            pizza.isChiliSauce = isChiliSauce
            return self
        }
        
        func withKetchup(_ isKetchup: Bool) -> PizzaBuilder {
            pizza.isKetchup = isKetchup
            return self
        }
        
    }
    
    

    Cart ở đây giữ vai trò tương đương Director trong hình bên trên.

    
    class Cart {
        
        let orderId: String
        let products: [Pizza]
        
        init(orderId: String, products: [Pizza]) {
            self.orderId = orderId
            self.products = products
        }
    }
    
    let pizza1 = PizzaBuilder()
                .setSize("s")
                .setSmell("Gà")
                .setBase("Dày")
                .build()
            
    let cart1 = Cart(orderId: "OrderId1", products: [pizza1])
    
    

    Cách thực hiện thật đơn giản phải không? Bản chất của nó là chia nhỏ các properties trong Object ra thành các hàm getter, setter trong Builder và sau đó thực hiện set giá trị cho chúng và khởi tạo Object qua hàm build(). Và đó chính là cách viết theo Builder Pattern.

    Chắc hẳn khi các bạn đọc đến đây sẽ có một suy nghĩ rằng: Vậy tại sao không dùng default value? Đúng vậy, khi Builder Pattern được giới thiệu, ngôn ngữ họ sử dụng khi đó là C++. Nhưng, Swift của chúng ta đã cung cấp tính năng default value, và chúng ta có thể hoàn toàn loại bỏ Builder. Nói như vậy thì Builder Pattern sẽ trở nên vô dụng trong Swift?

    Chắc chắn là không. Những tài liệu trên mạng về Builder Pattern khá nhiều, và nó đang hướng người đọc vào vấn đề default value trong các ngôn ngữ như: C++, Java…Chúng ta hãy cùng nhìn lại về những thứ mà Builder làm, đó là giảm thiệu sự phức tạp khi khởi tạo thông qua hàm builder(). Bản chất chính là nó đang đóng gói việc khởi tạo một Object. Khi đó, chúng ta sẽ có thể xử lý nhiều hiện object, do something… trước khi trả về một Object hoàn chỉnh.

    Áp dụng trong iOS

    Chúng ta có thể áp dụng Builder Pattern ở rất nhiều trường hợp trong lập trình iOS. Mình, mình áp dụng trong việc customize một style cho Label, Button…

    Thường khi, chúng ta customize một Label theo một style nào đó, chúng ta sẽ thường khởi tạo như thế này, nhìn có vẻ khá rối và không được thú vị cho lắm.

    
    let attrString = NSMutableAttributedString(string: title)
            
    attrString.addAttributes([NSMutableAttributedString.Key.foregroundColor: UIColor.red],
                                      range: attrString.mutableString.range(of: title))
            
    attrString.addAttributes([NSMutableAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 13)],
                                      range: attrString.mutableString.range(of: title))
            
    attrString.addAttribute(NSAttributedString.Key.strikethroughStyle, value: 2,
                                         range: NSMakeRange(0, attrString.length))
            
    titleLabel.attributedText = attrString
    
    

    Chúng ta có thể áp dụng Builder pattern trong trường hợp này để nhìn code trở lên dễ đọc hơn và cũng dễ dàng phát triển trong tương lai.

    Tạo một builder và protocol

    
    protocol AttributedStringBuilderProtocol {
        
        func build() -> NSMutableAttributedString
        func setFont(_ font: UIFont, forSubString subString: String) -> AttributedStringBuilder
        func setColor(_ color: UIColor, forSubString subString: String) -> AttributedStringBuilder
        func withStrikeThrough() -> AttributedStringBuilder
        
    }
    
    class AttributedStringBuilder: AttributedStringBuilderProtocol {
        
        private var attrString: NSMutableAttributedString
        
        init(string: String) {
            self.attrString = NSMutableAttributedString(string: string)
        }
        
        func build() -> NSMutableAttributedString {
            return self.attrString
        }
        
        func setFont(_ font: UIFont, forSubString subString: String) -> AttributedStringBuilder {
            self.attrString.addAttributes([NSMutableAttributedString.Key.font: font],
                                          range: self.attrString.mutableString.range(of: subString))
            return self
        }
        
        func setColor(_ color: UIColor, forSubString subString: String) -> AttributedStringBuilder {
            self.attrString.addAttributes([NSMutableAttributedString.Key.foregroundColor: color],
                                          range: self.attrString.mutableString.range(of: subString))
            return self
        }
        
        func withStrikeThrough() -> AttributedStringBuilder {
            self.attrString.addAttribute(NSAttributedString.Key.strikethroughStyle, value: 2,
                                         range: NSMakeRange(0, self.attrString.length))
            return self
        }
    }
    
    

    Sử dụng

    
    let atrString = AttributedStringBuilder(string: title)
                .setFont(.boldSystemFont(ofSize: 13), forSubString: title)
                .setColor(.red, forSubString: title)
                .withStrikeThrough()
                .build()
    
    titleLabel.attributedText = attrString
    
    

    Như vậy, với trường hợp trên, việc sử dụng Builder Pattern sẽ giúp những dòng code của chúng ta trở nên dễ nhìn hơn và thuận lợi trong việc phát triển tiếp những properties của nó.

    Tổng kết

    Tóm lại, Design pattern: Builder Pattern là một trong những pattern dễ thực hiện. Nhưng khi sử dụng pattern này chúng ta cần phải mất thời gian để xác định được trường hợp nào cần nó, nó có thể trở nên rườm rà hơn khi sử dụng trong Swift. Nhưng chúng ta hãy đọc, tìm hiểu để áp dụng những tư duy và ý tưởng của một pattern vào một bài toán phù hợp. Đầu tư thời gian vào việc chuẩn bị xây dựng một sản phẩm sẽ luôn sẽ giúp việc bảo trì code trở nên dễ dàng hơn.

  • Custom navigation trainsition iOS

    Custom navigation trainsition iOS

    Là một Developer iOS thì ai cũng quen với UINavigationController và chúng ta thường quen với các animation default. Nhưng với một số thiết kế đặc biệt chúng ta cần phải custom animation cho view controller transition.

    Để custom view controller transition bạn cần hiểu về UIViewControllerAnimatedTransitioning

    I. Overview UIViewControllerAnimatedTransitioning

    UIViewControllerAnimatedTransitioning là một protocol cho phép bạn implement các animation cho custom view controller transition

    Để implement protocol này thì có 2 việc ta cần phải làm

    • Xác định duration cho animation bằng func
    func transitionDuration( using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval
    • Thực hiện animation traisition cho custom view controller
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
    • UIViewControllerContextTransitioning bao gồm tất cả thông tin về transitioning như: toView, fromView, containerView…

    Như vậy để custom view controller transition nó sẽ gói gọn trong func animateTransition

    II. Sử dụng UIViewControllerAnimatedTransitioning cho custom view controller transition

    Ở đây tôi sẽ hướng dẫn các bạn thực hiện custom animation push và pop thành animation của present(push -> bottom to top và pop -> top to bottom)

    1. Tạo enum cho NavigationTransitionStyle

    enum NavigationTransitionStyle {
        case nomal
        case bottom
    }
    • nomal: transition mặc định
    • bottom: custom animation transition từ bottom to top cho push và ngược lại cho pop

    2. Tạo class implement UIViewControllerAnimatedTransitioning

    Để hỗ trợ cho pop và push ta cần define các variable như sau:

    class CustomNavigationAnimationTransition: NSObject, UIViewControllerAnimatedTransitioning {
        var popStyle: Bool = false
           var navigationStyle: NavigationTransitionStyle = .nomal
    }
    • popStyle: flag để xác định là pop hay push
    • navigationStyle: xác định animation dạng nomal hay bottom

    2.1 Cài đặt duration cho animation

        func transitionDuration( using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
            return 0.5
        }

    2.2 Cài đặt method animationTransition cho view controller

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
    }
    • Điều hướng nếu là pop thì chúng ta sẽ xử lý animation pop ở method animatePop
    if popStyle {
        animatePop(using: transitionContext)
        return
    }
    • Thực hiện get các variable cần thiết để animation view controller: fromView, toView, frameTransition, frameOffset
    let fromView = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
    let toView = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!
    let frameTransition = transitionContext.finalFrame(for: toView)
    var frameOffset = frameTransition.offsetBy(dx: 0, dy: frameTransition.height)
    if navigationStyle == .nomal {
         frameOffset = frameTransition.offsetBy(dx: frameTransition.width, dy: 0)
    }
    • Set frame cho toView và insert toView, fromView vào trong containerView
    toView.view.frame = frameOffset
    transitionContext.containerView.insertSubview(toView.view, aboveSubview: fromView.view)
    • Thực hiện animation bottom to top cho push
    UIView.animate( withDuration: transitionDuration(using: transitionContext),
                    animations: {
                        toView.view.frame = frameTransition
                    },
                    completion: {_ in
                        transitionContext.completeTransition(true)
                    })

    2.3 Cài đặt method animatePop

    Tương tự như ở method animationTransition ở đây ta sẽ bỏ bước set frame cho toView và thực hiện animation cho fromView

    func animatePop(using transitionContext: UIViewControllerContextTransitioning) {
    
            let fromView = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
            let toView = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!
    
            let frameTransition = transitionContext.initialFrame(for: fromView)
            var frameOffset = frameTransition.offsetBy(dx: 0, dy: frameTransition.height)
            if navigationStyle == .nomal {
                frameOffset = frameTransition.offsetBy(dx: frameTransition.width, dy: 0)
            }
            transitionContext.containerView.insertSubview(toView.view, belowSubview: fromView.view)
    
            UIView.animate( withDuration: transitionDuration(using: transitionContext),
                            animations: {
                                fromView.view.frame = frameOffset
                            },
                            completion: {_ in
                                transitionContext.completeTransition(true)
                            })
    }

    3. Sử dụng CustomNavigationAnimationTransition trong view controller

    • Khỏi tạo variable custom animation bên trên
    private let navigationAnimationTransition = CustomNavigationAnimationTransition()
    • Sau đó config navigationStyle cho view controller và set delegate cho navigation controller
    navigationAnimationTransition.navigationStyle = .bottom
    navigationController?.delegate = self
    • Implement UIViewControllerTransitioningDelegate trong view controller
    extension ViewController: UIViewControllerTransitioningDelegate {
        func navigationController(
            _ navigationController: UINavigationController,
            animationControllerFor operation: UINavigationController.Operation,
            from fromVC: UIViewController,
            to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
            navigationAnimationTransition.popStyle = (operation == .pop)
            return navigationAnimationTransition
    
        }
    }

    Như vậy chúng ta vừa mới hoàn thành custom view controller transition. Ngoài cách tạo animation như trên thì bạn có thể làm bất cứ dạng animation nào mà bạn muốn.

    Tài liệu tham khảo: https://developer.apple.com/documentation/uikit/uiviewcontrolleranimatedtransitioning

  • Vision:  Person Segmentation

    Vision: Person Segmentation

    Overview

    Vision framework được giới thiệu lần đầu vào WWDC 2017. Vision giúp thực hiện phát hiện khuôn mặt, phát hiện văn bản, nhận dạng mã vạch, đăng ký hình ảnh… Vision cũng cho phép sử dụng Core ML để tùy chỉnh cho các tác vụ như phân loại hoặc phát hiện đối tượng.

    Trong sample này chúng ta sẽ cùng tìm hiểu Person Segmentation API, giúp ứng dụng của bạn tách mọi người trong hình ảnh khỏi môi trường xung quanh.

    alt text

    Notes: Sample yêu cầu chạy Physical device với ios 15+ hoặc chạy Simulator ios 15+ trên macOS chip Intel

    Cách tạo một Vision requests

    Muốn sử dụng bất kỳ thuật toán nào của Vision, bạn sẽ cần ba bước:

    1. Request: Yêu cầu xác định loại thứ bạn muốn phát hiện và một trình xử lý hoàn thành sẽ xử lý kết quả. Đây là subclass của VNRequest.

    2. Request handler: Sử dụng VNImageRequestHandler để thực hiện Request.

    3. Results: Kết quả sẽ được đính kèm với Request ban đầu và được chuyển đến trình xử lý hoàn thành được xác định khi tạo Request. Chúng là các subclass của VNObservation.

    Prepare the Requests

    // 1. Request
    let request = VNGeneratePersonSegmentationRequest { (request, error) in
        // 3. Results
        self.personSegmentation(request: request, error: error)
    }
            
    request.qualityLevel = .accurate
    request.outputPixelFormat = kCVPixelFormatType_OneComponent8
            
    // 2. Request Handler
    let imageRequestHandle = VNImageRequestHandler(cgImage: cgImage, options: [:])
    

    Chúng ta sẽ tạo 1 request với VNGeneratePersonSegmentationRequest. VNGeneratePersonSegmentationRequest giúp tạo mask hình ảnh cho người mà nó phát hiện trong hình ảnh. Việc set thuộc tính qualityLevel cho request thành .fast, .balanced, hoặc .accurate giúp xác định chất lượng của mask được tạo như trong hình minh họa bên dưới.

    alt text

    Việc tăng độ chính xác của mask cũng đồng hời làm giảm suất làm việc của app nên chúng ta cần lựa chọn qualityLevel phù hợp cho từng tác vụ mà nó xử lý

    alt text

    Thuộc tính tiếp theo là format đầu ra của mask: outputPixelFormat. outputPixelFormat định dạng mà mặt nạ kết quả sẽ được trả về, có 3 định dạng có thể chọn đó là:

    alt text

    Trong sample chúng ta set outputPixelFormat với kCVPixelFormatType_OneComponent8 với range từ 0 đến 255.

    Bước cuối cùng là thực hiện request với VNImageRequestHandler đã tạo từ trước. result trả về của request handle sẽ là 1 instance của VNPixelBufferObservation

    DispatchQueue.global(qos: .userInitiated).async {
        do {
            try imageRequestHandle.perform([request])
        } catch let error {
            print(error)
        }
    }
    

    Xử lý Results

    alt text

    Chúng ta đã tìm hiểu qua về request và các thuộc tính của nó, ta sẽ đến với phần tiếp theo là xử lý result trả về. Những gì ta cần làm ở đây là thay thế background của ảnh gốc nằm ngoài mask trả về từ result.

    guard let result = request.results?.first as? VNPixelBufferObservation else {
        loadingView.stopAnimating()
        return
    }
    // 1. Processing
    let buffer: CVPixelBuffer = result.pixelBuffer
    let maskImage: CIImage = CIImage(cvImageBuffer: buffer)
    let bgImage = UIImage(named: "background")!
    let background = CIImage(cgImage: bgImage.cgImage!)
    let input = UIImage(named: "humanFace")!
    let inputImage = CIImage(cgImage: input.cgImage!)
            
    // 2. Scale mask, and background to size of original image
    let maskScaleX = inputImage.extent.width / maskImage.extent.width
    let maskScaleY = inputImage.extent.height / maskImage.extent.height
    let maskScaled =  maskImage.transformed(by: __CGAffineTransformMake(maskScaleX, 0, 0, maskScaleY, 0, 0))
            
    let backgroundScaleX = inputImage.extent.width / background.extent.width
    let backgroundScaleY = inputImage.extent.height / background.extent.height
    let backgroundScaled =  background.transformed(by: __CGAffineTransformMake(backgroundScaleX, 0, 0, backgroundScaleY, 0, 0))
            
    // 3. Blending Image
    let blendFilter = CIFilter.blendWithMask()
    blendFilter.inputImage = inputImage
    blendFilter.maskImage = maskScaled
    blendFilter.backgroundImage = backgroundScaled
    
    // 4. Handle Result
    if let blendedImage = blendFilter.outputImage {
        let context = CIContext(options: nil)
        let maskDisplayRef = context.createCGImage(maskScaled, from: maskScaled.extent)
        let filteredImageRef = context.createCGImage(blendedImage, from: blendedImage.extent)
        DispatchQueue.main.async {
            self.imageDisplay.image = UIImage(cgImage: filteredImageRef!)
            self.maskImage.image = UIImage(cgImage: maskDisplayRef!)
        }
    }
    

    Đây là những gì ta làm trong đoạn code trên:

    1. Import ảnh gốc, hình nền cần thay thế và mask từ result trả về.

    2. Scale kích thước của mask và hình nền về size của ảnh gốc.

    3. Tạo ra 1 CoreImage blend filter, ta dùng blendWithRedMask() vì khi tạo ra CIImage của mask từ CVPixelBuffer của nó, nó sẽ tạo ra 1 object mặc định ở red chanel.

    4. Xử lý result filter, update UI.

    Đây là kết quả sau khi đã xử lý xong:

    alt text

    Kết luận

    Qua bài viết này mình muốn chia sẻ tới mọi người trình tự tạo một Vision requests, hiểu hơn về Person Segmentation API và cách sử dụng của nó. Mong rằng bài viết tới mình có thể chia sẻ tới các bạn cách sử dụng của Person Segmentation API với video.

    Refer

  • Coding convention – Những điều cần biết trước khi bắt tay vào code (Part 1)

    Coding convention – Những điều cần biết trước khi bắt tay vào code (Part 1)

    Table of contents

    • Đặt tên biến
    • Đặt tên hàm
    • Đặt tên class, struct, enum, protocol
    • Spacing
    • Comment
    • Access Control
    • Self & Closure

    Đặt tên biến

    • Hai quy tắc cơ bản nhất khi đặt tên biến đó là: sử dụng tiếng Anh thay vì tiếng Việt, sử dụng lowerCamelCase (kiểu lạc đà) thay vì snake_case

    Not Preferred

     private let height_normal_avatar: CGFloat = 60.0
     private let chieuRongNormalAvatar: CGFloat = 120.0
    

    Preferred

     private let heightNormalAvatar: CGFloat = 60.0
     private let widthNormalAvatar: CGFloat = 120.0
    
    • Khi đặt tên biến, hãy chú trọng đến sự rõ ràng, rành mạch hơn là sự ngắn gọn. Cố gắng làm sao khi đọc tên biến lên, ta có thể tưởng tượng được ngay biến đó có nhiệm vụ gì hoặc đang ám chỉ đến đối tượng nào. Vì vậy khi đặt tên biến không nên viết tắt và cũng không nên đặt tên giống với các đối tượng của hệ thống

    Not Preferred

    @IBOutlet private weak var tableView: UITableView!
    @IBOutlet private weak var imgAvatar: UIImageView!
    @IBOutlet private weak var lblbName: UILabel!
    
    private var bool: Bool = false
    

    Preferred

    @IBOutlet private weak var salaryTableView: UITableView!
    @IBOutlet private weak var avatarImageView: UIImageView!
    @IBOutlet private weak var nameLabel: UILabel!
    
    private var isLoadingState: Bool = false
    
    • Tên biến nên được bắt đầu bằng 1 danh từ và khi khai báo biến, nên khai báo luôn kiểu dữ liệu của biến đó (điều này có thể làm giảm được phần nào thời gian compile của app)

    Not Preferred

    private var dataSalaryArray = [Salary]()
    private var isLoadingState = false
    

    Preferred

    private var dataSalaryArray: [Salary] = [Salary]()
    private var isLoadingState: Bool = false
    
    • Với những biến cùng kiểu, nên đặt tên có sự thống nhất từ trên xuống dưới, tránh tình trạng mỗi biến 1 style đặt tên khác nhau. Ví dụ trong trường hợp với height và top constraint của đối tượng avatarImageView

    Not Preferred

    @IBOutlet private weak var heightOfAvatarImageView: NSLayoutConstraint!
    @IBOutlet private weak var topConstraintForAvatarImageView: NSLayoutConstraint!
    

    Preferred

    @IBOutlet private weak var heightConstraintAvatarImageView: NSLayoutConstraint!
    @IBOutlet private weak var topConstraintAvatarImageView: NSLayoutConstraint!
    

    Đặt tên hàm

    • Cũng giống như việc đặt tên biến, việc đặt tên hàm cũng có các quy tắc tương tự: dùng tiếng Anh, dùng kiểu lowerCamelCase (kiểu lạc đà)
    • Tên hàm thường được bắt đầu bằng động từ, tên hàm phải rõ ràng, rành mạch. Cố gắng làm sao khi đọc tên hàm lên, ta có thể tưởng tượng được ngay hàm đó làm nhiệm vụ gì.
    • Đối với những function có nhiều param, nên đặt mỗi param trên 1 dòng và căn lề cho chúng. Ngoài ra, với những param có giá trị mặc định, nên đặt chúng ở cuối list parameter. Còn những param không có giá trị mặc định thì nên đặt lên đầu

    Not Preferred

    func setAttributedString(string: String, font: UIFont, lineSpacing: CGFloat, alignment: NSTextAlignment = .left, icon: UIImage? = nil, íconRect: CGRect? = nil) -> NSAttributedString {
        // Do something
    }
    

    Preferred

    func setAttributedString(string: String, 
                            font: UIFont, 
                            lineSpacing: CGFloat, 
                            lignment: NSTextAlignment = .left, 
                            icon: UIImage? = nil, 
                            íconRect: CGRect? = nil) -> NSAttributedString {
        // Do something
    }
    
    • Bên cạnh việc sử dụng parameter 1 cách truyền thống, ta có thể sử dụng thêm specifying argument labels (thêm 1 label vào trước tên của param) hoặc omitting argument labels (thêm dấu gạch dưới _ vào trước tên của param). Điều này làm cho việc gọi tên hàm sẽ trở nên gần gũi hơn với ngôn ngữ tự nhiên. (Tham khảo thêm tại Function)

    Preferred

    func greet(person: String, from hometown: String) -> String {
        return "Hello \(person)!  Glad you could visit from \(hometown)."
    }
    
    print(greet(person: "Bill", from: "Cupertino"))
    // Prints "Hello Bill!  Glad you could visit from Cupertino."
    

    Preferred

    func welcome(_ person: String, from hometown: String) -> String {
        return "Hello \(person)!  Glad you could visit from \(hometown)."
    }
    
    print(welcome("Bill", from: "Cupertino"))
    // Prints "Hello Bill!  Glad you could visit from Cupertino."
    

    Đặt tên class, struct, enum, protocol

    • Đặt tên class, struct, enum và protocol ta cũng sử dụng tiếng Anh nhưng sẽ dùng kiểu UpperCamelCase – đây là điểm khác biệt so với function và property
    • Tên class, struct, enum, protocol thường được bắt đầu bằng danh từ và khi đặt tên cũng cần ưu tiên sự rõ ràng, rành mạch.

    Spacing

    • Các dấu ngoặc nhọn mở đầu cho các function và các dấu ngoặc sau các biểu thức if/else/switch/while… đều phải được mở trên cùng 1 dòng với câu lệnh, có thêm 1 khoảng trắng phía bên trái và đóng trên 1 dòng khác

    Not Preferred

    if user.isHappy
    {
      // Do something
    }
    else {
      // Do something else
    }
    

    Preferred

    if user.isHappy {
      // Do something
    } else {
      // Do something else
    }
    
    • Nên có 1 dòng trắng giữa các function, giữa các block code và giữa khu vực khai báo các properties với khu vực ánh xạ các outlet. Trong 1 function cũng cần có những dòng trắng để phân tách các chức năng nhỏ trong function đó.

    Not Preferred

    import UIKit
    class SettingScreen: UIViewController {
        @IBOutlet private weak var settingTitleLabel: UILabel!
        @IBOutlet private weak var stateSettingSwitch: UISwitch!
        var snapView: UIView?
        var snapTabbarView: UIView?
        override func viewDidLoad() {
            super.viewDidLoad()
        }
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
        }
    }
    extension SettingScreen {
        func loadData(state: String) {
            DispatchQueue.main.async {
                self.settingTitleLabel.text = state
            }
        }
    }
    

    Preferred

    import UIKit
    
    class SettingScreen: UIViewController {
    
        @IBOutlet private weak var settingTitleLabel: UILabel!
        @IBOutlet private weak var stateSettingSwitch: UISwitch!
    
        var snapView: UIView?
        var snapTabbarView: UIView?
    
        override func viewDidLoad() {
            super.viewDidLoad()
        }
    
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
        }
    }
    
    extension SettingScreen {
    
        func loadData(state: String) {
            DispatchQueue.main.async {
                self.settingTitleLabel.text = state
            }
        }
    }
    
    • Dấu 2 chấm luôn không có khoảng trắng ở phía bên trái và có 1 khoảng trắng ở phía bên phải. Ngoại trừ 3 trường hợp: toán tử 3 ngôi A ? B : C, empty dictionary [:] và #selector syntax addTarget(_:action:)

    Not Preferred

     class ViewController : UIViewController {
         private var data :[String:CGFloat] = ["A" : 1.2, "B":3.2]
     }
    

    Preferred

     class ViewController: UIViewController {
         private var data: [String: CGFloat] = ["A": 1.2, "B": 3.2]
     }
    
    • Không nên có khoảng trắng ở cuối mỗi dòng code nhưng nên có thêm 1 dòng trắng ở cuối mỗi file
    • Không có giới hạn nhất định cho số ký tự trên mỗi dòng code. Tuy nhiên mỗi dòng code của bạn không nên có quá 100 ký tự. Có 1 vài cách để làm giảm số lượng ký tự trên 1 dòng code:
      • Đối với function có nhiều param và các param có tên quá dài, bạn có thể xuống dòng và căn lề cho chúng (đã để cập ở trên)
      • Đối với các biểu thức tính toán, bạn có thể đặt ra các biến phụ thay vì gộp chung lại vào 1 biểu thức

    Not Preferred

    // Tính diện tích tam giác bất kỳ khi biết độ dài 3 cạnh (hệ thức Heron)
    func calculateSquareTriangleUsingHeron(firstEdge: CGFloat,
                                           secondEdge: CGFloat,
                                           thirdEdge: CGFloat) -> CGFloat {
        return sqrt(((firstEdge + secondEdge + thirdEdge) / 2) * ((firstEdge + secondEdge + thirdEdge) / 2 - firstEdge) * ((firstEdge + secondEdge + thirdEdge) / 2 - secondEdge) * ((firstEdge + secondEdge + thirdEdge) / 2 - thirdEdge))
    }
    

    Preferred

    // Tính diện tích tam giác bất kỳ khi biết độ dài 3 cạnh (hệ thức Heron)
    func calculateSquareTriangleUsingHeron(firstEdge: CGFloat,
                                           secondEdge: CGFloat,
                                           thirdEdge: CGFloat) -> CGFloat {
        let halfPerimeter: CGFloat = (firstEdge + secondEdge + thirdEdge) / 2
        let halfPerimeterMinusA: CGFloat = halfPerimeter - firstEdge
        let halfPerimeterMinusB: CGFloat = halfPerimeter - secondEdge
        let halfPerimeterMinusC: CGFloat = halfPerimeter - thirdEdge
        let doubleSquare: CGFloat = halfPerimeter * halfPerimeterMinusA * halfPerimeterMinusB * halfPerimeterMinusC
        let square: CGFloat = sqrt(doubleSquare)
        return square
    }
    

    Comment

    • Đôi khi ta cần phải add comment để chú thích cho các đoạn code, phục vụ cho quá trình maintenance sau này. Tất nhiên là khi code được thay đổi thì comment cũng cần được update theo.
    • Khi comment, không nên dùng C-style /*…*/ mà nên dùng double-slash // hoặc triple-slash ///. Cũng không nên để code và comment xuất hiện trên cùng 1 dòng

    Access Control

    • Các function và property nên mặc định để là private hoặc fileprivate để đảm bảo tính đóng gói trong lập trình. Nên hạn chế việc sử dụng open, public hoặc internal. Tham khảo thêm tại Access Control

    Not Preferred

    class SalaryCell: UITableViewCell {
        @IBOutlet weak var monthLabel: UILabel!
        @IBOutlet weak var incomeLabel: UILabel!
        
        override func awakeFromNib() {
            super.awakeFromNib()
        }
    }
    
    extension ViewController: UITableViewDataSource {
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return dataSalaryArray.count
        }
        
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            guard let cell = salaryTableView.dequeueReusableCell(withIdentifier: "SalaryCell",
                                                                for: indexPath) as? SalaryCell else {
                return UITableViewCell()
            }
            cell.monthLabel.text = dataSalaryArray[indexPath.row].month
            cell.incomeLabel.text = dataSalaryArray[indexPath.row].incomeLabel
            return cell
        }
    }
    

    Preferred

    class SalaryCell: UITableViewCell {
        @IBOutlet private weak var monthLabel: UILabel!
        @IBOutlet private weak var incomeLabel: UILabel!
        
        override func awakeFromNib() {
            super.awakeFromNib()
        }
        
        func setupData(data: Salary) {
            monthLabel.text = data.month
            incomeLabel.text = "\(data.income)"
        }
    }
    
    extension ViewController: UITableViewDataSource {
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return dataSalaryArray.count
        }
        
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            guard let cell = salaryTableView.dequeueReusableCell(withIdentifier: "SalaryCell",
                                                                for: indexPath) as? SalaryCell else {
                return UITableViewCell()
            }
            cell.setupData(data: dataSalaryArray[indexPath.row])
            return cell
        }
    }
    
    • Khi khai báo property, các từ khoá liên quan đến access control nên được đặt lên đầu. Chỉ có 1 số từ khoá được đứng trước chúng đó là: static, @IBAction, @IBOutlet, @discardableResult.

    Not Preferred

    @IBOutlet weak private var salaryTableView: UITableView!
    @IBOutlet weak private var fullNameLabel: UILabel!
    

    Preferred

    @IBOutlet private weak var salaryTableView: UITableView!
    @IBOutlet private weak var fullNameLabel: UILabel!
    

    Self & Closure

    • Không nên sử dụng từ khoá self một cách tuỳ ý. Chỉ dùng self trong 2 trường hợp sau:
      • Khi trình biên dịch yêu cầu, thường là khi đang trong biểu thức closure
      func loadData() {
          DispatchQueue.main.async {
              self.salaryTableView.reloadData()
          }
      }
      
      • Khi đang ở trong hàm init, ta cần phân biệt giữa property của object và param của hàm init
      class Salary {
          var income: Int
          var month: String
      
          init(income: Int, month: String) {
              self.income = income
              self.month = month
          }
      }
      
    • Đối với closure, ta có thể dùng trailling closure syntax trong trường hợp chỉ có duy nhất 1 biều thức closure trong list parameter. Còn nếu có nhiều hơn 1, ta phải giữ lại tên cho các closure đó.

    Not Preferred

    // Trong trường hợp này, chỉ có duy nhất 1 biểu thức closure nên không cần thiết phải để lại label "animations"
    UIView.animate(withDuration: 1, animations: {
        self.avatarImageView.alpha = 0.0
    })
            
    // Trường hợp này có 2 biểu thức closure, vì vậy nên để lại cả 2 label "animations" và "completion" để phân biệt chúng với nhau
    UIView.animate(withDuration: 1) {
        self.avatarImageView.alpha = 0.0
    } completion: { (_) in
        self.avatarImageView.removeFromSuperview()
    }
    

    Preferred

    UIView.animate(withDuration: 1) {
        self.avatarImageView.alpha = 0.0
    }
            
    UIView.animate(withDuration: 1,
                    animations: {
                        self.avatarImageView.alpha = 0.0
                    }, completion: { _ in
                        self.avatarImageView.removeFromSuperview()
                    })
    

    To be continue…

  • Coding convention – Những điều cần biết trước khi bắt tay vào code (Part 2)

    Coding convention – Những điều cần biết trước khi bắt tay vào code (Part 2)

    Table of contents

    • Magic number & Duplicate code
    • Code Organization
    • Scene Delegate
    • Computed Property
    • Optional
    • Multi-line String
    • Bonus

    Magic number & Duplicate code

    • Khi code ta không nên dùng những con số vô định, hay còn gọi là magic number, gây khó hiểu cho người khác. Điều này sẽ ảnh hưởng đến quá trình maintain sau này. Ta có thể thay thế những con số magic này bằng cách tạo ra các constant với tên gọi clear nhất có thể, làm sao để khi người khác đọc code của bạn, họ cũng có thể hiểu được vì sao bạn lại dùng đến con số đó. Hoặc ít nhất trước khi dùng magic number, ta phải thêm comment để giải thích lý do tại sao sử dụng chúng.
    • Ta cũng không nên để những đoạn code giống nhau được lặp đi lặp lại trong source code của mình. Nếu nhận thấy có những đoạn code cùng thực hiện một chức năng nhất định, hoặc cùng được apply cho 1 đối tượng nhất định, ta có thể nghĩ đến việc grouping chúng lại thành các function để tiện cho việc implement cũng như maintain sau này.

    Not Preferred

    class SignUpViewController: UIViewController {
    
        @IBOutlet private weak var firstNameTextField: UITextField!
        @IBOutlet private weak var lastNameTextField: UITextField!
        @IBOutlet private weak var accountTextField: UITextField!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            // Có thể thấy đoạn code này đang set borderWidth, borderColor, cornerRadius cho
            // lần lượt 3 TextField khác nhau. Tưởng tượng nếu sau này màn hình được update
            // thêm nhiều textfield khác nữa, hoặc trong app cũng có nhiều textfield cần phải 
            // setup các thuộc tính tương tự như trên. Mỗi textfield cần 3 dòng code x số lượng
            // textfield cả app = ...
            // -> Ta nghĩ đến việc tạo ra các function common để dùng chung cho các đối tượng 
            // UITextField, sẽ thuận tiện hơn cho việc implement và maintain sau này, sửa 1 hàm 
            // có thể apply được toàn bộ
            firstNameTextField.layer.borderColor = UIColor.orange.cgColor
            firstNameTextField.layer.borderWidth = 1.0
            firstNameTextField.layer.cornerRadius = 25
            
            lastNameTextField.layer.borderColor = UIColor.orange.cgColor
            lastNameTextField.layer.borderWidth = 1.0
            lastNameTextField.layer.cornerRadius = 25
            
            accountTextField.layer.borderColor = UIColor.orange.cgColor
            accountTextField.layer.borderWidth = 1.0
            accountTextField.layer.cornerRadius = 25
        }
    }
    

    Preferred

    class SignUpViewController: UIViewController {
    
       @IBOutlet private weak var firstNameTextField: UITextField!
       @IBOutlet private weak var lastNameTextField: UITextField!
       @IBOutlet  weak var accountTextField: UITextField!
       
       override func viewDidLoad() {
           super.viewDidLoad()
           
           firstNameTextField.setupLayer()
           lastNameTextField.setupLayer()
           accountTextField.setupLayer()
       }
    }
    // Phần extension cho các compoment như UILabel, UITextField, UIButton, ... thường 
    // được tách ra thành các file riêng. Xem thêm phần Code Organization
    extension UITextField {
       func setupLayer(borderWidth: CGFloat = 1.0,
                       borderColor: CGColor = UIColor.orange.cgColor,
                       cornerRadius: CGFloat = 25) {
           layer.borderWidth = borderWidth
           layer.borderColor = borderColor
           layer.cornerRadius = cornerRadius
       }
    }

    Code Organization

    • Ta không nên gộp tất cả các property và các function vào trong một block code ở trong một file. Vì đối với những file có số lượng dòng code lớn, việc làm này sẽ khiến ta khó hình dung được cấu trúc tổ chức của file và mục đích sử dụng của các function trong file đó. Ta nên chia nhỏ file thành nhiều block code, mỗi block code giải quyết một nhiệm vụ khác nhau và chứa các function liên quan tới nhiệm vụ đó. Ta có thể thêm từ khoá // MARK: – dosomething vào trên mỗi block code

    Not Preferred

    // 1 block code không nên adopt quá nhiều protocol, delegate như vậy
    class HomeViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    
        @IBOutlet private weak var salaryTableView: UITableView!
        @IBOutlet private weak var avatarImageView: UIImageView!
        @IBOutlet private weak var widthConstraintAvatarImageView: NSLayoutConstraint!
        
        private var dataSalaryArray: [Salary] = [Salary(income: 123456, month: "1/2021"),
                                                Salary(income: 654321, month: "2/2021")]
        
        override func viewDidLoad() {
            super.viewDidLoad()
            salaryTableView.delegate = self
            salaryTableView.dataSource = self
            salaryTableView.register(UINib(nibName: "SalaryCell", bundle: nil), forCellReuseIdentifier: "SalaryCell")
        }
        
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return dataSalaryArray.count
        }
        
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            guard let cell: SalaryCell = salaryTableView.dequeueReusableCell(withIdentifier: "SalaryCell",
                                                                             for: indexPath) as? SalaryCell else {
                return UITableViewCell()
            }
            cell.setupData(data: dataSalaryArray[indexPath.row])
            return cell
        }
        
        func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            
        }
    
        func loadData() {
            // Do something
        }
    }
    

    Preferred

    class HomeViewController: UIViewController {
    
       // MARK: - Outlet
       @IBOutlet private weak var salaryTableView: UITableView!
       @IBOutlet private weak var avatarImageView: UIImageView!
       @IBOutlet private weak var widthConstraintAvatarImageView: NSLayoutConstraint!
       
       // MARK: - Property
       private var dataSalaryArray: [Salary] = [Salary(income: 123456, month: "1/2021"),
                                               Salary(income: 654321, month: "2/2021")]
       
       override func viewDidLoad() {
           super.viewDidLoad()
           salaryTableView.delegate = self
           salaryTableView.dataSource = self
           salaryTableView.register(UINib(nibName: "SalaryCell", bundle: nil), forCellReuseIdentifier: "SalaryCell")
       }
    }
    
    // MARK: - TableView DataSource
    extension HomeViewController: UITableViewDataSource {
       func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
           return dataSalaryArray.count
       }
       
       func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
           guard let cell: SalaryCell = salaryTableView.dequeueReusableCell(withIdentifier: "SalaryCell",
                                                                            for: indexPath) as? SalaryCell else {
               return UITableViewCell()
           }
           cell.setupData(data: dataSalaryArray[indexPath.row])
           return cell
       }
    }
    
    // MARK: - TableView Delegate
    extension HomeViewController: UITableViewDelegate {
       func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
           
       }
    }
    
    // MARK: - Do something
    extension HomeViewController {
       func loadData() {
           // Do something
       }
    }
    
    • Khi thêm từ khoá // MARK: – dosomething vào trước các block code, Xcode sẽ tự gen cho ta đường line phân cách giữa các block đó. Thêm vào đó, khi tap vào thanh công cụ như trên ảnh, ta cũng có thể overview được trong file đang có những block code nào, các block đảm nhận nhiệm vụ gì, các function liên quan đến nhiệm vụ đó. Việc này rất có ích khi file của bạn có số lượng dòng code lớn và có nhiều người cùng phát triển

    • Tương tự, ta cũng nên chia project thành nhiều các folder, mỗi folder có chức năng riêng và chứa các file liên quan đến chức năng đó.
    Preferred Not Preferred

    Scene Delegate

    • Khi tạo một project mới trong Xcode, hệ thống sẽ mặc định cho rằng app của bạn sẽ chỉ support cho version iOS cao nhất mà bản Xcode đó support trở lên (ví dụ: Xcode 11.3 sẽ support cho version iOS 13.6 trở lên, Xcode 12.0 sẽ support cho version iOS 14.0 trở lên, Xcode 12.5 là iOS 14.5 trở lên, …).
    • Từ Xcode 11.0 trở về trước, khi tạo mới 1 project, sẽ có 1 vài file mặc định được tạo như: ViewController.swift, Main.storyboard, AppDelegate.swift, Info.plist… Từ Xcode 11.0 trở lên, để phục vụ cho iOS 13, ngoài những file mặc định vừa liệt kê ở trên, còn có thêm 1 file nữa là SceneDelegate.swift (Tìm hiểu thêm tại đây).

    • Vì vậy, nếu muốn project có thể được build trên device (hoặc simulator) chạy version < iOS 13, ta sẽ phải tiến hành xoá file SceneDelegate.swift và thực hiện 1 vài config như sau.
    • Ngoại trừ trường hợp là bạn muốn làm việc với SwiftUI, việc xoá file SceneDelegate.swift và giảm target version build của app dường như là 1 việc bắt buộc bởi 1 vài lý do chính sau:
      • Khi phát triển 1 ứng dụng, chắc chắn chúng ta đều muốn đông đảo người dùng có thể tiếp cận được với ứng dụng đó. Nhưng không phải người dùng nào cũng sẵn sàng update version iOS mới nhất cho device của họ. Vì vậy việc để target vesion build của app ở mức “phổ thông” như iOS 11.0, 12.0, … sẽ làm tăng tính thương mại cho app của bạn.
      • Về mặt technical, khi build app trên những device “thấp” hoặc trên những iOS version thấp, ta sẽ có cơ hội để test nhiều hơn. Vì thực tế sẽ có rất nhiều bug rất “dị”, chúng chỉ xảy ra trên những “môi trường” thấp mà không xảy ra ở trên “môi trường” cao, hoặc cũng có nhiều trường hợp ngược lại. Đối với 1 developer, ta cần phải đảm bảo những dòng code của ta phải chạy ngon trên nhiều môi trường khác nhau.
      • Cũng về mặt technical, khi bạn muốn chia sẻ source code của mình và người khác muốn clone về. Trong trường hợp bạn dùng bản Xcode mới nhất còn họ dùng bản Xcode thấp hơn, nếu bạn không giảm target version build thì người khác sẽ không thể build source của bạn được.

    Computed Property

    • Để cho ngắn gọn, nếu 1 computed property thuộc kiểu read-only, ta có thể bỏ qua mệnh đề get. Mệnh đề này chỉ cần thêm vào khi có thêm cả mệnh đề set

    Preferred

    var diameter: Double {
      return radius * 2
    }
    

    Not Preferred

    var diameter: Double {
      get {
        return radius * 2
      }
    }
    

    Optional

    • Khi truy cập giá trị optional, nếu giá trị đó chỉ được truy cập 1 lần hoặc có nhiều optional trong chuỗi, ta có thể dùng 1 chuỗi optional liên tiếp
    textContainer?.textLabel?.setNeedsDisplay()
    
    • Trong trường hợp giá trị optional được truy cập nhiều lần, nên sử dụng if…let để mở ra 1 block code rồi thao tác trong đó
    if let textContainer = textContainer {
      // do many things with textContainer
    }
    
    • Khi đặt tên cho các biến và các property optinal, không cần thiết phải đặt kiểu như optionalNameLabel hay couldAvatarImageView vì trạng thái optional (?) đã có trong khi khai báo biến rồi.
    • Khi unwrapp biến optional, cũng không cần thiết phải đặt các tên như unwrappedView hay realLabel mà hãy dùng chính tên gốc của biến đó

    Preferred

    var subview: UIView?
    var volume: Double?
    
    // later on...
    if let subview = subview, let volume = volume {
      // do something with unwrapped subview and volume
    }
    
    // another example
    resource.request().onComplete { [weak self] response in
      guard let self = self else { return }
      let model = self.updateModel(response)
      self.updateUI(model)
    }
    

    Not Preferred

    var optionalSubview: UIView?
    var volume: Double?
    
    if let unwrappedSubview = optionalSubview {
      if let realVolume = volume {
        // do something with unwrappedSubview and realVolume
      }
    }
    
    // another example
    UIView.animate(withDuration: 2.0) { [weak self] in
      guard let strongSelf = self else { return }
      strongSelf.alpha = 1.0
    }
    
    • Nếu có nhiều biến optional được unwrapp với guard let hoặc if…let, hãy ghép chúng lại với nhau thành 1 câu lệnh để giảm thiếu việc lồng điều kiện. Khi ghép, hãy đặt guard trên 1 dòng riêng, đặt các điều kiện trên từng dòng riêng và thụt lề cho chúng, cuối cùng mệnh đề else được căn lề thẳng với guard

    Preferred

    guard 
      let number1 = number1,
      let number2 = number2,
      let number3 = number3 
    else {
      fatalError("impossible")
    }
    // do something with numbers
    

    Not Preferred

    if let number1 = number1 {
      if let number2 = number2 {
        if let number3 = number3 {
          // do something with numbers
        } else {
          fatalError("impossible")
        }
      } else {
        fatalError("impossible")
      }
    } else {
      fatalError("impossible")
    }
    
    
    

    Multi-line String

    • Khi muốn viết 1 văn bản dài nhiều dòng, nên sử dụng cú pháp Multi-line String Literal. Đó là mở văn trên 1 dòng rồi bắt đầu văn bản từ dòng thứ 2 trở đi, kết hợp với việc thụt lè và căn lề thằng các dòng tiêp theo

    Preferred

    let message: String = """
      You cannot charge the flux \
      capacitor with a 9V battery.
      You must use a super-charger \
      which costs 10 credits. You currently \
      have \(credits) credits available.
      """
    

    Not Preferred

    let message: String = """You cannot charge the flux \
      capacitor with a 9V battery.
      You must use a super-charger \
      which costs 10 credits. You currently \
      have \(credits) credits available.
      """
    

    Bonus

    • Không giống như 1 số ngôn ngữ khác (C/C++, JavaScript, …), Swift không yêu cầu phải có dấu chấm phảy ở cuối mỗi dòng code

    Preferred

    let swift = "not a scripting language"
    

    Not Preferred

    let swift = "not a scripting language";
    
    • Dấu ngoặc đơn bao quanh các điều kiện là không bắt buộc và nên được bỏ qua

    Preferred

    if name == "Hello" {
      print("World")
    }
    

    Not Preferred

    if (name == "Hello") {
      print("World")
    }
    
    • Tuy nhiên với những biểu thức phức tạp, có dấu ngoặc đơn bao quanh sẽ làm code trở nên clear hơn

    Preferred

    let playerMark: String = (player == current) ? "X" : "O"

    • Ngoài ra, còn 1 lỗi nữa mà người viết thấy đa phần các bạn newbie rất hay mắc phải. Tuy rằng lỗi này liên quan đến logic nhưng cũng xin liệt kê vào đây để anh em newbie tiện theo dõi. Đó là khi ta muốn truy xuất các phần tử trong 1 collection data (set, dictionary, array, … và ở đây xin lấy ví dụ là array), việc đầu tiên ta cần làm là phải check xem array đó có phần tử hay không rồi mới tiến hành việc truy xuất. Dưới đây là 1 ví dụ:

    Preferred

    private func getDataFromPlistFile(_ name: String) -> Dictionary<String, AnyObject>? {
    if PlistManager().getPlist(withName: name)?.count == 0 {
    return nil
    }
    let data = PlistManager().getPlist(withName: name)?[0]
    return data
    }

    Chúng ta có thể viết clear hơn như sau (dĩ nhiên đây là cách được recommend):

    Preferred

    private func getDataFromPlistFile(_ name: String) -> Dictionary<String, AnyObject>? {
    if let data = PlistManager().getPlist(withName: name)?.first {
    return data
    }
    }

    Tuyệt đối không được truy xuất trực tiếp vào các phần tử của array khi chưa check điều kiện, vì ta không thể chắc chắn được là array đó luôn có phần tử hay là không. Điều này sẽ tiềm ẩn nguy cơ bug rất cao.

    Not Preferred

    private func getDataFromPlistFile(_ name: String) -> Dictionary<String, AnyObject>? {
    let data = PlistManager().getPlist(withName: name)?[0]
    return data
    }

    Reference

  • 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.
  • 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>