Tag: #Swift #iOS #Apple

  • Học lập trình với ngôn ngữ Swift – Bài 1: Chào mừng bạn đến với Swift

    Học lập trình với ngôn ngữ Swift – Bài 1: Chào mừng bạn đến với Swift

    Trước khi bắt đầu thực hiện những dòng code đầu tiên cho ứng dụng của bạn bằng ngôn ngữ lập trình Swift, chúng ta sẽ tìm hiểu qua về nó để biết rằng tại sao chúng ta lại chọn Swift để làm các ứng dụng, nó có những ưu điểm gì? lịch sử hình thành như nào? Xu hướng phát triển ra sao? Liệu nó có đáng để chúng ta tìm hiểu và học hay không?. Vậy các bạn cùng mình tiếp tục theo dõi bài viết này nhé.

    Định nghĩa về Swift

    Swift là một ngôn ngữ lập trình hướng đối tượng dành cho việc phát triển iOS và macOSwatchOStvOS và z/OS. được giới thiệu bởi Apple tại hội nghị WWDC 2014.Swift được mong đợi sẽ tồn tại song song cùng Objective-C, ngôn ngữ lập trình hiện tại dành cho các hệ điều hành của Apple. Swift được thiết kế để hoạt động với các framework Cocoa và Cocoa Touch của Apple và phần lớn mã Objective-C hiện có được viết cho các sản phẩm của Apple. Nó được biên dịch với trình biên dịch LLVM và đã được đưa vào Xcode kể từ phiên bản 6, phát hành năm 2014. Trên các nền tảng của Apple[12], nó sử dụng thư viện runtime Objective-C cho phép mã CObjective-CC++ và Swift cùng chạy trong một chương trình.[13]

    Apple dự định Swift hỗ trợ nhiều khái niệm cốt lõi liên quan đến Objective-C, đáng chú ý là thu hồi động, các ràng buộc phổ thông, lập trình mở rộng và các tính năng tương tự, nhưng theo cách “an toàn hơn”, giúp dễ dàng bắt lỗi phần mềm hơn; Swift có các tính năng giải quyết một số lỗi lập trình phổ biến như con trỏ rỗng cung cấp cú pháp đặc biệt để giúp tránh kim tự tháp diệt vong. Swift hỗ trợ khái niệm về khả năng mở rộng giao thức, một hệ thống mở rộng có thể được áp dụng cho các kiểu, cấu trúc và lớp, mà Apple khuyến khích như một sự thay đổi thực sự trong mô hình lập trình mà họ gọi là “lập trình hướng giao thức” (tương tự như đặc điểm).

    Swift được giới thiệu tại Worldwide Developers Conference (WWDC) 2014 của Apple.Nó đã trải qua quá trình nâng cấp lên phiên bản 1.2 trong năm 2014 và nâng cấp lớn hơn cho Swift 2 tại WWDC 2015. Ban đầu, ngôn ngữ độc quyền, phiên bản 2.2 được được chuyển sang phần mềm nguồn mở theo Giấy phép Apache 2.0 vào ngày 3 tháng 12 năm 2015, dành cho các nền tảng của Apple và Linux.[17][18]

    Thông qua phiên bản 3.0, cú pháp của Swift đã trải qua quá trình phát triển quan trọng, với nhóm nòng cốt làm cho sự ổn định nguồn trở thành trọng tâm trong các phiên bản sau.[19][20] Trong quý đầu tiên của năm 2018, Swift đã vượt qua Objective-C về mức độ phổ biến.[21]

    Swift 4.0, được phát hành vào năm 2017, đã giới thiệu một số thay đổi đối với một số lớp và cấu trúc tích hợp. Mã được viết bằng các phiên bản trước của Swift có thể được cập nhật bằng chức năng di chuyển được tích hợp trong Xcode

    Vào tháng 3 năm 2017, chưa đầy 3 năm sau khi chính thức ra mắt, Swift đã đứng đầu trong bảng xếp hạng TIOBE hàng tháng về các ngôn ngữ lập trình phổ biến nhất.[22] Một tài liệu 500 trang về Swift cũng được phát hành tại WWDC, miễn phí trên iBooks Store.

    Nguồn: WIKI, bạn có thể đọc thêm về lịch sử hình thành và phát triển và các thông tin khác ở WIKI

    Giới thiệu về Swift

    Apple định nghĩa về ngôn ngữ lập trình Swift là một cách tuyệt vời để viết phần mềm, cho dù đó là phần mềm trên điện thoại, máy tính, server hay bất kể thứ gì khác chạy code. Nó là ngôn ngữ lập trình an toàn, nhanh chóng và có tính tương tác, kết hợp với tư duy ngôn ngữ hiện đại tốt nhất với sự không ngoan từ văn hoá kỹ thuật rộng lớn của Apple và những đóng góp đa dạng từ cộng đồng mã nguồn mở. Trình biên dịch thì tối ưu hoá cho hiệu suất và ngôn ngữ được tối ưu hoá cho sự phát triển mà không ảnh hưởng tới cả hai.

    Swift rất thân thiện với các lập trình viên mới, nó là ngôn ngữ lập trình chất lượng công nghiệp, biểu cảm và thú vị như ngôn ngữ kịch bản. Viết code Swift trong Playground cho phép bạn thử nghiệm code và xem kết quả ngay lập tức mà không tốn chi phí để xây dụng ứng dụng.

    Swift xác định loại bỏ các lớp lớn các lỗi lập trình phổ biến bằng cách áp dụng các mẫu lập trình hiện đại:

    • Các biến luôn được khởi tạo trước khi sử dụng.
    • Các chỉ số mảng được kiểm tra lỗi ngoài giới hạn(out-of-bounds).
    • Số nguyên được kiểm tra tràn(overflow).
    • Optionals đảm bảo rằng các giá trị nil được xử lý rõ ràng.
    • Bộ nhớ được quản lý tự động.
    • Xử lý lỗi cho phép phục hồi có kiểm soát từ các lỗi không mong muốn.

    Mã Swift được biên dịch và tối ưu hóa để tận dụng tối đa phần cứng hiện đại. Cú pháp và thư viện chuẩn đã được thiết kế dựa trên nguyên tắc hướng dẫn rằng cách rõ ràng để viết mã của bạn cũng sẽ hoạt động tốt nhất. Sự kết hợp giữa an toàn và tốc độ khiến Swift trở thành lựa chọn tuyệt vời cho mọi thứ từ “Xin chào, thế giới!” cho toàn bộ hệ điều hành.

    Swift kết hợp suy luận kiểu mạnh mẽ và so khớp mẫu với cú pháp nhẹ, hiện đại, cho phép các ý tưởng phức tạp được thể hiện một cách rõ ràng và ngắn gọn. Kết quả là mã không chỉ dễ viết hơn mà còn dễ đọc và dễ bảo trì hơn.

    Swift đã được phát triển trong nhiều năm và nó tiếp tục phát triển với các tính năng và khả năng mới. Mục tiêu của Apple dành cho Swift rất tham vọng. Apple rất muốn xem bạn tạo thật nhiều ứng dụng với Swift.

    Khả năng tương thích phiên bản

    Phần này mô tả Swift 5.8, phiên bản mặc định của Swift được bao gồm trong Xcode 14. Bạn có thể sử dụng Xcode 14 để xây dựng các target được viết bằng Swift 5.8, Swift 4.2 hoặc Swift 4.

    Khi bạn sử dụng Xcode 14 để xây dựng code Swift 4 và Swift 4.2, hầu hết các chức năng của Swift 5.8 đều khả dụng. Điều đó nói rằng, những thay đổi sau chỉ có sẵn cho code sử dụng Swift 5.8 trở lên:

    Các hàm trả về loại không trong suốt yêu cầu thời gian chạy Swift 5.1.

    Sự cố gắng? biểu thức không đưa ra mức tùy chọn bổ sung cho các biểu thức đã trả về các tùy chọn.

    Biểu thức khởi tạo số nguyên lớn được suy ra là kiểu số nguyên chính xác. Ví dụ: UInt64(0xffff_ffff_ffff_ffff) đánh giá giá trị chính xác thay vì tràn.

    Đồng thời yêu cầu Swift 5.8 hoặc version cao hơn và một phiên bản của thư viện tiêu chuẩn Swift cung cấp các loại đồng thời tương ứng. Trên các nền tảng của Apple, hãy đặt target triển khai ít nhất là iOS 13, macOS 10.15, tvOS 13 hoặc watchOS 6.

    Targer được viết bằng Swift 5.8 có thể phụ thuộc vào Target được viết bằng Swift 4.2 hoặc Swift 4 và ngược lại. Điều này có nghĩa là nếu bạn có một dự án lớn được chia thành nhiều Framework, bạn có thể di chuyển code của mình từ Swift 4 sang Swift 5.8 bằng một Framework.

    Tổng kết

    Vậy là chúng ta đã đã biết qua hầu hết các thông tin về Swift, bài tiếp theo mình sẽ chia sẻ với các bạn cách làm quen với công cụ XCode và thực hiện những dòng code đầu tiên trên ngôn ngữ lập trình Swift.

  • Highlight text of UILabel in Swift

    Highlight text of UILabel in Swift

    Hi mọi người, mấy năm trước mình có làm một dự án mobile về mảng Logistics, trong ứng dụng thường sử dụng khá nhiều các text, một số từ được Highlight text đi kèm với các action tương tự như là một button nhằm mục đích gây sự chú ý với người dùng. Để làm được việc này chúng ta cần xác định được vị trí text cần Highlight để thực hiện thay đổi UI cho đoạn text đó gắn action cho nó. Mình thấy đây là một tính năng khá thú vị và sẽ gặp nhiều trong các dự án sắp tới của các bạn. Vậy nên mình xin chia sẻ cách làm của mình như sau.

    Cách Highlight text

    Về lý thuyết để highlight text trong swift thì chúng ta cần l xác định vị trí(position) và độ dài(lenght) của text cần highlight sau đó thực hiện thay đổi thuộc tính của đoạn text đó bằng NSAttributedString.

    Việc bắt sư kiện khi chúng ta tương tác với Highlight text cũng phải tìm vị trí và độ dài của đoạn text, sau đó chúng ta dựa vào điểm người dùng bấm vào trên Label để xác định xem người dùng có bấm đúng vị trí text được highlight hay không.

    1. Cách thực hiện Highlight text

    Đầu tiên chúng ta sẽ tạo một func trong extension của UILabel như sau:

    extension UILabel {
        func highlightText(_ text: String, highlightColor: UIColor, in mainText: String) {
            // chuyển mainText sang NSMutableAttributedString để xử lý tìm range của highlight text
            let highlightAttributedString = NSMutableAttributedString(string: mainText)
            // xác định vị trí, độ dài của text cần highlight
            let range = (mainText as NSString).range(of: text)
            // thêm thuộc tính cho đoạn text cần highlight
            highlightAttributedString.addAttribute(NSAttributedString.Key.foregroundColor, value: highlightColor, range: range)
            // gán giá trị vào label
            self.attributedText = highlightAttributedString
        }
    }

    Khi sử dụng chúng ta sẽ làm như sau:

    Trường hợp này mình sẽ biến chữ “DaoNM2” trong “Good evening! DaoNM2” thành màu cam như sau

    lbInfo.highlightText("DaoNM2", highlightColor: .red, in: "Good evening! DaoNM2")
    Kết quả

    2. Bắt sự kiện và thực hiện hành động khi bấm vào text được Highlight

    Ở mục 1 mình đã hướng dẫn cách thực hiện đổi màu text của 1 đoạn text trong UILabel, vậy để bắt sự kiện khi chúng ta bấm vào thì làm cách nào?

    Đầu tiên chúng ta sẽ tạo một func trong extension của UITapGestureRecognizer, nhằm mục đích xử lí điểm chạm của người dùng và xác định xem có đúng vị trí của text đã được Highlight hay không như sau:

    extension UITapGestureRecognizer {
        func didTapHighlightedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
            guard let attributedText = label.attributedText else {
                return false
            }
    
            let mutableStr = NSMutableAttributedString(attributedString: attributedText)
            mutableStr.addAttributes([NSAttributedString.Key.font: label.font!], range: NSRange(location: 0, length: attributedText.length))
               
            // If the label have text alignment. Delete this code if label have a default (left) aligment. Possible to add the attribute in previous adding.
            let paragraphStyle = NSMutableParagraphStyle()
            paragraphStyle.alignment = label.textAlignment
            mutableStr.addAttributes([NSAttributedString.Key.paragraphStyle: paragraphStyle], range: NSRange(location: 0, length: attributedText.length))
    
            // Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
            let layoutManager = NSLayoutManager()
            let textContainer = NSTextContainer(size: CGSize.zero)
            let textStorage = NSTextStorage(attributedString: mutableStr)
               
            // Configure layoutManager and textStorage
            layoutManager.addTextContainer(textContainer)
            textStorage.addLayoutManager(layoutManager)
               
            // Configure textContainer
            textContainer.lineFragmentPadding = 0.0
            textContainer.lineBreakMode = label.lineBreakMode
            textContainer.maximumNumberOfLines = label.numberOfLines
            let labelSize = label.bounds.size
            textContainer.size = labelSize
               
            // Find the tapped character location and compare it to the specified range
            let locationOfTouchInLabel = self.location(in: label)
            let textBoundingBox = layoutManager.usedRect(for: textContainer)
            let textContainerOffset = CGPoint(x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
                                              y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y)
            let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x,
                                                         y: locationOfTouchInLabel.y - textContainerOffset.y)
            let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
            return NSLocationInRange(indexOfCharacter, targetRange)
        }
    }

    Trong hàm này mình sẽ dùng NSLayoutManager để xác định xem điểm chạm của người dùng mà UITapGestureRecognizer bắt được có đúng vị trí highlight text hay không.

    func này sẽ trả về cho chúng ta biết được vị trí người dùng đang chạm vào UILabel có trùng với range truyền vào hay không.

    Để ứng dụng nó vào bài toán của chúng ta thì chúng ta sẽ thực hiện nó như sau:

        private func setUpLabelInfo() {
            lbInfo.highlightText("More...", highlightColor: .red, in: "Two one-offs, a roadster and a coupé, mark the end of production of super sports cars powered by the V12 combustion engine in the lead-up to the hybrid era. More...")
            let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapMore(_:)))
            lbInfo.isUserInteractionEnabled = true
            lbInfo.addGestureRecognizer(tapGesture)
        }
        
        @objc
        private func tapMore(_ gesture: UITapGestureRecognizer) {
            let mainText = lbInfo.text ?? ""
            let highlightText = "More..."
            let highlightTextRange = ((mainText) as NSString).range(of: highlightText)
            if gesture.didTapHighlightedTextInLabel(label: lbInfo, inRange: highlightTextRange) {
                print("Did tap: \(highlightText), hightlightRange: \(highlightTextRange)")
            }
        }

    Ở viewDidLoad chúng ta chỉ cần gọi nó ra là xong.

       override func viewDidLoad() {
            super.viewDidLoad()
    
            lbTitle.textColor = .orange
            lbTitle.textAlignment = .center
            lbTitle.text = "Lambo"
            setUpLabelInfo()
        }

    Kết quả thu được như sau:

    Vậy là chúng ta đã có thể thêm action vào cho text được highlight.

    Từ ví dụ này chúng ta có thể tuỳ biến cho phù hợp dự án của bạn hoặc phát triển nó lên theo cách mà các bạn mong muốn.

    Mình hi vọng bài viết giúp cho các bạn có thêm phương án lựa chọn khi tham gia những dự án có yêu cầu tương tự. Chúc các bạn thành công!

  • Hướng dẫn cách Gen Document Design bằng code Python

    Hướng dẫn cách Gen Document Design bằng code Python

    Hi các bạn, có bao giờ các bạn gặp phải tình huống khi kết thúc dự án thì phải làm Document design cho dự án của mình? Yêu cầu phải liệt kê hết các func trong project ra file excel và giải thích nó làm gì. Nếu câu trả lời là có thì các bạn có thể đọc tiếp bài viết này để xem cách thực hiện nó như thế nào nhé.

    Yêu cầu

    • Cần liệt kê hết tất cả các func trong souce code và giải thích func đó dùng để làm gì
    • Định dạng yêu cầu theo file excel

    Chuẩn bị

    Do tool được code trên nền tảng python nên mọi người cần cài đặt Python3 trước để có thể thực hiện gen document. Để cài đặt thì mọi người có thể làm theo hướng dẫn sau:

    1. Homebrew

    Link tham khảo: https://brew.sh v/bin/bash -c “$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)”

    2. Install Python 3 on Mac – Brew Install

    Link tham khảo: https://www.freecodecamp.org/news/python-version-on-mac-update/ vbrew install pyenv // Install vpyenv install 3.9.2 // Update version

    3. Install pip: pip la một package của Python

    Link tham khảo: https://pip.pypa.io/en/stable/

    Download get-pip.py provided by https://pip.pypa.io hoặc sử dung terminal: vcurl https://bootstrap.pypa.io/get-pip.py -o get-pip.py vsudo python3 get-pip.py

    4. Openpyxl install: Cài đặt openpyxl sử dụng pip.

    Link tham khảo: https://openpyxl.readthedocs.io/en/stable/ vsudo pip install openpyxl

    5. Tải sẵn file template ở đây:

    File design là file excel template
    File DocC+.py là file code python nhằm mục đích gen gen souce và update file excel.

    NOTE: Đây là code dùng để gen dự án sử dụng ngôn ngữ lập trình swift, nếu các bạn cần gen trên ngôn ngữ khác chúng ta sẽ cần thực hiện chỉnh sửa file DocC+.py cho phù hợp với ngôn ngữ mà bạn sử dụng.

    Hướng dẫn sử dụng

    Bước 1: Copy file DocC+.py và design.xlsx vào trong thư mục cần gen.

    Bước 2: Bật Terminal -> navigate đến project folders chữa file DocC+.py

    Bước 3: Chạy câu lệnh dưới đây: python3 DocC+.py

    Vậy là chúng ta đã gen xong file liệt kê tất cả các func có trong thư mục mà các bạn chọn. Rất nhanh gọn và tiện lợi đúng không?

    Ưu điểm

    • Tiết kiệm rất nhiều thời gian thực hiện, thay vì phải copy bằng tay mất rất nhiều thời gian và khiến người thực hiện khá stress thì tool giúp chúng ta làm nó trong vài phút
    • Có thể sử dụng cho tất cả các ngôn ngữ lập trình
    • Quá dễ sử dụng

    Tổng kết

    Vậy là bài viết trên mình đã giới thiệu, chia sẻ và hướng dẫn các bạn sử dụng một tool cực kì hữu dụng và dễ sử dụng. Mình hi vọng nó sẽ giúp các bạn giải quyết bài toán mà bạn gặp phải. Chúc các bạn thành công!

  • Hướng dẫn cách sử dụng Lottie Animation trong Swift

    Hướng dẫn cách sử dụng Lottie Animation trong Swift

    Có lẽ tất cả mọi người trên thế giới đều yêu thích cái đẹp và mình cũng không ngoại lệ. Việc xây dựng những ứng dụng có UI đẹp mắt, animation mượt mà và thú vị khiến hàng triệu người dùng trầm trồ là mong muốn của rất nhiều những Developer trên toàn thế giới. Là một developer nếu bạn có tìm thấy bài viết này của mình thì chắc hẳn bạn cũng có khát khao làm những điều thú vị cho ứng dụng của mình có đúng không? Vậy chúng ta cùng đi tiếp vào bài viết này để làm cách nào để có thể làm ứng dụng của các bạn trở nên thú vị hơn nhé.

    Trong một dịp mình có cơ hội làm việc trong một dự án có sử dụng rất nhiều Animation để làm cho ứng dụng trở nên đẹp và thú vị hơn với người dùng. Lúc mới vào dự án khi sử dụng một số tính năng có sử dụng animation làm mình thấy rất hứng thú, thật khó để cưỡng lại sự đẹp đẽ và hoa mỹ của nó. Trong khi làm việc trong dự án thì mình cũng đã phát hiện ra một thư viện hỗ trợ cho việc thực hiện animation rất hay đó chính là Lottie. Vì vậy ngày hôm nay mình muốn chia sẻ với các bạn về thư viện này và cách thêm nó vào ứng dụng của các bạn.

    Lottie là gì?

    Lottie là định dạng tệp hoạt hình dựa trên JSON cho phép các nhà thiết kế gửi animation trên bất kỳ nền tảng nào dễ dàng như vận chuyển nội dung tĩnh. Bạn có thể đọc thêm về nó ở đây.

    Lottie được sử dụng khi nào?

    Lottie khá là linh hoạt và nó có thể sử dụng được trên nhiều nền tảng khác nhau từ iOS, Android, Web …. Vì vậy nó giúp đồng bộ animation trên tất cả các nền tảng mà không xảy ra sai sót nào.

    Lottie hỗ trợ làm animation rất tốt, nó có thể làm được những animation rất phức tạp mà việc code animation không thể làm được hoặc làm đơn giản hoá việc thêm animation vào trong ứng dụng.

    Hiện nay nó đang được sử dụng khá phổ biến trên các ứng dụng.

    Làm sao để sử dụng Lottie cho ứng dụng của bạn?

    Bước 1: Tạo dự án mới

    Nếu bạn muốn thêm vào dự án có sẵn của mình thì bỏ qua bước này nhé.

    Đầu tiên chúng ta mở Xcode lên và tạo một ứng dụng demo để có thể test Lottie chạy trên ứng dụng một cách dễ dàng hơn.

    Tạo ứng dụng mới

    Bước 2: Thêm thư viện Lottie vào ứng dụng

    Để thêm thư viện Lottie vào ứng dụng chúng ta có thể sử dụng bằng nhiều cách khác nhau như Cocoa Pods, Carthage hoặc Swift Package Manager. Nếu bạn chưa biết cách thêm thư viện Lottie vào dự án thì bạn có thể tham khảo hướng dẫn tại đây.

    Ở bài viết này mình sẽ hướng dẫn các bạn sử dụng Swift Package Manager để thêm vào ứng dụng như sau:

    Bạn mở dự án của bạn trên Xcode, chọn File -> Add Packages… Trên đầu bên phải của popup hiện ra có công cụ tìm kiếm các bạn đánh link: https://github.com/airbnb/lottie-ios.git để tìm thư viện. Sau đó bấm vào Add Package và chờ một lúc để Xcode tải thư viện bạn, sau khi tải xong bạn bấm Add để hoàn tất

    Bước 3: Thêm Lottie vào dự án

    Đầu tiên chúng ta cần chuẩn bị file Lottie JSON, nếu bạn chưa có thì bạn có thể download nó tại đây. Bạn nhớ tải file JSON nhé.

    Bạn cũng có thể sử dụng ứng dụng Adobe After Effect để tạo file Lottie của riêng mình.

    Khi đã có file lottie JSON rồi chúng ta sẽ thực hiện thêm file vào trong dự án bằng cách kéo thả nó trực tiếp vào thư mục trong Xcode của bạn, tại nơi mà bạn muốn lưu nó. Thông thường chúng ta tạo mới thư mục Resouces/LottieJSON và lưu nó trong đó.

    Add file lottie vào dự án

    Hãy nhớ bạn tích Copy Items if needed và các mục như trên hình nhé.

    Thêm Lottie bằng cách sử dụng code

    Đầu tiên để sử dụng được thư viện Lottie hãy nhớ import thư viện Lottie vào màn hình mà bạn sử dụng

    import Lottie

    Tiếp theo chúng ta tạo func addLottieAnimation() để thực hiện nhiệm vụ add lottie vào view hiện tại. Bạn cũng có thể custom một func riêng để handle xử lí triệt để các logic của Lottie animation. Trong bài này mình chỉ hướng dẫn cơ bản để các bạn có thể thêm Lottie vào ứng dụng của mình.

        func addLottieAnimation() {
            // 1. Create lottie animation view
            let lottieView: LottieAnimationView = LottieAnimationView(name: "404-Notfound")
            lottieView.frame = view.bounds
            
            // 2. Set animation content mode
            lottieView.contentMode = .scaleAspectFit
            
            // 3. Set animation loop mode
            lottieView.loopMode = .loop
            
            // 4. Adjust animation speed
            lottieView.animationSpeed = 0.5
            
            view.addSubview(lottieView)
            
            // 5. Play animation
            lottieView.play()
        }

    Sau đó bạn call func này ở viewDidLoad thì sẽ nhận được kết quả như sau:

    Thêm Lottie bằng cách sử dụng Builder Interface

    Đầu tiên bạn mở file giao diện của bạn lên băng Interface Builder, kéo một UIView và thực hiện constraint cho nó.

    Thêm UIview vào để sử dụng Lottie

    Sau đó ở trên cùng của tab bên phải bạn chọn Identity Inspector và thay đổi giá trị như hình dưới

    Thay đổi class sang LottieAnimationView

    Tiếp tục chuyển sang tab Attributes Inspector để điền tên file Lottie mà bạn muốn

    Tạo một liên kết giữa view của bạn với file controller để sử dụng, sau đó bạn thực hiện code như dưới đây để Lottie có thể hoạn động.

    import UIKit
    import Lottie
    
    class ViewController: UIViewController {
        @IBOutlet weak var animationLottieView: LottieAnimationView!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
            // 1. Set animation content mode
            animationLottieView.contentMode = .scaleAspectFit
    
            // 2. Set animation loop mode
    
            animationLottieView.loopMode = .loop
    
            // 3. Adjust animation speed
    
            animationLottieView.animationSpeed = 0.5
    
            // 4. Play animation
            animationLottieView.play()
        }
    }

    Chạy ứng dụng và bạn sẽ nhận được kết quả như mong đợi!

    Vậy là mình đã giới thiệu và hướng dẫn cho các bạn một thư viện khá hiệu quả cho các bạn sử dụng để bổ trợ cho các bạn việc thực hiện animation tốt hơn, dễ dàng hơn, và nhanh hơn so với cách code truyền thống. Từ giờ các bạn có thể tha hồ sáng tạo trên những ứng dụng sắp tới của bản thân và khiến mọi người sử dụng thích thú.

    Mình hi vọng nó sẽ giúp các bạn làm ra những ứng dụng có trải nghiệm tuyệt vời, giúp những người sử dụng ứng dụng của các bạn phải trầm trồ khi sử dụng nó. Chúc các bạn thành công với những dự án sắp tới của mình.

  • Architecture pattern: Clean Swift

    Architecture pattern: Clean Swift

    Xin chào mọi người! Lại là DaoNM2 đây! Tiếp tục với series về Architecture pattern hôm nay mình sẽ giới thiệu cho các bạn một kiến trúc khá mới so với các mẫu kiến trúc hiện tại đó là Clean Swift (VIP). Trước đây mình đã có cơ hội tiếp cận với kiến trúc này, khi tham gia vào việc xây dựng một ứng dụng rất lớn cho một công ti rất nổi tiếng về ô tô xe máy ở Việt Nam. Khi đó mình cũng đã tích luỹ được một số kinh nghiệm về kiến trúc này, vì vậy mình muốn chia sẻ với các bạn một số thông tin cũng như kinh nghiệm mà mình đã tích luỹ được khi làm việc với mẫu kiến trúc này.

    Đầu tiên nếu bạn là người mới hoặc bạn mới tìm hiều về các mẫu kiến trúc trong lập trình bao giờ đây là bài đầu tiên bạn đọc, thì để có thể hiểu kiến trúc này tốt hơn thì bạn có thể tham khảo các bài viết về các mẫu thiết kế trước khi tiếp tục ở link dưới đây:

    Bối cảnh hình thành

    Clean swift lần đầu tiên được giới thiệu bởi Raymond Law trên website clean-swift.com của anh ấy. Ý tưởng hình thành mẫu kiến trúc này là do anh ấy đã quá chán với các vấn đề của MVC(một mẫu kiến trúc mà Apple khuyên dùng) vì vậy anh ấy đã nghĩ ra Clean Swift để giải quyết các vấn đề mà các mẫu kiến trúc trước đây chưa làm được. Clean Swift được Raymond Law xây dựng dựa trên Clean Architecture của Uncle Bob.

    Clean Swift là gì?

    Clean Swift là một mẫu kiến trúc xây dựng dựa trên Clean Architecture của Uncle Bob để áp dụng cho việc xây dựng các ứng dụng iOS và MacOS.

    Trong Clean Swift Architecture pattern thì tất cả logic của ứng dụng sẽ được chia đều ra 3 thành phần chính của nó là View controller, Interactor và Presenter. Mỗi phần sẽ đảm nhiệm một số logic cụ thể, và chúng liên lạc với nhau bằng các liên kết 1 chiều, vì vậy source code của bạn sẽ luôn luôn đi theo một chiều chứ không đa chiều như các architecture pattern khác.

    Khi ứng dụng Clean Swift vào trong dự án của bạn, nó sẽ được cấu trúc theo từng màn hình của ứng dụng(scenes).

    Các thành phần của Clean Swift

    Mẫu kiến trúc Clean Swift được cấu tạo bởi các phần như sau:

    • View
    • View Controller
    • Router
    • Presenter
    • Interactor
    • Worker(optional)
    • Model(optional)
    Clean Swift architecture pattern

    Clean Swift gồm 3 phần chính là ViewController, Presenter và Interactor. 3 phần này có liên kết 1 chiều và tạo thành 1 vòng tròn. Khi View controller nhận được request nó sẽ gọi sang Interactor để nó xử lí logic, khi xong logic Interactor sẽ gửi dữ liệu sang bên Presenter để nó thực hiện format lại dữ liệu rồi trả về cho ViewController làm nhiệm vụ update lên View cho người dùng. Các thành phần này sẽ được kết nối với nhau bằng protocol.

    View

    Là các thành phần nằm trong UIKit hoặc bất kể thứ gì liên quan tới UI ví dụ như: storyboard, xib, UIView, UIControl …

    View Controller

    Định nghĩa các màn hình(scenes), nó có thể chứa 1 hoặc nhiều View

    Nó sẽ giữ các instances của Interactor và Router

    Là nơi nhận các tương tác của người dùng và gọi đến Interactor hoặc Router để xử lý, nó cũng nhận output của Presenter làm input và truyền nó lên view để hiển thị cho người dùng.

    Interactor

    Chứa các business logic của màn hình

    Giữ instance của Presenter và các Workers(nếu có)

    Nhận thông tin input từ ViewController và xử lý hoặc yêu cầu Worker làm việc để truyền kết quả sang cho Presenter

    Interactor sẽ không được import UIKit để đảm bảo source không có liên kết trực tiếp với View

    Presenter

    Giữ một tham chiếu yếu đến View Controller để truyền dữ liệu sang View Controller

    Là nơi xử lý logic hiển thị, khi nhận được input từ Interactor nó sẽ thực hiện format lại dữ liệu và truyền sang cho ViewController để nó hiển thị thông tin cho người dùng.

    Worker

    Là nơi được coi là trung tâm dữ liệu, nó sẽ thực hiện các nhiệm vụ liên quan tới việc lấy dữ liệu từ API hoặc LocalDB

    Là thành phần phụ nên ở các màn hình đơn giản không có tương tác với dữ liệu chúng ta có thể bỏ qua worker

    Router

    Nó giữ một tham chiếu yếu tới View Controller, nhằm mục đích tránh việc tham chiếu lẫn nhau(retain cycles) dẫn đến không thể release các đối tượng này khi nó không còn được sử dụng -> Lack memory.

    Router sinh ra để giảm tải công việc cho View Controller nó sẽ làm nhiệm vụ điều hướng trong ứng dụng.

    Model

    Nó là nơi định nghĩa các đối tượng cho ứng dụng, nó chỉ làm nhiệm vụ định nghĩa các đối tượng và không có xử lí logic hay liên kết trực tiếp với các thành phần khác của kiến trúc.

    Các đối tượng trong model sẽ được khai báo theo value type(struct, enum)

    Tương tự như Worker, một số màn hình đơn giản không tương tác với dữ liệu sẽ không cần đến Model

    Ưu điểm

    • Dễ maintain, fixbugs vì liên kết 1 chiều giữa các thành phần
    • Hỗ trợ viết Unit test một cách dễ dàng
    • Viết các phương thức ngắn hơn với trách nhiệm duy nhất
    • Tách được business logic sang cho Interactor xử lí
    • Có thể tái sử dụng các Workers và Services
    • Có thể áp dụng được cho các dự án lớn để giảm tình trạng conflict khi merge source.

    Nhược điểm

    • Quá nhiều các protocol với các nhiệm vụ riêng biệt, làm cho việc đặt tên trở nên khó khăn và không cẩn thận sẽ gây khó hiểu cho người đọc
    • Kích thước ứng dụng lớn do chứa nhiều protocol và nhiều file
    • Cần thời gian để cho các thành viên dự án có thể hiểu và tuân theo

    Tổng kết

    Clean swift là một mẫu kiến trúc không phổ biến như các mẫu khác, tuy nhiên ưu điểm của nó mang lại là rất lớn. Để vận hành các dự án lớn có yêu cầu viết Unitest chúng ta có thể coi Clean Swift là một trong những ứng cử viên sáng giá. Mình hi vọng bài viết có thể giúp các bạn có thêm kiến thức về mẫu kiến trúc Clean Swift và giúp các bạn có thể chọn được mẫu kiến trúc ưng ý cho những dự án sắp tới.

    Nếu các bạn muốn biết thêm nhiều thông tin hơn về Clean Swift Architecture Pattern thì các bạn có thể tham khảo tại link sau: Clean Swift

  • AVFoundation trên Swift và ứng dụng để xây dựng tính năng QR Scan

    AVFoundation trên Swift và ứng dụng để xây dựng tính năng QR Scan

    Xin chào tất cả các bạn

    Thời gian gần đây, như các bạn có thể thấy cứ bước chân ra khỏi nhà là thấy đâu đâu cũng có những ô mã QR. Từ việc đi đá bát phở cũng quét thanh toán QR, đăng nhập zalo hay telegram cũng có thể quét QR, hay thậm chí trà đá vỉa hè cũng có QR luôn…. Điều đó chứng tỏ mã QR đang dần được ứng dụng rất rộng rãi vào tất cả các lĩnh vực trong cuộc sống, thay thế cho giấy tờ truyền thống cũng như giúp cuộc sống trở nên tiện lợi hơn.

    Tuy nhiên nếu chỉ có mỗi mã QR thì cũng không giúp ích được gì khi mà không có những thiết bị đọc và giải mã những chiếc QR vi diệu này. Và những thiết bị để đọc mã QR cũng chẳng đâu xa ngay chính trên chiếc smart phone mà lúc nào cũng theo các bạn 24/7. Việc tích hợp QR Scan vào các ứng dụng di động ngày nay như một tính năng không thể thiếu cũng như giúp ứng dụng trở nên đa nhiệm hơn

    Thời gian vừa qua mình cũng có cơ duyên được trải qua một dự án về ngân hàng và phát triển tính năng QRPay, một tính năng mà như các bạn có thể thấy lúc nào cũng xuất hiện trên các ứng dụng Internet Banking cũng như các ví điện tử. Vậy nên ở bài viết này, mình xin chia sẻ các bạn cách tạo một ứng dụng Scan QR đơn giản và những framework liên quan trên iOS bằng ngôn ngữ lập trình Swift. Let’s go !

    Phần 1: Tìm hiểu về mã QR

    QR là viết tắt của Quick response có thể tạm dịch là mã phản hồi nhanh. Đây là dạng mã vạch có thể đọc được bởi một máy đọc chuyên dụng hoặc bằng smartphone có chức năng chụp ảnh kèm với ứng dụng cho phép quét mã. Mã QR còn có thể được gọi là Mã vạch ma trận (Matrix-barcode) hoặc Mã vạch 2 chiều (2D), là một dạng thông tin đã được mã hóa và có thể hiển thị để máy quét mã có thể đọc được.

    Mã QR là một mã vạch ma trận được phát triển bởi công ty Denso Wave vào năm 1994. Denso Wave là công ty con của Toyota. Mã QR gồm những chấm đen và các ô vuông trên nền trắng, nó thể chứa đa dạng các thông tin như URL, thông tin cá nhân, thời gian, địa điểm của một sự kiện nào đó, mô tả, giới thiệu một sản phẩm nào đó,…

    Phần 2: AV Foundation Framework trong Swift

    Như các bạn có thể thấy, để làm việc với Media trên các thiết bị iOS, Apple đã cung cấp rất nhiều các framework mạnh mẽ. Ở tầng cao nhất (high-level) là UIKit và AVKit, tầng thấp nhất (low-level) là CoreAudio, Core Media, Core Animation và ở giữa chính là nhân vật chính của chúng ta – AVFoundation

    UIKit framework giúp dễ dàng kết hợp tính năng chụp ảnh tĩnh và quay video cơ bản vào ứng dụng. Cả Mac OS X và iOS đều có thể sử dụng thẻ HTML5 <audio> và <video> bên trong WebView hoặc UIWebView để phát nội dung âm thanh và video. 

    Ngoài ra còn có AVKit framework, giúp đơn giản hóa việc xây dựng các ứng dụng phát video hiện đại. Tất cả các framework này đều thuận tiện và dễ sử dụng và nên thường được dùng khi thêm chức năng phương tiện vào ứng dụng. Tuy nhiên, mặc dù các framework này rất tiện lợi nhưng chúng thường thiếu tính linh hoạt và khả năng kiểm soát cần thiết cho các ứng dụng nâng cao hơn.

    Ở tầng thấp nhất, chúng ta có các low-level framework, cung cấp chức năng hỗ trợ và được sử dụng bởi tất cả các framework cấp cao hơn. Hầu hết chúng đều là các framework cấp độ thấp, cực kỳ mạnh mẽ và hiệu quả, nhưng rất phức tạp để tìm hiểu và sử dụng, đồng thời yêu cầu chúng ta phải hiểu rõ về cách phương tiện được xử lý ở cấp độ phần cứng.

    Chính vì vậy, vị cứu tính của chúng ta – AV Foundation nằm ở giữa low-level và high-level framework. Mang trong mình sức mạnh cũng như hiệu năng của các low-level framework nhưng lại dễ tiếp cận, dễ đọc hơn cho các developer. Nó có thể làm việc trực tiếp với các framework cấp thấp như Core Media và Core Audio và hoạt động với các high-level framework như Media Player và Assets Library. Như vậy chúng ta có thể đủ thấy sức mạnh của AVF mạnh như nào phải k ạ ^^

    AV Foundation là framework đầy đủ tính năng để làm việc với phương tiện nghe nhìn trên iOS, macOS, watchOS và tvOS. Sử dụng AV Foundation, chúng ta có thể dễ dàng phát, tạo và chỉnh sửa phim QuickTime và các tệp MPEG-4, HLS streams và xây dựng chức năng truyền thông mạnh mẽ vào ứng dụng.

    Với AV Foundation, chúng ta có thể tạo ra các ứng dụng liên quan đến chụp ảnh và quay video, phát nhạc,… nói chúng tất cả những thứ liên quan đến cụm từ Media trên mobile và ngoài ra nó còn kèm theo rất nhiều thứ hay ho như điều khiển đèn flash phía trước và phía sau, âm thanh cho video…vv.

    Như vậy có thể thấy AV Foundation framework khá lớn nên trong bài viết này, mình chỉ tìm hiểu một ứng dụng nhỏ của framework này đó là sử dụng camera trên thiết bị của Apple để đọc mã QR Code. Và chi tiết cách xây dựng ở phần 3 dưới đây.

    Phần 3: Xây dựng ứng dụng QR Scan đơn giản bằng AV Foundation framework trên Swift

    Đầu tiên chúng ta sẽ tạo một class QRScanCustomView như sau:

    import Foundation
    import UIKit
    import AVFoundation
    
    protocol QRScanCustomViewDelegate: AnyObject {
        func onDecodeFailed()
        func onDecodeSuccess(_ decodeString: String)
        func onCameraAccessDenied()
    }
    
    public class QRScanCustomView: UIView {
        weak var delegate: QRScanCustomViewDelegate?
        private var captureSession: AVCaptureSession?
        private var scanAreaWidth: CGFloat = 300.0
        private var scanAreaHeight: CGFloat = 300.0
        private var scanAreaXpos: CGFloat = 0.0
        private var scanAreaYpos: CGFloat = 0.0
        private var scanAreaCornerRadius: CGFloat = 8.0
        private var scanViewBackgroundColor = UIColor.black.withAlphaComponent(0.9)
        private var isAnimationFromTop: Bool = true
        private var animationTimer = Timer()
        private var animationView = UIView()
        private let gradientBackgroundView = UIView()
        private var gradientView = UIView()
        private let animationContainerView = UIView()
        
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            commonInit()
        }
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        
        public override var layer: AVCaptureVideoPreviewLayer {
            if let layer = super.layer as? AVCaptureVideoPreviewLayer {
                return layer
            }
            return AVCaptureVideoPreviewLayer()
        }
    }
    
    extension QRScanCustomView {
        func startScanning() {
            captureSession?.startRunning()
        }
        
        func stopScanning() {
            captureSession?.stopRunning()
        }
        
        func startAnimation() {
            runAnimation()
            animationTimer = Timer.scheduledTimer(timeInterval: 2.0, target: self, selector: #selector(runAnimation), userInfo: nil, repeats: true)
        }
        
        func stopAnimation() {
            animationTimer.invalidate()
        }
        
        func setSizeOfScanArea(width: CGFloat, height: CGFloat) {
            scanAreaWidth = width
            scanAreaHeight = height
        }
        
        private func commonInit() {
            self.clipsToBounds = true
            captureSession = AVCaptureSession()
            
            guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else {
                return
            }
            let videoInput: AVCaptureDeviceInput
            do {
                videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
            } catch {
                delegate?.onCameraAccessDenied()
                return
            }
            
            if captureSession?.canAddInput(videoInput) ?? false {
                captureSession?.addInput(videoInput)
            } else {
                scanningDidFail()
                return
            }
            
            let metadataOutput = AVCaptureMetadataOutput()
            
            if captureSession?.canAddOutput(metadataOutput) ?? false {
                captureSession?.addOutput(metadataOutput)
                
                metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
                metadataOutput.metadataObjectTypes = [.qr]
            } else {
                scanningDidFail()
                return
            }
            
            layer.session = captureSession
            layer.videoGravity = .resizeAspectFill
            layer.frame = UIScreen.main.bounds
            
            captureSession?.startRunning()
            
            // MARK: SET AVAILABLE RANGE TO SCAN
            let rectView = layer.metadataOutputRectConverted(fromLayerRect: rectForScannerRange())
            metadataOutput.rectOfInterest = rectView
        }
        
        // MARK: Get rect of scan range
        private func rectForScannerRange() -> CGRect {
            scanAreaXpos = (UIScreen.main.bounds.size.width - scanAreaWidth) / 2
            scanAreaYpos = (UIScreen.main.bounds.size.height - scanAreaHeight) / 2
            let newRect = CGRect(x: scanAreaXpos, y: scanAreaYpos, width: scanAreaWidth, height: scanAreaHeight)
            return newRect
        }
        // MARK: Add view with clear scanner range
        func addQRScannerOverlayView(view: UIView) {
            view.addSubview(overlayView())
            view.layer.addSublayer(createCornerFrame(with: rectForScannerRange()))
            // Set authorize here to run it after qrScannerDelegate was declared
            setupScanAnimationView(view: view)
        }
        // MARK: Create scan range
        private func overlayView() -> UIView {
            let overlayView = UIView(frame: frame)
            overlayView.backgroundColor = scanViewBackgroundColor
            let path = CGMutablePath()
            path.addRoundedRect(in: rectForScannerRange(), cornerWidth: scanAreaCornerRadius, cornerHeight: scanAreaCornerRadius)
            path.closeSubpath()
            path.addRect(CGRect(origin: .zero, size: overlayView.frame.size))
            
            let maskLayer = CAShapeLayer()
            maskLayer.backgroundColor = UIColor.black.cgColor
            maskLayer.path = path
            maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
            overlayView.layer.mask = maskLayer
            overlayView.clipsToBounds = true
            
            return overlayView
        }
        // MARK: Draw focus corner frame
        private func createCornerFrame(with rect: CGRect) -> CAShapeLayer {
            let length: CGFloat = 24.0
            let radius: CGFloat = 8.0
            let offset: CGFloat = 8.0
            let lineWidth: CGFloat = 4.0
            let distance: CGFloat = rect.size.width - offset * 2 - length * 2 - radius * 2
            
            let path = CGMutablePath()
            
            var start = CGPoint(x: rect.origin.x + offset, y: rect.origin.y + offset + radius + length)
            var end = CGPoint(x: start.x + radius + length, y: start.y - length - radius)
            path.move(to: start)
            path.addLine(to: CGPoint(x: start.x, y: start.y - length))
            path.addArc(tangent1End: CGPoint(x: start.x, y: start.y - length - radius),
                        tangent2End: CGPoint(x: start.x + radius, y: start.y - length - radius),
                        radius: radius)
            path.addLine(to: end)
            
            start = CGPoint(x: end.x + distance, y: end.y)
            end = CGPoint(x: start.x + length + radius, y: start.y + radius + length)
            path.move(to: start)
            path.addLine(to: CGPoint(x: start.x + length, y: start.y))
            path.addArc(tangent1End: CGPoint(x: start.x + length + radius, y: start.y),
                        tangent2End: CGPoint(x: start.x + length + radius, y: start.y + radius),
                        radius: radius)
            path.addLine(to: end)
            
            start = CGPoint(x: end.x, y: end.y + distance)
            end = CGPoint(x: start.x - radius - length, y: start.y + length + radius)
            path.move(to: start)
            path.addLine(to: CGPoint(x: start.x, y: start.y + length))
            path.addArc(tangent1End: CGPoint(x: start.x, y: start.y + length + radius),
                        tangent2End: CGPoint(x: start.x - radius, y: start.y + length + radius),
                        radius: radius)
            path.addLine(to: end)
            
            start = CGPoint(x: end.x - distance, y: end.y)
            end = CGPoint(x: start.x - length - radius, y: start.y - radius - length)
            path.move(to: start)
            path.addLine(to: CGPoint(x: start.x - length, y: start.y))
            path.addArc(tangent1End: CGPoint(x: start.x - length - radius, y: start.y),
                        tangent2End: CGPoint(x: start.x - length - radius, y: start.y - radius),
                        radius: radius)
            path.addLine(to: end)
            
            let shape = CAShapeLayer()
            shape.path = path
            shape.strokeColor = UIColor.orange.cgColor
            shape.lineWidth = lineWidth
            shape.fillColor = UIColor.clear.cgColor
            shape.lineCap = .round
            return shape
        }
        // MARK: Create scan animation view, size of inputView = size of QRScanView
        private func setupScanAnimationView(view: UIView) {
            animationContainerView.backgroundColor = .clear
            animationContainerView.frame = rectForScannerRange()
            view.addSubview(animationContainerView)
            
            animationView.frame = CGRect(x: animationContainerView.bounds.minX - 8,
                                         y: animationContainerView.bounds.minY,
                                         width: animationContainerView.bounds.width + 16,
                                         height: 3.0)
            animationView.layer.cornerRadius = animationView.frame.height / 2
            animationView.backgroundColor = UIColor.orange
            animationContainerView.addSubview(animationView)
            animationContainerView.bringSubviewToFront(animationView)
            
            gradientBackgroundView.frame = animationContainerView.bounds
            gradientBackgroundView.backgroundColor = .clear
            animationContainerView.addSubview(gradientBackgroundView)
            animationContainerView.bringSubviewToFront(gradientBackgroundView)
            gradientBackgroundView.clipsToBounds = true
        }
        
        @objc
        private func runAnimation() {
            animationView.clipsToBounds = false
            if isAnimationFromTop {
                for subView in gradientBackgroundView.subviews {
                    subView.removeFromSuperview()
                }
                gradientBackgroundView.addSubview(createGradientViewEffect(rect: CGRect(x: 0,
                                                                                        y: -20,
                                                                                        width: gradientBackgroundView.bounds.width,
                                                                                        height: 20),
                                                                           isRevertColor: false))
                UIView.animate(withDuration: 2.0, animations: {
                    self.animationView.transform = CGAffineTransform(translationX: 0,
                                                                     y: self.animationContainerView.frame.size.height)
                    self.gradientView.transform = CGAffineTransform(translationX: 0,
                                                                    y: self.animationContainerView.frame.size.height)
                    Timer.scheduledTimer(timeInterval: 1.6, target: self, selector: #selector(self.hideGradientView), userInfo: nil, repeats: false)
                })
                
            } else {
                for subView in gradientBackgroundView.subviews {
                    subView.removeFromSuperview()
                }
                gradientBackgroundView.addSubview(createGradientViewEffect(rect: CGRect(x: 0,
                                                                                        y: gradientBackgroundView.bounds.height,
                                                                                        width: gradientBackgroundView.bounds.width,
                                                                                        height: 20),
                                                                           isRevertColor: true))
                UIView.animate(withDuration: 2.0, animations: {
                    self.animationView.transform = CGAffineTransform(translationX: 0,
                                                                     y: 0)
                    self.gradientView.transform = CGAffineTransform(translationX: 0,
                                                                    y: 0)
                    Timer.scheduledTimer(timeInterval: 1.6, target: self, selector: #selector(self.hideGradientView), userInfo: nil, repeats: false)
                })
            }
            isAnimationFromTop = !isAnimationFromTop
        }
        
        @objc
        private func hideGradientView() {
            UIView.animate(withDuration: 0.4, animations: {
                self.gradientView.alpha = 0
            })
        }
        
        private func createGradientViewEffect(rect: CGRect, isRevertColor: Bool) -> UIView {
            gradientView.layer.sublayers?.removeAll()
            gradientView.frame = rect
            let gradientLayer: CAGradientLayer = CAGradientLayer()
            gradientLayer.frame = gradientView.bounds
            let firstColor = UIColor.orange.withAlphaComponent(0.01).cgColor
            let secondColor = UIColor.orange.withAlphaComponent(1).cgColor
            gradientLayer.colors = isRevertColor ? [secondColor, firstColor] : [firstColor, secondColor]
            gradientLayer.locations = [0.0, 1.0]
            gradientView.layer.insertSublayer(gradientLayer, at: 0)
            gradientView.alpha = 0.6
            return gradientView
        }
        
        func scanningDidFail() {
            delegate?.onDecodeFailed()
            captureSession = nil
        }
        
        func scanSuccess(code: String) {
            delegate?.onDecodeSuccess(code)
        }
    }
    
    extension QRScanCustomView: AVCaptureMetadataOutputObjectsDelegate {
        public func metadataOutput(_ output: AVCaptureMetadataOutput,
                                   didOutput metadataObjects: [AVMetadataObject],
                                   from connection: AVCaptureConnection) {
            stopScanning()
            
            if let metadataObject = metadataObjects.first {
                guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else {
                    return
                }
                guard let stringValue = readableObject.stringValue else {
                    return
                }
                AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
                scanSuccess(code: stringValue)
            }
        }
    }

    Để có thể sử dụng thư viện, chúng ta phải import thư viện “import AVFoundation” và khởi tạo “captureSession” để quản lý session capture. Có thể hiểu đơn giản rằng captureSession giúp quản lý việc sử dụng camera của chúng ta.

    Ngoài ra, vì ứng dụng của chúng ta sử dụng camera để capture mã QR nên phải khai báo Camera Usage Description trong Info.plist. Nếu không khai báo ứng dụng của chúng ta sẽ không thể chạy vì vi phạm policy của Apple

    Tiếp theo chúng ta sẽ khởi tạo AVCaptureVideoPreviewLayer:

    public override var layer: AVCaptureVideoPreviewLayer {
            if let layer = super.layer as? AVCaptureVideoPreviewLayer {
                return layer
            }
            return AVCaptureVideoPreviewLayer()
        }

    Layer này chính là một layer hiển thị lên màn hình có vai trò thực hiện việc sử dụng camera để scan mã QR

    Tạo ra các func để quản lý start, stop capture session:

       func startScanning() {
            captureSession?.startRunning()
        }
        
        func stopScanning() {
            captureSession?.stopRunning()
        }

    Ở func commonInit(), chúng ta sẽ tạo khởi tạo videoCaptureDevice để sử dụng kiểu capture video sử dụng camera của chúng ta và add vào kiểu input của captureSession, ở đây có thể hiểu là chúng ta đang khởi tạo đầu vào cho ứng dụng của chúng ta sử dụng camera để scan QR

            guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else {
                return
            }
            let videoInput: AVCaptureDeviceInput
            do {
                videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
            } catch {
                delegate?.onCameraAccessDenied()
                return
            }
            
            if captureSession?.canAddInput(videoInput) ?? false {
                captureSession?.addInput(videoInput)
            } else {
                scanningDidFail()
                return
            }

    Sau khi thực hiện khởi tạo đầu vào, chúng ta sẽ khởi tạo đầu ra cho captureSession qua class AVCaptureMetadataOutput, ở đây mình đã custom một khoảng trắng giữa màn hình để tạo vùng scan bằng thuộc tính “rectOfInterest”. Nếu không gắn thuộc tính này, mặc định cả màn hình capture của chúng ra sẽ là vùng scan:

     let metadataOutput = AVCaptureMetadataOutput()
            
            if captureSession?.canAddOutput(metadataOutput) ?? false {
                captureSession?.addOutput(metadataOutput)
                
                metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
                metadataOutput.metadataObjectTypes = [.qr]
            } else {
                scanningDidFail()
                return
            }
            
            layer.session = captureSession
            layer.videoGravity = .resizeAspectFill
            layer.frame = UIScreen.main.bounds
            
            captureSession?.startRunning()
            
            // MARK: Set scan Area
            let rectView = layer.metadataOutputRectConverted(fromLayerRect: rectForScannerRange())
            metadataOutput.rectOfInterest = rectView

    Để hứng được output của captureSession. Chúng ta sẽ kế thừa delegate của AVCaptureMetadataOutput. Delegate này sẽ giúp chúng ta get được String decode được từ mã QR:

    extension QRScanCustomView: AVCaptureMetadataOutputObjectsDelegate {
        public func metadataOutput(_ output: AVCaptureMetadataOutput,
                                   didOutput metadataObjects: [AVMetadataObject],
                                   from connection: AVCaptureConnection) {
            stopScanning()
            
            if let metadataObject = metadataObjects.first {
                guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else {
                    return
                }
                guard let stringValue = readableObject.stringValue else {
                    return
                }
                AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
                scanSuccess(code: stringValue)
            }
        }
    }

    Ngoài ra mình còn tạo ra các func để custom màn hình và tạo animation quét QR để trông ứng dụng của chúng ta trông đẹp mắt hơn. Các bạn có thể tham khảo ở phần source đầy đủ bên trên.

    Về cơ bản chỉ cần khởi tạo đủ AVCaptureSession, tạo ra input và output cho AVCaptureSession và kế thừa delegate để hứng output là chúng ta có thể tạo ra một ứng dụng scan QR đơn giản.

    Bài viết của mình vẫn đang trong quá trình update để đầy đủ và chi tiết hơn, nếu có góp ý gì mọi người có thể comment bên dưới để mình bổ sung và cải thiện thêm nhé.

    Cảm ơn mọi người đã quan tâm đến bài viết của mình. Chúc mọi người có một ngày làm việc hiệu quả !

  • iOS Architecture Patterns: Cocoa MVC

    iOS Architecture Patterns: Cocoa MVC

    Là một iOS developer chắc hẳn các bạn không lạ gì với Cocoa MVC. Nó được coi là một trong những architecture pattern để phát triển ứng dụng iOS phổ biến nhất. Nó rất dễ sử dụng và được chính Apple khuyên dùng. iOS, MacOS và watchOS đều sử dụng cấu trúc này làm kiến ​​trúc mặc định để phát triển. Tuy được rất phổ biến và được Apple khuyên dùng nhưng nó cũng có những ưu điểm và nhược điểm, vì vậy bài viết này mình sẽ giới thiệu và giải thích cho các bạn về Cocoa MVC, ưu điểm, nhược điểm và khi nào nên chọn Cocoa MVC sử dụng cho ứng dụng của bạn.

    Giải thích về Cocoa MVC

    Cocoa MVC là viết tắt của Cocoa Model View Controller. Cocoa MVC gán các đối tượng trong ứng dụng iOS bằng một trong 3 vai trò sau: Model, View hoặc Controller. Mẫu kiến trúc này không chỉ xác định vai trò của các đối tượng trong ứng dụng mà nó còn xác định cả cách các đối tượng giao tiếp với nhau. Ba loại đối tượng này được phân tách khỏi các loại khác bằng các ranh giới trừu tượng và giao tiếp với các đối tượng thuộc các loại khác thông qua các ranh giới đó. Tập hợp các đối tượng của một loại MVC nhất định trong một ứng dụng đôi khi được gọi là một Layer, ví dụ: Layer model.

    Cocoa MVC

    Model

    Những đối tượng được gán với vai trò model trong mẫu kiến trúc Cocoa MVC sẽ làm nhiệm vụ đóng gói dữ liệu cụ thể cho một ứng dụng và xác định logic và tính toán để thao tác và xử lý dữ liệu đó. Những đối tượng này có thể có một hoặc nhiều mối quan hệ với các đối tượng mô hình khác và do đó, đôi khi lớp Model của một ứng dụng thực sự là một hoặc nhiều đối tượng. Phần lớn dữ liệu đều nằm ở Model sau khi nó được tải vào ứng dụng bằng các cách khác nhau(API, Files, …). Vì Model đại diện cho kiến thức và chuyên môn cho một vấn đề cụ thể nên nó có thể được tái sử dụng khi có các trường hợp tương tự. Các Model sẽ không có liên kết trực tiếp với View và View cũng không được trực tiếp sửa dữ liệu của Model mà nó sẽ phải thực hiện thông qua Controller.

    Giao tiếp: Các hành động của người dùng trên View sẽ gọi đến Controller, khi này Controller sẽ gọi đến Model tương ứng để thực hiện cập nhật dữ liệu cho Model. Khi Model thay đổi (ví dụ: dữ liệu mới được nhận qua kết nối mạng), nó sẽ thông báo cho Controller, lúc này Controller sẽ cập nhật các Views thích hợp.

    View

    View là một đối tượng trong ứng dụng mà người dùng có thể nhìn thấy. Một đối tượng View biết cách tự vẽ và có thể phản hồi các hành động của người dùng. Mục đích chính của View là hiển thị dữ liệu từ Model của ứng dụng và cho phép chỉnh sửa dữ liệu đó. Mặc dù vậy, View thường được tách rời khỏi Model trong ứng dụng MVC.

    Bởi vì bạn thường sử dụng lại và cấu hình lại chúng, nên View cung cấp tính nhất quán giữa các ứng dụng. Cả UIKit và AppKit framework đều cung cấp các bộ sưu tập View classes và Trình tạo giao diện(Interface Builder) cung cấp hàng tá view objects trong Thư viện của nó.

    Giao tiếp: View tìm hiểu về các thay đổi trong dữ liệu của Model thông qua Controller của ứng dụng và truyền đạt các thay đổi do người dùng. ví dụ: văn bản được nhập vào TextField thông qua Controller đến Model của ứng dụng.

    Controller

    Controller vai trò trung gian giữa một hoặc nhiều View của ứng dụng và một hoặc nhiều Model của nó. Do đó, cController là một đường dẫn mà qua đó View tìm hiểu về những thay đổi trong Model và ngược lại. Controller cũng có thể thực hiện các tác vụ thiết lập và điều phối cho một ứng dụng và quản lý vòng đời của các đối tượng khác.

    Giao tiếp: Controller diễn giải các hành động của người dùng được thực hiện trong View và truyền dữ liệu mới hoặc dữ liệu đã thay đổi tới Model. Khi Model thay đổi, Controller sẽ giao tiếp dữ liệu Model mới đó với View để chúng có thể hiển thị nó.

    Ưu điểm của Cocoa MVC

    Cocoa MVC có khá nhiều ưu điểm như sau:

    1. Dễ hiểu và dễ sử dụng, vì vậy ai cũng có thể làm việc với nó một cách dễ dàng kể cả người mới
    2. Được Apple khuyên dùng, vì vậy nó rất phổ biến khi gặp vấn đề sẽ dễ xử lí.
    3. Giúp developer tách source của họ ra làm các đối tượng khác nhau với 3 vai trò riêng biệt. Khi có lỗi xảy ra chúng ta sẽ khoanh vùng được nơi xảy ra lỗi.
    4. Tránh việc phải tạo một file quá dài, ảnh hưởng tới việc maintain ứng dụng
    5. Có thể tái sử dụng và mở rộng

    Nhược điểm của Cocoa MVC

    1. Phân chia nhiệm vụ giữa các vai trò không đồng đều, View chỉ làm nhiệm vụ hiển thị và nhận action từ người dùng, Controller thì chỉ là trung gian điều hướng giữa View-Model, trong khi đó Model phải làm quá nhiều việc từ lưu dữ liệu, xử lí dữ liệu, thực hiện Business Logic của ứng dụng, … Đó là lí do Model còn hay được gọi với cái tên khác là Massive
    2. Không hỗ trợ tốt cho UnitTest bởi View phải phụ thuộc vào cả Controller và Model. View sẽ không thể xử lý được vấn đề gì bởi View không thể nhận yêu cầu và cũng không có dữ liệu để hiển thị. Để tiến hành UnitTest trên View, chúng ta cần giả lập cả Controller và Model.
    3. Đối với các ứng dụng quy mô lớn, quy trình xử lý nghiệp vụ có tính phức tạp cao, lượng dữ liệu lớn thì mô hình MVC trở nên rất cồng kềnh và khó để thực hiện.

    Khi nào bạn nên sử dụng Cocoa MVC

    Như đã phân tích ở trên, các bạn cũng đã nhìn thấy cách vận hành của mô hình này, các ưu điểm và nhược điểm của nó. Vậy khi nào thì chúng ta nên sử dụng Cocoa MVC cho ứng dụng của mình. Theo mình thì sẽ các tiêu chí như sau:

    1. Khi bạn không biết về các architecture patterns khác tốt hơn, bạn chỉ hiểu rõ về MVC hoặc bạn và member trong team là người mới thì nên chọn Cocoa MVC cho dự án của mình.
    2. Dự án của bạn có kích thước vừa và nhỏ, số iOS developer có số lượng ít, có ít hiểu biết về các architecture patterns khác.

    Tổng kết

    Qua bài viết trên mình đã giới thiệu cho các bạn về một architecture pattern rất phổ biến trong lập trình ứng dụng iOS. Ngoài ra cũng giúp các bạn hiểu rõ về cách hoạt động cũng như ưu điểm và nhược điểm của Cocoa MVC. Mình hi vọng bài viết này sẽ giúp các bạn có thể hiểu rõ hơn về Cocoa MVC cũng như có những lựa chọn tốt nhất cho từng dự án mà sắp tới các bạn phát triển.

  • Mutating func trong Struct và enum – Swift

    Mutating func trong Struct và enum – Swift

    Như mọi người đã biết, struct và enum trong Swift là value types(kiểu giá trị), mặc định thì các thuộc tính của kiểu giá trị thì không thể được sửa đổi ở bên trong các phương thức thể hiện của nó(instance methods).

    Tuy nhiên nếu chúng ta cần phải chỉnh sửa các thuộc tính của struct hoặc enum trong một phương thức cụ thể, thì chúng ta sẽ đặt mutating trước các func, nó sẽ giúp các func của bạn có thể thay đổi được các thuộc tính bên trong func và khi kết thúc func giá trị sẽ được nghi lại vào các thuộc tính của struct ban đầu. Phương thức này cũng có thể gán lại một instance mới cho thuộc tính self của nó và nó sẽ được thay thế khi phương thức kết thúc.

    Mutating trong Struct

    Trong ví dụ này mình sẽ tạo một Struct có tên là Counter và tạo ra một func lấy giá trị của thuộc tính count trong Counter như sau:

    struct Counter {
        private var count: Int = 0
        
        func getCount() -> Int {
            count
        }
    }

    Đây là một ví dụ bình thường về struct, hàm getCount() ở đây không thực hiện thay đổi giá trị của struct Counter mà nó chỉ lấy giá trị của thuộc tính count theo cách thông thường.

    Vậy khi chúng ta muốn viết một hàm increase() để tăng giá trị count thông thường chúng ta sẽ viết như sau:

    Nếu là class thì sẽ không vấn đề gì vì class là reference type. Ở trường họp này do chúng ta đang viết một func chỉnh sửa thuộc tính count của struct Counter nên xCode sẽ báo lỗi rằng self ở đây là immutable(không thể thay đổi), như đã giải thích ở trên thì struct là value type nên mặc định sẽ không thể thay đổi được thuộc tính của nó trong các func của struct đó.

    func getCount() không bị báo lỗi vì func này không làm thay đổi thuộc tính trong struct.

    Để func increase() không bị báo lỗi chúng ta cần thêm mutating đằng trước func để xCode biết là func này có thể thay đổi được thuộc tính của struct:

    struct Counter {
        private var count: Int = 0
        
        func getCount() -> Int {
            count
        }
        
        mutating func increase() {
            count += 1
        }
    }
    
    var counter = Counter() // count = 0
    counter.increase() // count = 1

    Để có thể thay đổi được thuộc tính của instance counter thì chúng ta cần phải khai báo nó là var, vì func increase() sẽ thay đổi giá trị của counter vì vậy cần khai báo là var để mutating func có thể gán lại giá trị mới cho instance counter.

    Nếu chúng ta để là let Xcode sẽ thông báo lỗi không thể sử dụng mutating func trên giá trị không thể thay đổi, counter đang là một “let” constant. Do struct là value type nên nó là immutable có nghĩa là không thay đổi được, nếu chúng ta cố tình khai báo let count Xcode sẽ thông báo lỗi.

    Lỗi khi khai báo let counter để call mutating func

    NOTE: Để hiểu rõ hơn các bạn có thể xem thêm thông tin ở đây: Stored Properties of Constant Structure Instances

    Mutating func không chỉ thay đổi được thuộc tính của struct mà nó còn có thể thay đổi cả giá trị của chính instance (self)

    struct Counter {
        private var count: Int = 0
        
        func getCount() -> Int {
            count
        }
        
        mutating func increase() {
            count += 1
        }
        
        mutating func resetCounter() {
            self = Counter(count: 0)
        }
    }
    
    var counter = Counter() // count = 0
    counter.increase() // count = 1
    counter.resetCounter() // count = 0

    mutating func resetCounter() là một ví dụ, ở trong func này chúng ta thực hiện tạo ra một instance Counter mới với giá trị khởi tạo là 0 và gán lại cho chính instance gọi func này.

    Mutating func trong enum

    Tương tự như struct, enum cũng là value type và để thay đổi giá trị trong func chúng cũng cần phải sửa dụng mutating cho func đó.

    Để hiểu rõ hơn ta đi vào ví dụ sau:

    Chúng ta cần tạo ra một công tắc quạt với một tính năng là mỗi khi bấm nút thì sẽ làm thay đổi tốc độ quay của quạt một cách tuần tự và lặp đi lặp lại. Để làm theo yêu cầu chúng ta sẽ tạo enum như sau:

    enum FanStateSwitch {
        case off, low, high
        mutating func next() {
            switch self {
            case .off:
                self = .low
            case .low:
                self = .high
            case .high:
                self = .off
            }
        }
    }
    
    var fanSwitch = FanStateSwitch.off
    fanSwitch.next() // fanSwitch is low
    fanSwitch.next() // fanSwitch is high
    fanSwitch.next() // fanSwitch is off

    Tương tự như struct khi khởi tạo enum hãy nhớ khởi tạo nó với var thay vì let.

    Hi vọng bài viết sẽ giúp các bạn hiểu rõ hơn về mutating func và cách sử dụng, ứng dụng nó vào trong dự án.

    Chúc các bạn thành công!

  • Hướng dẫn cách làm slider hai chiều trong Swift

    Hướng dẫn cách làm slider hai chiều trong Swift

    Là một iOS developer, chắc hẳn mọi người đã từng làm những tính năng mà các UIKit của Apple không thể đáp ứng được yêu cầu. Lúc này chúng ta cần phải thực hiện custom hoặc tạo một common UI với cơ chế mới thoả mãn yêu cầu của khách hàng. Và ở dự án mình cũng có những yêu cầu như vậy, nên bài viết này mình sẽ hướng dẫn các bạn làm slider hai chiều trong Swift.

    Mục đích của chúng ta là đạt được kết quả như sau:

    Slider hai chiều

    I. Yêu cầu

    Cần tạo một common slider thoả mãn các điều kiện như sau:

    1. Slider được chia thành các đoạn bằng nhau có thể chỉnh sửa, Slider có 2 điểm kéo, có thể kéo được từ giá trị min tới giá trị max, kéo chồng lên nhau, khi kéo tới các điểm sẽ thực hiện rung.
    2. Cho phép thay đổi giao diện

    II. Giải pháp

    1. Tạo một mảng string để lưu các điểm cho phép và dùng nó để tính toán và hiển thị dữ liệu cho Slider hai chiều
    2. Dựa vào số phần tử trong mảng để tính khoảng cách giữa các điểm và vị trí của nó.
    3. Đối với tính năng kéo mình sẽ dùng UIPanGestureRecognizer để xử lí. Khi kết thúc hành động kéo thì xác định vị trí của điểm được kéo so với các điểm trong mảng, nếu gần điểm nào thì tự di chuyển tới vị trí của điểm đó.

    III. Hướng dẫn chi tiết

    Để cho tất cả mọi người đều có thể thực hiện một các dễ dàng thì mình sẽ hướng dẫn chi tiết theo các bước như sau:

    1. Tạo mới file swift

    Chúng ta cần tạo mới file swift để làm common Slider hai chiều theo các bước sau:

    Trong project, bấm chuột phải vào thư mục muốn tạo file -> new file -> chọn swift -> đặt tên cho file, ở đây mình đặt là: TWSlider.swift

    Kết quả tạo file

    2. Thêm nội dung và logic cho file TWSlider.swift

    Trước tiên chúng ta cần thêm image vào asset của project để sử dụng cho slider, các bạn cũng có thể thêm các image tương ứng với yêu cầu của dự án đang làm.

    Và chúng ta cũng cần thêm Then vào để setup UI cho tiện như sau

    public protocol Then {}
    
    public extension Then {
        @discardableResult
        func then(_ block: (Self) -> Void) -> Self {
            block(self)
            return self
        }
    }
    
    extension NSObject: Then {}
    extension CGPoint: Then {}
    extension CGRect: Then {}
    extension CGSize: Then {}
    extension CGPath: Then {}

    1. Tạo protocol để bắn dữ liệu cần thiết ra màn hình cần xử lí logic

    protocol TWSliderDelegate: AnyObject {
        func sliderScrolled(_ slider: TWSlider?,
                            toMinIndex minIndex: Int,
                            andMaxIndex maxIndex: Int,
                            endDragDrop: Bool)
    }

    2. Tạo các biến cho phép thay đổi các giá trị của slider

    class TWSlider: UIView {
        
        // PUBLIC variable
        // The number of points in slider
        var numberOfSegments: Int = 0
        
        // This value should be set if slider button should overlap or not. Default to NO. ie., 1 segment space will be present between the sliders.
        var shouldSliderButtonOverlap: Bool = false
        
        // The color that is used for rangeSlider unselected range view. (i.e., the view that is not within the slider points). Default is #DFDEE4.
        var rangeSliderBackgroundColor: UIColor = .gray
        
        // The color that is used for rangeSlider selected range view. (i.e., the view that is between the slider points). Default is #FF9800.
        var rangeSliderForegroundColor: UIColor = .orange
        
        // The label color to be used for min range display. Default is white.
        var rangeDisplayLabelColor: UIColor = .white
        
        // The label color to be used for min range display. Default is black.
        var minMaxDisplayLabelColor: UIColor = .black
        
        // The color for segment button when it is within the selected range. Default is clear.
        var segmentSelectedColor: UIColor = .clear
        
        // The color for segment button when it is outside the selected range. Default is #F8F8FA.
        var segmentUnSelectedColor: UIColor = .white
        
        // The image used for displaying slider buttons. By default "ic_sliderButton". rangeSliderButtonColor will be used if not set
        var rangeSliderButtonImage: UIImage?
        
        // The image for segment button when it is within the selected range. If not set, segmentSelectedColor will be used.
        var segmentSelectedImage: UIImage?
        
        // The image for segment button when it is outside the selected range. If not set, segmentUnSelectedColor will be used.
        var segmentUnSelectedImage: UIImage?
        
        // The size of the slider button. If not set, defaults to (16, 16).
        var sliderSize: CGSize = CGSize(width: 16, height: 16)
        
        // The size of the segments. If not set, defaults to (4, 4).
        var segmentSize: CGSize = CGSize(width: 4, height: 4)
        
        // The min and max range label text to be set by caller
        var minRangeText: String?
        var maxRangeText: String?
        
        // The delegate property
        weak var delegate: TWSliderDelegate?
        
        // PRIVATE VAR
        // Slider button size
        private var SLIDER_BUTTON_WIDTH: CGFloat = 44.0
        
        // Slider frame
        private var DEFAULT_SLIDER_FRAME: CGRect!
        
        // Segment width
        private var segmentWidth: CGFloat!
        
        // The backgroundView represent unselected/outside range view
        private var sliderBackgroundView: UIView!
        
        // The foregroundView represent selected/inside range view
        private var sliderForegroundView: UIView!
        
        // The label placed below the min and max sliders
        private var minRangeView: UIView!
        private var maxRangeView: UIView!
        private var minRangeLabel: UILabel!
        private var maxRangeLabel: UILabel!
        
        // min and max value
        private var minLabel: UILabel!
        private var maxLabel: UILabel!
        
        // Represent the range slider on either side of the slider
        private var startSliderButton: UIButton!
        private var endSliderButton: UIButton!
        
        // The segment index or percent for initial slider position loading for segmented and unsegmented respectively
        private var minRangeInitialIndex: Int?
        private var maxRangeInitialIndex: Int?
        
        // Padding range view with slider button
        private let iconRangeMidView = "ic_rangeMidView"
        private let iconRangeView = "ic_rangeView"
        private let iconSliderButton = "ic_sliderButton"
        private let iconSegmentUnSelected = "ic_segmentUnSelected"
        private let paddingRangeView: CGFloat = 4
        
        private var sliderMidView: UIView!
        private var sliderMidLabel: UILabel!
        private var checkInit: Bool = false
        // MARK: init
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            setUpUI()
        }
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            setUpUI()
        }
        
        func setUpUI() {
            setDefaultValues()
            initSliderViews()
        }
        
        override func layoutSubviews() {
            super.layoutSubviews()
            updateFrame()
        }
    }

    3. Tạo các hàm private để setup UI cho slider

    extension TWSlider {
        
        // Default Initializaer
        private func setDefaultValues() {
            checkInit = false
            numberOfSegments = 2
            shouldSliderButtonOverlap = false
            
            minRangeLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 24, height: 24))
            minRangeLabel.then {
                $0.textColor = rangeDisplayLabelColor
                $0.textAlignment = .center
                $0.font = UIFont.systemFont(ofSize: 12, weight: .medium)
            }
            
            minRangeView = UIView(frame: CGRect(x: 0, y: 0, width: 24, height: 29))
            minRangeView.then {
                $0.center = CGPoint(x: SLIDER_BUTTON_WIDTH / 2, y: bounds.midY - (SLIDER_BUTTON_WIDTH / 2) - (paddingRangeView / 2))
                if let image = UIImage(named: iconRangeView) {
                    $0.backgroundColor = UIColor(patternImage: image)
                }
                addSubview($0)
                $0.addSubview(minRangeLabel)
            }
            
            maxRangeLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 24, height: 24))
            maxRangeLabel.then {
                $0.textColor = rangeDisplayLabelColor
                $0.textAlignment = .center
                $0.font = UIFont.systemFont(ofSize: 12, weight: .medium)
            }
            
            maxRangeView = UIView(frame: CGRect(x: 0, y: 0, width: 24, height: 29))
            maxRangeView.then {
                $0.center = CGPoint(x: SLIDER_BUTTON_WIDTH / 2, y: bounds.midY + (SLIDER_BUTTON_WIDTH / 2) + (paddingRangeView / 2))
                if let image = UIImage(named: iconRangeView) {
                    $0.backgroundColor = UIColor(patternImage: image)
                }
                addSubview($0)
                $0.addSubview(maxRangeLabel)
            }
            
            minLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 24, height: 24))
            minLabel.then {
                $0.center = CGPoint(x: SLIDER_BUTTON_WIDTH / 2, y: bounds.midY + (SLIDER_BUTTON_WIDTH / 2))
                $0.textColor = minMaxDisplayLabelColor
                $0.textAlignment = .center
                $0.font = UIFont.systemFont(ofSize: 12, weight: .regular)
                addSubview($0)
            }
            
            maxLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 24, height: 24))
            maxLabel.then {
                $0.center = CGPoint(x: bounds.maxX - SLIDER_BUTTON_WIDTH, y: bounds.midY + (SLIDER_BUTTON_WIDTH / 2))
                $0.textColor = minMaxDisplayLabelColor
                $0.textAlignment = .center
                $0.font = UIFont.systemFont(ofSize: 12, weight: .regular)
                addSubview($0)
            }
            
            // Mid View
            sliderMidLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 49, height: 24))
            sliderMidLabel.then {
                $0.textColor = rangeDisplayLabelColor
                $0.textAlignment = .center
                $0.font = UIFont.systemFont(ofSize: 12, weight: .medium)
            }
            sliderMidView = UIView(frame: CGRect(x: 0, y: 0, width: 49, height: 29))
            sliderMidView.then {
                $0.center = CGPoint(x: SLIDER_BUTTON_WIDTH / 2, y: bounds.midY + (SLIDER_BUTTON_WIDTH / 2) + (paddingRangeView / 2))
                if let image = UIImage(named: iconRangeMidView) {
                    $0.backgroundColor = UIColor(patternImage: image)
                }
                addSubview($0)
                $0.addSubview(sliderMidLabel)
                $0.alpha = 0
            }
            segmentWidth = getSegmentWidth(forSegmentCount: numberOfSegments)
            rangeSliderButtonImage = UIImage(named: iconSliderButton)
            segmentUnSelectedImage = UIImage(named: iconSegmentUnSelected)
            
        }
        
        // Init the sliding representing views
        private func initSliderViews() {
            DEFAULT_SLIDER_FRAME = CGRect(x: SLIDER_BUTTON_WIDTH / 2, y: self.bounds.midY, width: self.bounds.width - SLIDER_BUTTON_WIDTH, height: 4)
            
            sliderBackgroundView = UIView(frame: DEFAULT_SLIDER_FRAME)
            sliderBackgroundView.then {
                $0.backgroundColor = rangeSliderBackgroundColor
                addSubview($0)
            }
            
            sliderForegroundView = UIView(frame: DEFAULT_SLIDER_FRAME)
            sliderForegroundView.then {
                $0.backgroundColor = rangeSliderForegroundColor
                addSubview($0)
            }
            startSliderButton = getSegmentButton(withSegmentIndex: 1, isSlider: true)
            addSubview(startSliderButton)
            
            endSliderButton = getSegmentButton(withSegmentIndex: numberOfSegments, isSlider: true)
            addSubview(endSliderButton)
            
            // Pan gesture for identifying the sliding (More accurate than touchesMoved).
            addPanGestureRecognizer()
        }
        
        // Update the frames
        private func updateFrame() {
            if self.numberOfSegments >= 2 {
                // If the range selectors are at the extreme points, then reset the frame. Else fo nothing
                if isRangeSlidersPlacedAtExtremePosition() {
                    DEFAULT_SLIDER_FRAME = CGRect(x: SLIDER_BUTTON_WIDTH / 2, y: self.bounds.midY, width: self.bounds.width - SLIDER_BUTTON_WIDTH, height: 4)
                    self.sliderBackgroundView.frame = DEFAULT_SLIDER_FRAME
                    self.sliderForegroundView.frame = DEFAULT_SLIDER_FRAME
                    
                    self.sliderBackgroundView.backgroundColor = self.rangeSliderBackgroundColor
                    self.sliderForegroundView.backgroundColor = self.rangeSliderForegroundColor
                    
                    segmentWidth = getSegmentWidth(forSegmentCount: numberOfSegments)
                    
                    startSliderButton.center = CGPoint(x: SLIDER_BUTTON_WIDTH / 2, y: sliderBackgroundView.frame.midY)
                    endSliderButton.center = getSegmentCenterPoint(forSegmentIndex: numberOfSegments)
                    
                    minRangeView.center = CGPoint(x: startSliderButton.frame.midX, y: bounds.midY - (SLIDER_BUTTON_WIDTH / 2) - (paddingRangeView / 2))
                    maxRangeView.center = CGPoint(x: endSliderButton.frame.midX, y: bounds.midY - (SLIDER_BUTTON_WIDTH / 2) - (paddingRangeView / 2))
                    
                    minLabel.center = CGPoint(x: startSliderButton.frame.midX, y: bounds.midY + (SLIDER_BUTTON_WIDTH / 2))
                    maxLabel.center = CGPoint(x: endSliderButton.frame.midX, y: bounds.midY + (SLIDER_BUTTON_WIDTH / 2))
                    
                    sliderMidView.center = CGPoint(x: startSliderButton.frame.midX, y: bounds.midY - (SLIDER_BUTTON_WIDTH / 2) - (paddingRangeView / 2))
                    
                    setImageForSegmentOrSliderButton(startSliderButton, isSlider: true)
                    setImageForSegmentOrSliderButton(endSliderButton, isSlider: true)
                    
                    // Reset the frame of all the intermediate buttons
                    for segmentIndex in 1...numberOfSegments {
                        let segmentButton = self.viewWithTag(segmentIndex) as? UIButton
                        segmentButton?.center = getSegmentCenterPoint(forSegmentIndex: segmentIndex)
                        if let button = segmentButton {
                            setImageForSegmentOrSliderButton(button, isSlider: false)
                        }
                    }
                    
                    // Slide the buttons if the initial position is needed
                    slideRangeSliderButtonsIfNeeded()
                    checkInit = true
                }
            }
        }
    }

    4. Tạo các hàm để tính toán vị trí và di chuyển slider

    extension TWSlider {
        private func isRangeSlidersPlacedAtExtremePosition() -> Bool {
            let sliderBackgroundViewMaxX = sliderBackgroundView.frame.maxX + (SLIDER_BUTTON_WIDTH / 2)
            return (startSliderButton.frame.minX == 0.0 && endSliderButton.frame.maxX == sliderBackgroundViewMaxX)
        }
        
        private func slideRangeSliderButtonsIfNeeded() {
            var startScrollPoint = CGPoint.zero
            var endScrollPoint = CGPoint(x: bounds.size.width, y: 0)
            if let min = minRangeInitialIndex, let max = maxRangeInitialIndex, min < max {
                let startX = getSegmentCenterPoint(forSegmentIndex: min).x
                startScrollPoint.x = startX > (SLIDER_BUTTON_WIDTH / 2) ? startX : SLIDER_BUTTON_WIDTH / 2
                let endX = getSegmentCenterPoint(forSegmentIndex: max).x
                endScrollPoint.x = endX > (SLIDER_BUTTON_WIDTH / 2) ? endX : SLIDER_BUTTON_WIDTH / 2
            }
            scrollStartAndEndSlider(for: startScrollPoint, andEndScroll: endScrollPoint)
            minRangeInitialIndex = 0
            maxRangeInitialIndex = 0
        }
        
        private func scrollStartAndEndSlider(for startScrollPoint: CGPoint, andEndScroll endScrollPoint: CGPoint) {
            startSliderButton.isSelected = true
            sliderDidSlide(for: startScrollPoint)
            startSliderButton.isSelected = false
            
            endSliderButton.isSelected = true
            sliderDidSlide(for: endScrollPoint)
            endSliderButton.isSelected = false
        }
    }

    5. Tạo các hàm tính toán frame của slider

    extension TWSlider {
        private func getSliderViewWidth() -> CGFloat {
            let startMinX = startSliderButton.frame.midX
            let endMinX = endSliderButton.frame.midX
            if startMinX > endMinX {
                return startMinX - endMinX
            } else {
                return endMinX - startMinX
            }
        }
        
        private func sliderMidPoint(forPoint point: CGFloat) -> CGFloat {
            let sliderMidPoint = point - (SLIDER_BUTTON_WIDTH / 2)
            return sliderMidPoint
        }
        
        private func getSegmentWidth(forSegmentCount segmentCount: Int) -> CGFloat {
            let segmentCount = CGFloat(segmentCount - 1)
            let sliderWidth = frame.width - SLIDER_BUTTON_WIDTH
            return sliderWidth / segmentCount
        }
        
        private func getSegmentButton(withSegmentIndex segmentIndex: Int, isSlider: Bool) -> UIButton {
            // Create rounded button for representing slider segments
            let segmentButton = UIButton(type: .custom)
            segmentButton.frame = CGRect(x: 0, y: 0, width: SLIDER_BUTTON_WIDTH, height: SLIDER_BUTTON_WIDTH)
            segmentButton.center = getSegmentCenterPoint(forSegmentIndex: segmentIndex)
            setImageForSegmentOrSliderButton(segmentButton, isSlider: isSlider)
            return segmentButton
        }
        
        // MARK: - Calculation for segment button frame
        private func getSegmentCenterPoint(forSegmentIndex segmentIndex: Int) -> CGPoint {
            let pointX = CGFloat((CGFloat(segmentIndex - 1) * segmentWidth) + (SLIDER_BUTTON_WIDTH / 2))
            return CGPoint(x: pointX, y: sliderBackgroundView.frame.midY)
        }
        
        private func setImageForSegmentOrSliderButton(_ button: UIButton, isSlider: Bool) {
            if let image = rangeSliderButtonImage, isSlider {
                button.setImage(image, for: .normal)
            } else if let iamge = segmentSelectedImage {
                button.setImage(iamge, for: .normal)
            } else {
                button.setImage(getImageWithSize(isSlider ? sliderSize : segmentSize, with: segmentSelectedColor), for: .normal)
            }
            
            button.imageView?.layer.masksToBounds = true
            let buttonWidth = button.imageView?.frame.size.width ?? button.frame.size.width
            button.imageView?.layer.cornerRadius = buttonWidth / 2
        }
        
        private func getImageWithSize(_ size: CGSize, with backgroundColor: UIColor?) -> UIImage? {
            let imageView = UIView(frame: CGRect(x: 0, y: 0, width: size.width, height: size.height))
            imageView.backgroundColor = backgroundColor
            
            UIGraphicsBeginImageContextWithOptions(CGSize(width: size.width, height: size.height), false, 1.0)
            if let context = UIGraphicsGetCurrentContext() {
                imageView.layer.render(in: context)
            }
            let image = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext()
            return image
        }
    }

    6. Tạo các hàm để tính toán vị trí và xử lí delegate của Pangesture

    extension TWSlider {
        
        private func addPanGestureRecognizer() {
            let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture))
            panGesture.maximumNumberOfTouches = 1
            addGestureRecognizer(panGesture)
        }
        
        // Pan Gesture selector method
        
        @objc
        func handlePanGesture(_ panGesture: UIPanGestureRecognizer) {
            let point = panGesture.location(in: self)
            
            if panGesture.state == .began {
                self.setSelectedStateForSlidingButton(point)
            } else if panGesture.state == .changed {
                sliderDidSlide(for: point)
            } else if panGesture.state == .ended || panGesture.state == .failed || panGesture.state == .cancelled {
                // Move the slider to nearest segment
                moveSliderToNearestSegment(withEnding: point)
                resetSelectedStateForSlidingButtons()
            }
        }
        
        // If sliding began, check if startSlider is moved or endSlider is moved
        private  func setSelectedStateForSlidingButton(_ point: CGPoint) {
            if startSliderButton.frame.contains(point) {
                startSliderButton.isSelected = true
                endSliderButton.isSelected = false
            } else if endSliderButton.frame.contains(point) {
                endSliderButton.isSelected = true
                startSliderButton.isSelected = false
            } else {
                startSliderButton.isSelected = false
                endSliderButton.isSelected = false
            }
        }
        
        private func sliderDidSlide(for point: CGPoint) {
            var newPoint = point
            // Check if startButton is moved or endButton is moved. Based on the moved button, set the frame of the slider button and foregroundSliderView
            newPoint = resetFrameOnBoundsCross(for: point)
            
            if startSliderButton.isSelected {
                if shouldStartButtonSlide(for: newPoint) {
                    UIView.animate(withDuration: 0.1, animations: {
                        // Change only the x value for startbutton
                        self.startSliderButton.frame = CGRect(x: self.sliderMidPoint(forPoint: newPoint.x), y: self.startSliderButton.frame.origin.y, width: self.startSliderButton.frame.size.width, height: self.startSliderButton.frame.size.height)
                        
                        // Change the x and width for slider foreground view
                        if self.shouldSliderButtonOverlap {
                            let startMidX = self.startSliderButton.frame.midX
                            let endMidX = self.endSliderButton.frame.midX
                            var originX = self.startSliderButton.frame.origin.x
                            if endMidX < startMidX {
                                originX = self.endSliderButton.frame.origin.x
                            }
                            self.sliderForegroundView.frame = CGRect(x: originX + self.SLIDER_BUTTON_WIDTH / 2, y: self.sliderForegroundView.frame.origin.y, width: self.getSliderViewWidth(), height: self.sliderForegroundView.frame.size.height)
                        } else {
                            let originX = self.startSliderButton.frame.origin.x + self.SLIDER_BUTTON_WIDTH / 2
                            self.sliderForegroundView.frame = CGRect(x: originX, y: self.sliderForegroundView.frame.origin.y, width: self.getSliderViewWidth(), height: self.sliderForegroundView.frame.size.height)
                        }
                        
                        // Change the x and width for slider foreground view
                        self.minRangeView.center = CGPoint(x: self.startSliderButton.frame.midX, y: self.startSliderButton.frame.minY - self.paddingRangeView)
                        
                        self.sliderMidView.center = CGPoint(x: self.endSliderButton.frame.midX, y: self.startSliderButton.frame.minY - self.paddingRangeView)
                        
                        // Update the intermediate segment colors
                        self.updateSegmentColor(for: newPoint)
                    }, completion: { _ in
                        self.callScrollDelegate(point: newPoint, isStartSliderButton: true)
                    })
                }
            } else if endSliderButton.isSelected {
                if shouldEndButtonSlide(for: newPoint) {
                    UIView.animate(withDuration: 0.1, animations: {
                        // Change only the x value for endbutton
                        self.endSliderButton.frame = CGRect(x: self.sliderMidPoint(forPoint: newPoint.x), y: self.endSliderButton.frame.origin.y, width: self.endSliderButton.frame.size.width, height: self.endSliderButton.frame.size.height)
                        
                        // Change the x and width for slider foreground view
                        if self.shouldSliderButtonOverlap {
                            let startMidX = self.startSliderButton.frame.midX
                            let endMidX = self.endSliderButton.frame.midX
                            var originX = self.startSliderButton.frame.origin.x
                            if endMidX < startMidX {
                                originX = self.endSliderButton.frame.origin.x
                            }
                            self.sliderForegroundView.frame = CGRect(x: originX + self.SLIDER_BUTTON_WIDTH / 2, y: self.sliderForegroundView.frame.origin.y, width: self.getSliderViewWidth(), height: self.sliderForegroundView.frame.size.height)
                        } else {
                            let originX = self.startSliderButton.frame.origin.x + self.SLIDER_BUTTON_WIDTH / 2
                            self.sliderForegroundView.frame = CGRect(x: originX, y: self.sliderForegroundView.frame.origin.y, width: self.getSliderViewWidth(), height: self.sliderForegroundView.frame.size.height)
                        }
                        
                        // Change the x and width for slider foreground view
                        self.maxRangeView.center = CGPoint(x: self.endSliderButton.frame.midX, y: self.endSliderButton.frame.minY - self.paddingRangeView)
                        
                        self.sliderMidView.center = CGPoint(x: self.startSliderButton.frame.midX, y: self.endSliderButton.frame.minY - self.paddingRangeView)
                        
                        // Update the intermediate segment colors
                        self.updateSegmentColor(for: newPoint)
                    }, completion: { _ in
                        self.callScrollDelegate(point: newPoint, isStartSliderButton: false)
                    })
                }
            }
        }
        
        // Method that handles if the sliders move out of range
        private func resetFrameOnBoundsCross(for point: CGPoint) -> CGPoint {
            var newPoint = point
            if shouldSliderButtonOverlap {
                if point.x < 0 {
                    newPoint.x = 0
                } else if sliderMidPoint(forPoint: point.x) >= sliderBackgroundView.bounds.maxX {
                    newPoint.x = sliderBackgroundView.bounds.maxX + SLIDER_BUTTON_WIDTH / 2
                }
            } else {
                if startSliderButton.isSelected {
                    if sliderMidPoint(forPoint: point.x) >= endSliderButton.frame.midX - segmentWidth {
                        newPoint.x = endSliderButton.frame.midX - segmentWidth
                    } else if point.x < 0 {
                        newPoint.x = 0
                    }
                } else if endSliderButton.isSelected {
                    if point.x <= startSliderButton.frame.midX + segmentWidth {
                        newPoint.x = startSliderButton.frame.midX + segmentWidth
                    } else if sliderMidPoint(forPoint: point.x) >= sliderBackgroundView.bounds.maxX {
                        newPoint.x = sliderBackgroundView.bounds.maxX + SLIDER_BUTTON_WIDTH / 2
                    }
                }
            }
            
            return newPoint
        }
        
        private func shouldStartButtonSlide(for point: CGPoint) -> Bool {
            if shouldSliderButtonOverlap {
                return (point.x >= (SLIDER_BUTTON_WIDTH / 2)) && (point.x <= (bounds.maxX - SLIDER_BUTTON_WIDTH / 2))
            } else {
                var endButtonMidPoint = endSliderButton.frame.midX
                endButtonMidPoint -= segmentWidth
                return round(point.x) <= round(endButtonMidPoint) && point.x >= SLIDER_BUTTON_WIDTH / 2
            }
        }
        
        private func shouldEndButtonSlide(for point: CGPoint) -> Bool {
            if shouldSliderButtonOverlap {
                return point.x >= SLIDER_BUTTON_WIDTH / 2
            } else {
                var startButtonMidPoint = startSliderButton.frame.midX
                startButtonMidPoint += shouldSliderButtonOverlap ? 0 : segmentWidth
                return round(point.x) >= round(startButtonMidPoint) && point.x <= sliderMidPoint(forPoint: frame.size.width)
            }
        }
        
        // Call the delegate to set the label for min range and max range
        private func callScrollDelegate(point: CGPoint, isStartSliderButton: Bool) {
            let nearestSegmentIndex = Int(round(sliderMidPoint(forPoint: point.x) / segmentWidth))
            
            var startIndex = Int(round(startSliderButton.frame.minX / segmentWidth))
            var endIndex = Int(round(endSliderButton.frame.minX / segmentWidth))
            
            if isStartSliderButton {
                startIndex = nearestSegmentIndex
            } else {
                endIndex = nearestSegmentIndex
            }
            updateData(startIndex: startIndex, endIndex: endIndex, endDragDrop: false)
        }
        
        private func updateData(startIndex: Int, endIndex: Int, endDragDrop: Bool) {
            if startIndex > endIndex {
                let min = endIndex < 0 ? 0 : endIndex
                let max = startIndex > numberOfSegments ? numberOfSegments : startIndex
                delegate?.sliderScrolled(self, toMinIndex: min, andMaxIndex: max, endDragDrop: endDragDrop)
                minRangeLabel.text = maxRangeText
                maxRangeLabel.text = minRangeText
            } else {
                let min = startIndex < 0 ? 0 : startIndex
                let max = endIndex > numberOfSegments ? numberOfSegments : endIndex
                delegate?.sliderScrolled(self, toMinIndex: min, andMaxIndex: max, endDragDrop: endDragDrop)
                minRangeLabel.text = minRangeText
                maxRangeLabel.text = maxRangeText
            }
            
            if startIndex != endIndex {
                UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseIn) {
                    self.sliderMidView.alpha = 0
                    self.minRangeView.alpha = 1
                    self.maxRangeView.alpha = 1
                } completion: { _ in
                    print("completion \(startIndex) = \(endIndex)")
                }
            } else {
                sliderMidLabel.text = "\(minRangeText ?? "") - \(maxRangeText ?? "")"
                UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseIn) {
                    self.sliderMidView.alpha = 1
                    self.minRangeView.alpha = 0
                    self.maxRangeView.alpha = 0
                } completion: { _ in
                    print("completion \(startIndex) = \(endIndex)")
                }
            }
        }
        
        private func updateSegmentColor(for point: CGPoint) {
            
            if shouldSliderButtonOverlap {
                let startMinX = startSliderButton.frame.midX
                let endMinX = endSliderButton.frame.midX
                
                var min: CGFloat = 0.0
                var max: CGFloat = 0.0
                if startMinX > endMinX {
                    min = (endSliderButton.frame.minX / segmentWidth).rounded(.up)
                    max = (startSliderButton.frame.minX / segmentWidth).rounded(.down)
                } else {
                    min = (startSliderButton.frame.minX / segmentWidth).rounded(.up)
                    max = (endSliderButton.frame.minX / segmentWidth).rounded(.down)
                }
                updateSegmentColor(withStart: Int(min) + 1, andEnd: Int(max) + 1)
            } else {
                if startSliderButton.isSelected {
                    let startMinX = (sliderMidPoint(forPoint: startSliderButton.frame.midX) / segmentWidth).rounded(.up)
                    let endMinX = (sliderMidPoint(forPoint: endSliderButton.frame.midX) / segmentWidth).rounded(.up)
                    updateSegmentColor(withStart: Int(startMinX) + 1, andEnd: Int(endMinX) + 1)
                } else if endSliderButton.isSelected {
                    let startMinX = (startSliderButton.frame.minX / segmentWidth).rounded(.up)
                    let endMinX = (endSliderButton.frame.minX / segmentWidth).rounded(.down)
                    updateSegmentColor(withStart: Int(startMinX) + 1, andEnd: Int(endMinX) + 1)
                }
            }
            let pointX: Int = Int(round(point.x - SLIDER_BUTTON_WIDTH / 2))
            if pointX % Int(round(segmentWidth)) == 0 {
                let generator = UIImpactFeedbackGenerator(style: .medium)
                generator.impactOccurred()
            }
        }
        
        private func updateSegmentColor(withStart startIndex: Int, andEnd endIndex: Int) {
            // Segments before startSegment slider
            
            if startIndex > 1 {
                for segmentIndex in 1..<startIndex {
                    if let segmentButton = viewWithTag(segmentIndex) as? UIButton {
                        if let image = segmentUnSelectedImage {
                            segmentButton.setImage(image, for: .normal)
                        } else {
                            let image = getImageWithSize(segmentSize, with: segmentUnSelectedColor)
                            segmentButton.setImage(image, for: .normal)
                        }
                    }
                }
            }
            
            // Segments between startSegment slider and endSegment slider
            if startIndex <= endIndex {
                for segmentIndex in startIndex...endIndex {
                    if let segmentButton = viewWithTag(segmentIndex) as? UIButton {
                        if let image = segmentSelectedImage {
                            segmentButton.setImage(image, for: .normal)
                        } else {
                            let image = getImageWithSize(segmentSize, with: segmentSelectedColor)
                            segmentButton.setImage(image, for: .normal)
                        }
                    }
                }
            }
            
            // Segments after endSegment slider
            if endIndex + 1 <= numberOfSegments {
                for segmentIndex in (endIndex + 1)...numberOfSegments {
                    if let segmentButton = viewWithTag(segmentIndex) as? UIButton {
                        if let image = segmentUnSelectedImage {
                            segmentButton.setImage(image, for: .normal)
                        } else {
                            let image = getImageWithSize(segmentSize, with: segmentUnSelectedColor)
                            segmentButton.setImage(image, for: .normal)
                        }
                    }
                }
            }
        }
        
        // Slide to nearest position
        private func moveSliderToNearestSegment(withEnding point: CGPoint) {
            var newPoint = point
            newPoint = resetFrameOnBoundsCross(for: point)
            
            let nearestSegmentIndex = Int(round(sliderMidPoint(forPoint: newPoint.x) / segmentWidth))
            sliderDidSlide(for: CGPoint(x: CGFloat(SLIDER_BUTTON_WIDTH / 2 + CGFloat(nearestSegmentIndex) * segmentWidth), y: newPoint.y))
            
            var startIndex = Int(round(startSliderButton.frame.minX / segmentWidth))
            var endIndex = Int(round(endSliderButton.frame.minX / segmentWidth))
            
            if startSliderButton.isSelected {
                startIndex = nearestSegmentIndex
            } else if endSliderButton.isSelected {
                endIndex = nearestSegmentIndex
            }
            updateData(startIndex: startIndex, endIndex: endIndex, endDragDrop: true)
        }
        
        // After ending, reset the selected state of both buttons
        private func resetSelectedStateForSlidingButtons() {
            startSliderButton.isSelected = false
            endSliderButton.isSelected = false
        }
    }

    5. Tạo các hàm public cho phép thay đổi giá trị của TWSlider

    extension TWSlider {
        
        // Scroll to desired location on loading
        func scrollStartSlider(to startIndex: Int, andEnd endIndex: Int) {
            minRangeInitialIndex = startIndex
            maxRangeInitialIndex = endIndex
            if checkInit {
                slideRangeSliderButtonsIfNeeded()
            }
        }
        
        // MARK: - Setter methods, setup value for slider
        func setNumberOfSegments(_ numberOfSegments: Int, minText: String?, maxText: String?) {
            self.numberOfSegments = numberOfSegments
            
            // After setting the numberOfSegments, set all the necessary views
            segmentWidth = getSegmentWidth(forSegmentCount: self.numberOfSegments)
            addSegmentButtons()
            addSubview(startSliderButton)
            addSubview(endSliderButton)
            minLabel.text = minText
            maxLabel.text = maxText
        }
        
        private func addSegmentButtons() {
            for segmentIndex in 1...numberOfSegments {
                let segmentButton = getSegmentButton(withSegmentIndex: segmentIndex, isSlider: false)
                segmentButton.then {
                    $0.tag = segmentIndex
                    $0.isUserInteractionEnabled = false
                    addSubview($0)
                }
            }
        }
    }

    3. Cách sử dụng

    Bước 1: mở file storyboard hoặc file .xib và thêm UIView và constraint, sửa lại tên class thành class custom slider mới tạo ở trên ở project này là TWSlider -> Kéo outlet

    Bước 2: mở viewcontroller vừa được kéo outlet viết hàm setup cho slider như sau

    class ViewController: UIViewController {
    
        @IBOutlet weak var customSlider: TWSlider!
        private let data: [String] = ["10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20"]
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
            customSlider.then {
                $0.shouldSliderButtonOverlap = true
                $0.delegate = self
            }
            customSlider.then {
                $0.setNumberOfSegments(data.count, minText: data.first, maxText: data.last)
                $0.scrollStartSlider(to: 0 + 1, andEnd: 10 + 1)
            }
        }
    }
    
    extension ViewController: TWSliderDelegate {
        func sliderScrolled(_ slider: TWSlider?, toMinIndex minIndex: Int, andMaxIndex maxIndex: Int, endDragDrop: Bool) {
            customSlider.minRangeText = data[minIndex]
            customSlider.maxRangeText = data[maxIndex]
        }
    }

    Bạn có thể tuỳ chỉnh số hiển thị trên slider bằng các số tương ứng với project.

    Mình để project ở đây cho mọi người tham khảo nếu cần nhé!

    Vậy là chúng ta đã hoàn thành việc custom Slider hai chiều, mình hi vọng nó có thể giải quyết được bài toán của các bạn. Cảm ơn các bạn đã theo dõi bài viết!

  • Regex là gì? Ứng dụng Regex để kiểm tra dữ liệu trong Swift

    Regex là gì? Ứng dụng Regex để kiểm tra dữ liệu trong Swift

    Như mọi người đã biết, tất cả các ứng dụng hiện nay đều có những ô nhập (TextField, TextView …) để hỗ trợ người dùng điền thông tin của họ và gửi về phía server để xử lí. Khi này để giảm tải công việc cho server chúng ta cần phải kiểm tra dữ liệu để loại bỏ bớt các trường hợp dữ liệu không đúng trước khi gửi lên server. Để làm việc này thì thông thường chúng ta sẽ nghĩ ngay đến Regex. Vậy Regex là gì? Áp dụng Regex vào source code Swift như nào? mời các bạn theo dõi tiếp bài viết của mình nhé.

    Regex là gì?

    Regex là viết tắt của Regular Expression, tiếng việt được gọi là Biểu Thức Chính Quy. Regex là một công cụ rất mạnh trong việc xử lí chuỗi, nó thường được dùng để kiểm tra tính hợp lệ của một chuỗi hay tìm kiếm, vì vậy chúng ta nên trang bị cho mình một số kiến thức cơ bản về Regex để có thể xử lí công việc nhẹ nhàng và nhàn hạ hơn.

    Nhược điểm của Regex

    Công nhận regex là một công cụ rất mạnh trong việc xử lí chuỗi, tuy nhiên nó cũng có một số nhược điểm như sau:

    Khó đọc

    Nhìn vào đoạn Regex: ^([A-Za-z0-9!@#$%^&*?]{8,})$

    Khi mới bắt đầu chúng ta sẽ rất khó để biết được regex này đang làm nhiệm vụ gì. Vì nó được mô tả bới các kí tự không quen thuộc với ngôn ngữ chúng ta hay dùng.

    Dễ quên

    Do nó khó đọc, khó hiểu nên nó cũng khiến chúng ta dễ quên, vì vậy mỗi lần cần đến 1 regex nào là chúng ta thường phải tìm đến google để trợ giúp. Để khắc phục nhược điểm này mỗi khi chúng ta làm việc mới một regex khó hoặc dài thì chúng ta sẽ lưu lại vào 1 file lưu trữ, để sau này dùng đến tìm lại cho nhanh.

    Regex dùng để làm gì?

    Regex thường được dùng để tìm kiếm, để tách chuỗi, để kiểm tra chuỗi, …

    1. Dùng để kiểm tra chuỗi

    Ví dụ ta cần kiểm tra độ phức tạp của mật khẩu với yêu cầu như sau:

    • Phải có ít nhất một kí tự viết thường
      Regex: [a-z]: Sẽ khớp với bất kì kí tự viết thường nào trong chuỗi(a-z)
      Input: AAA -> FALSE
      Input: aabc
      hoặc Aza -> TRUE
    • Phải có ít nhất một kí tự viết hoa
      Regex: [A-Z] : Sẽ khớp với bất kì kí tự viết hoa nào trong chuỗi(A-Z)
      Input: AAA hoặc Abc -> TRUE
      Input bbca -> FALSE
    • Phải có ít nhất một kí tự số
      Regex: [0-9] : Sẽ khớp với bất kì kí tự số nào trong chuỗi(0-9)
      Input: 1234 hoặc A0bc -> TRUE
      Input bbca -> FALSE
    • Phải có ít nhất một trong số kí tự đặc biệt sau @!#$
      Regex: [@!#$] : Sẽ khớp với bất kì kí tự số nào trong chuỗi(@!#$)
      Input: 12@4 hoặc A!bc -> TRUE
      Input bbca -> FALSE
    • Không được có dấu cách
      Regex: [ ] : Sẽ khớp với bất kì kí tự nào trong chuỗi là space
      Input: 12 4 hoặc A bc -> TRUE
      Input bbca -> FALSE

    2. Dùng để tách chuỗi

    Ví dụ chúng ta có một chuỗi kí tự như sau: ABCxyz123mM

    • Tách lấy hết các kí tự viết hoa từ chuỗi trên ta sử dụng regex: [A-Z], ta sẽ thu được kết quả từ chuỗi ABCxyz123mM -> ABCM
    • Tách lấy hết kí tự viết thường ta dùng regex: [a-z], ta thu được kết quả như sau: ABCxyz123mM -> abcm
    • Tách lấy hết kí tự số ta thường dùng regex: [0-9], Ta thu được kết quả như sau: ABCxyz123mM -> 123

    3. Dùng để tìm kiếm

    Regex khá là phổ biến, nó có mặt trên hầu hết các IDE hiện nay. xCode, Android studio, Subline text, note pad ++, …

    Ví dụ phần mềm Subline text

    Để kích hoạt tính năng này các bạn bấm command + f (tổ hợp tìm kiếm) và chọn mục regex (.*)

    Ảnh dưới đây mình có viết 1 đoạn regex: ^N.*o$ để tìm ra chuỗi bắt đầu bằng kí tự N và có kết thúc bằng kí tự o

    Giải thích ý nghĩa các kí tự để viết Regex

    Sau đây mình sẽ giải thích kĩ hơn về các kí tự để viết regex và cách sử dụng chúng.

    1.Kí tự thường

    Mã RegexMô tảGhi chú
    a|bKhớp với a hoặc b
    [0-9]Khớp với các kí tự là số 0,1,2..9
    [a-z]Khớp với các kí tự viết thường từ a tới zNếu đổi z thành c thì sẽ là từ a tới c
    [ABC]Khớp với các kí tự là ABC
    [^ABC]Khớp với các kí tự không phải là ABCNếu dấu ^ xuất hiện phía trong [] có nghĩa nó là
    Phủ định của tập hợp đó
    \dKhớp với số bất kìthay thế cho [0-9]
    \DKhớp với các kí tự không phải sốPhủ định của \d
    \sKhớp với tất cả kí tự là khoảng trằng, tab, hoặc xuống dòng
    \SKhớp với tất cả các kí tự không phải là khoảng trắng, tab hoặc xuống dòngPhủ định của \s
    \S+Khớp với một hoặc nhiều kí tự không phải là khoảng trắng, tab hoặc xuống dòng
    \wKhớp với bất kì ký tự chữThay thế cho [a-zA-Z0-9]
    \WKhớp với kí tự bất kì không phải chữphủ định của \w
    \bKhớp khi kí tự trước đó nằm ở cuối chuỗiregex: dao\b
    minhdao: true
    daominh: false
    \BPhủ định của \b

    2. Kí tự đặc biệt

    RegexMô tảGhi chú
    .Khớp với tất cả các kí tự trừ kí tự xuống dòngThay thế cho [^\n\r]
    ^Bắt đầuregex: ^dao
    daominh: True
    minhdao: False
    $Kết thúcregex: dao$
    daominh: False
    minhdao: True
    |Điều kiện hoặca|b
    a: true
    b: true
    c: false
    \Biến 1 kí tự đặc biệt thành kí tự thường hoặc ngược lạid: kí tự d thông thường
    \d: khớp với kí tự số bất kì

    3. Lặp

    RegexMô tảGhi chú
    *Xuất hiện 0 hoặc nhiều lầnTương đương {0,}
    +Xuất hiện 1 hoặc nhiều lầnTương đương {1,}
    ?Xuất hiện 0 hoặc 1 lầnTương đương {0,1}
    {x,y}Xuất hiện từ x lần tới y lần{3}: xuất hiện đúng 3 lần
    {3,}: xuất hiện từ 3 lần hoặc nhiều hơn
    {3, 10} Xuất hiện từ 3 lần đến 10 lần

    4. Nhóm

    RegexMô tảGhi chú
    ()Nhóm nhiều mã lại với nhau tạo thành nhóm điều kiện
    (?:x)Khớp với x nhưng không nhớ kết quả khớp
    x(?=y)Chỉ khớp x nếu ngay sau x là y
    x(?!y)chỉ khớp x nếu ngay sau x không phải y

    Ứng dụng regex vào việc kiểm tra dữ liệu UITextField bằng ngôn ngữ swift

    Để ứng dụng được regex vào việc kiểm tra dữ liệu ở swift chúng ta cần biết một chút kiến thức cơ bản về regex(mình đã giới thiệu ở phía trên) và hiểu rõ về cách hoạt động của TextField

    Các hàm common cần dùng

    extension String {
        
        /// check string is match regex or not
        /// - Parameter regex: regular expression
        /// - Returns: true if match
        func isMatches(_ regex: String) -> Bool {
            do {
                let regex = try NSRegularExpression(pattern: regex)
    
                let matches = regex.matches(in: self, range: NSRange(location: 0, length: self.count))
                return !matches.isEmpty
            } catch {
                print("Something went wrong! Error: \(error.localizedDescription)")
            }
    
            return false
        }
        
        /// Return string matchs regex
        /// - Parameter regex: regular expression
        /// - Returns: all string match regex
        func filter(regex: String) -> String {
            
            do {
                let regex = try NSRegularExpression(pattern: regex)
    
                let results = regex.matches(in: self, range: NSRange(self.startIndex..., in: self))
                
                return results.map { String(self[Range($0.range, in: self)!]) }.joined(separator: "")
            } catch {
                print("Something went wrong! Error: \(error.localizedDescription)")
            }
            
            return ""
        }
        
        /// Remove mark in string
        var folded: String {
            self.folding(options: .diacriticInsensitive, locale: nil)
                .replacingOccurrences(of: "đ", with: "d")
                .replacingOccurrences(of: "Đ", with: "D")
        }
    }
    • func isMatches(_ regex: String) -> Bool: hàm này dùng để kiểm tra xem chuỗi có khớp với regex hay không?
    • func filter(regex: String) -> String: Hàm này dùng để lấy ra tất cả các kí tự thoả mãn điều kiện của regex
    • var folded: String : giúp biến đổi các kí tự có dấu về không dấu VD: Đ -> D, ê -> e …
    extension UITextField {
        
        /// validate input data
        /// - Parameters:
        ///   - maxLength: max char of text field
        ///   - range: location and lenth of current selected text
        ///   - string: new string will be replacce at range
        func validateInput(maxLength: Int, range: NSRange, string: String) {
            guard let textFieldText = self.text else {
                return
            }
            
            if self.text.safeValue.count == maxLength, !string.isEmpty {
                return
            }
            
            let text: NSString = textFieldText as NSString
            let finalString: String = text.replacingCharacters(in: range, with: string)
            let newString = String(finalString.prefix(min(maxLength, finalString.count)))
            
            self.text = newString
            let countChange: Int = newString.count - textFieldText.count
            let validateCount: Int = countChange > 0 ? countChange : 0
            self.setCursorPosition(range.location + validateCount)
        }
        
        /// Set Cursor position
        /// - Parameter cursorPosition: Int
        func setCursorPosition(_ cursorPosition: Int) {
            guard cursorPosition <= (self.text ?? "").count,
                  let posCursor = self.position(from: beginningOfDocument, offset: cursorPosition) else { return }
            DispatchQueue.main.async {
                self.selectedTextRange = self.textRange(from: posCursor, to: posCursor)
            }
        }
    }
    • func validateInput(maxLength: Int, range: NSRange, string: String): Dùng khi cần tự validate các kí tự cho phép, hoặc chặn, hoặc copy paste chuỗi, rồi set lại giá trị cho text field.
    • func setCursorPosition(_ cursorPosition: Int): Dùng để di chuyển con trỏ khi copy paste.

    Hiểu rõ các func trong UITextField delegate hoạt động

    Ở đây mình chỉ nói đến các delegate hay dùng làm nhiệm vụ kiểm tra dữ liệu

    1. func textFieldDidEndEditing(_ textField: UITextField): Dùng để kiểm tra dữ liệu khi người dùng focus out, thường được dùng khi muốn kiểm tra dữ liệu tại thời điểm người dùng kết thúc thao tác nhập dữ liệu.
    2. func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool : thường được dùng khi muốn kiểm tra dữ liệu tại thời điểm người dùng nhập(realtime), dùng để chặn những kí tự không cho phép.
      – textField: đây là text field đang được người dùng tương tác
      – range: Vị trí và độ dài mã text sắp được thay đổi
      – chuỗi được thay thế tại range
      NOTE: Khi replacement string = empty có nghĩa là người dùng đang thực hiện thao tác xoá hoặc dùng gợi ý nội dung hoặc keychain của bàn phím.
    3. event Editting Changed: Dùng khi không chặn kí tự ở hàm 2(shouldChangeCharacterIn), event này chạy khi có thay đổi kí tự trong text field, với điều kiện shouldCHangeCharacter phải return true

    Sử dụng regex để kiểm tra và lọc realtime kí tự nhập vào UITextField

    Từ các common code phía trên để kiểm tra chuỗi mà người dùng nhập vào chúng ta sẽ thực hiện nó trong hàm shouldChangeCharacter in range

    Ví dụ: Text field chỉ cho nhập các kí tự chữ hoa chữ thường và không cho nhập tiếng việt

    Ta sẽ làm như sau:

    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
            // case clear/use suggestion content type/ return/
            if string.count == 0 {
                return true
            }
            
            textField.validateInput(maxLength: 20, range: range, string: string.filterWithRegex(regex: "[a-zA-Z]"))
            validatePassword(text: textField.text.safeValue)
            return false
        }

    Nếu muốn khi copy paste một chuỗi có dấu tự convert về chuỗi không dấu ta làm như sau:
    Sử dụng folded để convert chuỗi về chuỗi không dấu trước khi filter nó với regex

    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
            // case clear/use suggestion content type/ return/
            if string.count == 0 {
                return true
            }
            
            textField.validateInput(maxLength: 20, range: range, string: string.folded.filterWithRegex(regex: "[a-zA-Z]"))
            validatePassword(text: textField.text.safeValue)
            return false
        }

    Tương tự với từng yêu cầu chúng ta sẽ tạo ra các regex khác nhau và đưa vào sử dụng một cách dễ dàng và hiệu quả

    Sử dụng Regex để kiểm tra khi người dùng kết thúc nhập liệu

    Nếu bạn muốn kiểm tra chuỗi người dùng vừa xong có đúng định dạng email hay không thì ta sử dụng hàm textFieldDidEndEditing khi người dùng kết thúc nhập để kiểm tra như sau:

        func textFieldDidEndEditing(_ textField: UITextField) {
            guard let textFieldText = textField.text else {
                return
            }
            // check email format
            if textFieldText.isMatches("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$") {
                // hide error
            } else {
                //show error
            }
        }

    Để tránh sai sót khi sử dụng các regex ở nhiều chỗ khác nhau trong project, chúng ta nên tạo ra enum để quản lí các chuỗi regex này như sau:

    enum Regex: String {
        case none = "[\\s\\S]"
        case min8CharNoSpace = "^([A-Za-z0-9!@#$%^&*?]{8,})$"
        case haveCharAndNumber = "(?=.*[a-zA-Z])(?=.*[0-9])"
        case haveUpercaseAndLowerCase = "(?=.*[a-z])(?=.*[A-Z])"
        case haveSpecialChar = "[!@#$%^&*?]"
        case vietnamese = "^[a-zA-Z0-9ÀÁÂÃÈÉÊÌÍÒÓÔÕÙÚĂĐĨŨƠàáâãèéêìíòóôõùúăđĩũơƯĂẠẢẤẦẨẪẬẮẰẲẴẶẸẺẼỀỀỂẾưăạảấầẩẫậắằẳẵặẹẻẽềềểếỄỆỈỊỌỎỐỒỔỖỘỚỜỞỠỢỤỦỨỪễệỉịọỏốồổỗộớờởỡợụủứừỬỮỰỲỴÝỶỸửữựỳỵỷỹ\\s]+$"
        case email = "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$"
        case emailInput = "[a-zA-Z0-9.@]"
        case password = "[a-zA-Z0-9!@#$%^&*?]"
        
        var maxLength: Int {
            switch self {
            case .none:
                return .max
            case .min8CharNoSpace:
                return 100
            case .haveCharAndNumber:
                return 100
            case .haveUpercaseAndLowerCase:
                return 100
            case .haveSpecialChar:
                return 100
            case .vietnamese:
                return 100
            case .email:
                return 100
            case .emailInput:
                return 100
            case .password:
                return 30
            }
        }
    }

    Mình hi vọng bài viết này giúp mọi người hiểu rõ hơn về regex và cách ứng dụng nó vào trong công việc. Chúc các bạn thành công!