Tag: Swift

  • iOS/Swift/Localization: Cách tách Localizable.strings thành nhiều file theo từng màn hình

    iOS/Swift/Localization: Cách tách Localizable.strings thành nhiều file theo từng màn hình

    Xin chào, trong số chúng ta hẳn không ít developer đã từng gặp tình trạng phải ngồi sửa các conflict file localizable strings khi làm việc, nhất là khi dự án của bạn có nhiều người tham gia lúc này file localizable.strings sẽ như là một đấu trường hỗn loạn với rất nhiều developer xâu xé, thi nhau chiếm đất. Người này sửa lên text người kia, người này sửa bug của mình lại tạo thành các bug của người khác. Khi dự án có thêm nhiều tính năng, file Localizable.strings sẽ càng ngày càng phình to ra, khi nó quá lớn thì một số máy MAC của anh em sẽ gặp tình trạng lag, chậm khi sửa file này.

    Câu hỏi đặt ra là để giải quyết bài toán này thì chúng ta cần phải làm gì?

    Chia để trị là cách mình giải quyết vấn đề này.


    Nếu bạn chưa làm ứng dụng có tính năng đa ngôn ngữ bao giờ thì quay lại đọc bài của mình trước rồi quay lại nhé!
    HƯỚNG DẪN THỰC HIỆN ỨNG DỤNG MOBILE HỖ TRỢ ĐA NGÔN NGỮ

    Ưu điểm của việc tách file Localizable.strings theo từng màn hình

    • Mỗi màn hình có một file .strings quản lý riêng vì vậy không bị conflict khi merge code
    • Không bị bug khi fix string màn hình A lại lỗi màn hình B, C, D …
    • Dễ dàng quản lý, dễ dàng maintain
    • Không bị vì lỗi format trên file Localizable.strings mà cả ứng dụng không chạy được đa ngôn ngữ

    Ý tưởng

    Theo cách kiến trúc lập trình phần mềm, chúng ta luôn tìm cách phân tách các thành phần riêng để xử lí các tác vụ, và giảm tải cho các phần càng ngày càng lớn lên khi ứng dụng có nhiều tính năng hơn. Vậy tại sao chúng ta lại để file Localization của mình đến hàng nghìn dòng đến vạn dòng. Không thể chịu được cảnh mở file localizable giật lag, cuộn mỏi tay. Mình đã suy nghĩ đến giải pháp chia ra để trị, mỗi màn hình sẽ có một file .strings riêng để thực hiện Localization.

    Cách thực hiện

    Từ rất lâu Apple đã cung cấp cho chúng ta cách thức để làm được việc này.

    Nếu bạn muốn thực hành luôn thì có thể tài project bắt đầu ở đây nhé.

    Bước 1: Tạo 2 màn hình tương ứng với 2 tính năng của ứng dụng.

    Bước 2: Tạo 2 file .strings tương ứng với 2 màn hình là FirstScreen.strings và SecondScreen.strings như hình dưới, bạn copy nội dung từ File Localizable.strings rồi xoá file đi như hình.

    Bước 3: Sửa lại extension String file LanguageManager.swift như sau

    extension String {
        var localized: String {
            // default language of device
            let currentLanguage = LanguageManager.shared.getAppLanguage().rawValue
            guard let bundlePath = Bundle.main.path(forResource: currentLanguage, ofType: "lproj"),
                  let bundle = Bundle(path: bundlePath),
                  let tableName = self.split(separator: ".").first// lấy table name từ string
            else {
                return self
            }
            
            return NSLocalizedString(self, tableName: String(tableName), bundle: bundle, value: "", comment: "")
        }
    }

    Để sử dụng chúng ta code như sau:

    lbHeaderTitle.text = Localization.SecondScreen.title.localized

    NOTE: Để hàm này hoạt động đúng thì tất cả thành viên trong dự án sẽ phải tuân thủ cách đặt tên nằm trong .strings.
    Ví Dụ: Feature1.strings, thì nội dung bên trong phải là Feature1.xxxx, xxxx ở đây là key string của bạn.

    Nếu bạn cảm thấy việc tuân thủ luật này quá cứng nhắc dễ mắc sai lầm thì chúng ta có một hàm mới như sau:

    // Định nghĩa các table Name, lưu ý đặt tên phải trùng với tên file .strings
    enum LocalizationTableName: String {
        case firstScreen = "FirstScreen"//FirstScreen.strings
        case secondScreen = "SecondScreen"//SecondScreen.strings
    }
    
    extension String {
        func localized(tableName: LocalizationTableName) -> String {
            // default language of device
            let currentLanguage = LanguageManager.shared.getAppLanguage().rawValue
            guard let bundlePath = Bundle.main.path(forResource: currentLanguage, ofType: "lproj"),
                  let bundle = Bundle(path: bundlePath)
            else {
                return self
            }
            
            return NSLocalizedString(self, tableName: tableName.rawValue, bundle: bundle, value: "", comment: "")
        }
    }

    Khi sử dụng chúng ta sẽ code như sau:

    lbHeaderTitle.text = Localization.SecondScreen.title.localized(tableName: .secondScreen)

    Vậy là chúng ta đã thành công tạo localization cho từng màn hình. Mình hi vọng bài viết sẽ giúp ích cho các bạn ở các dự án sau này.

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

  • Localization: Tối ưu hoá quá trình khi làm ứng hỗ trợ đa ngôn ngữ

    Localization: Tối ưu hoá quá trình khi làm ứng hỗ trợ đa ngôn ngữ

    Như mọi người đã biết, hầu hết các ứng dụng ngày nay đều hỗ trợ tính năng đa ngôn ngữ, nhằm mục đích tiếp cận được nhiều người dùng hơn, cho mọi người sử dụng ứng dụng dễ dàng hơn. Tuy nhiên hiện nay hầu hết các dự án đều làm một cách tự phát, chưa có một quy trình chuẩn. Điều này làm cho quá trình phát triển sinh ra nhiều bug UI sai Message, sai chính tả, nó làm tốn thời gian không đáng có của các bên.

    Trong quá trình làm việc ở các dự án, khi tài liệu được cập nhật đồng nghĩa với việc các Dev phải quay lại kiểm tra các thay đổi message, rồi bắt đầu loay hoay tìm kiếm trong source code để thực hiện thay đổi.

    Hay mỗi khi code một màn hình mới việc phải định nghĩa một đống key, định nghĩa enum/constant và copy/paste nó sang các file localization thật nhàm chán và tiềm tàng nhiều rủi ro về lỗi sai chính tả, copy nhầm. Trước đây, có một member trong dự án của mình chỉ vì copy sai text làm thừa một dấu “;” làm lỗi cả file đa ngôn ngữ, việc này làm cho toàn bộ text trên ứng dụng khi release cho bị sai. Do file đa ngôn ngữ là string nên những lỗi sai chính tả như này mất rất nhiều thời gian để điều tra lỗi.

    Sau nhiều năm làm việc với các dự án khác nhau, cùng các anh em, cộng sự hoàn thành các ứng dụng to nhỏ khác nhau. Mình đã đúc kết được một quy trình giúp cải thiện năng suất làm việc của mọi người, giúp giảm các bug UI về sai message, sai chính tả, giảm thời gian thực hiện tính năng đa ngôn ngữ, giúp quá trình bảo trì, thay đổi message trở nên dễ dàng hơn.

    Quy trình

    Brainstoming

    Trước tiên, để đạt được hiệu quả Leader của các bên gồm BA, Mobile, Web … ngồi lại với nhau để thống nhất về cách làm việc theo quy trình trên. Sau đó phổ biến lại cho member và đảm bảo việc member thực hiện đúng quy trình làm việc.

    Tạo file excel chung lưu các message cần làm đa ngôn ngữ

    File message template các bạn tải ở đây nhé:

    Trong file này mình có định nghĩa sẵn các trường cần nhập và có sẵn công thức để gen ra code của swift và android. Nếu dự án của các bạn sử dụng ngôn ngữ khác thì có thể chỉnh lại công thức cho phù hợp với dự án của mình.

    Nhập nội dung

    Thêm/sửa message

    Screen/Feature: Điền vào tiên màn hình hoặc tính năng.

    MessageKey: Điền vào tên item trên màn hình, nếu SRS có mô tả thì ưu tiên sử dụng key trong SRS, nếu không hãy đặt tên sao cho đúng ý nghĩa của item.

    Tiếng việt: Text được hiển thị khi ngôn ngữ là Tiếng Việt

    Tiếng anh: Text được hiển thị khi ngôn ngữ là Tiếng Anh

    Note:
    TH1. Tài liệu đã định nghĩa sẵn các message cho các ngôn ngữ: thì BA có thể cử 1 bạn ra điền hết vào file này trước khi bên dev thực hiện code. Trường hợp này thì việc làm đa ngôn ngữ khá nhàn, mình sẽ giải thích chi tiết ở các phần phía dưới.
    TH2: Tài liệu chưa định nghĩa message cho các ngôn ngữ(các dự án thường rơi vào trường hợp này): Khi này khi các Dev thực hiện code sẽ tự insert thêm message vào file trên teams, để file tự tạo ra code tương ứng với các ngôn ngữ.

    Công thức tạo code tự động

    Công thức tạo code từ excel

    Các Developers sẽ không phải làm các thao tác lặp đi lặp lại nhàm chán. Ngoài ra nó giúp giảm lỗi đánh máy, lỗi copy paste, lỗi sai chính tả và giúp việc làm đa ngôn ngữ nhanh hơn. Trên file excel mình có tạo ra công thức để tự tạo ra code của swift/android. Khi bạn điền thông tin vào sheet MessageList thì code sẽ được gen tự động, việc của mọi người là chỉ cần copy code vào project và sử dụng.

    Đưa ra các luật lệ

    Tạo ra các luật yêu cầu member phải tuân thủ như sau:

    1. Đặt tên màn hình theo quy định của dự án, cách đặt tên cần thống nhất giữa tài liệu SRS và các class trong code của Dev.
    2. Dev của các bên(mobile, web) và BA khi thực hiện thêm mới, hay chỉnh sửa sẽ thực hiện trực tiếp trên files Localize chung của dự án, khi chỉnh sửa cần tìm đúng tên màn hình, message để sửa, nếu thêm mới thì thêm xuống cuối cùng của của danh sách list message của màn hình đó.
    3. Để tránh việc conflict hoặc giẫm chân nhau khi các bên chạy song song, thì mỗi màn hình những member liên quan tới màn hình đó sẽ cử ra một bạn điển vào file message trước.

    Cách thực hiện code

    TH1: đối với dự án không cho sử dụng thư viện mã nguồn mở

    Khi thực hiện chúng ta chỉ cần vào file excel và copy code vào project của mình rồi sử dụng bình thường.

    TH2: Dự án cho phép sử dụng thư viện mã nguồn mở

    Chúng ta sẽ sử dụng thư viện để tự tạo ra enum nhanh gọn lẹ hơn, ví dụ bên iOS Swift thì chúng ta có thể sử dụng Swiftgen để thực hiện. Link hướng dẫn sử dụng Swiftgen mình cũng có viết môt bài rồi các bạn có thể tham khảo link dưới đây:

    Ưu điểm

    • Giảm thời gian làm việc, tăng năng suất làm việc của team
    • Giảm khả năng bị lỗi UI/Message hay các lỗi về copy/paste khi thực hiện
    • Giảm thời gian bảo trì ứng dụng khi thay đổi message, khi có thay đổi BA sẽ update file message list và báo lại cho Devs, lúc này Dev không cần phải tìm xem thay đổi ở đâu, mà chỉ cần copy code từ file message list là xong.
    • Giảm thời gian implement khi có yêu cầu hỗ trợ thêm ngôn ngữ khác. Vì khi này chúng ta chỉ cần thêm 1 cột nữa ở file excel rồi copy code sang project là xong.
    • Giúp tối ưu được effort khi các team không start cùng một thời điểm. Vì team start sau sẽ sử dụng lại được phần đã làm của team start trước.

    Nhược điểm

    • Cần quản lí, phân chia công việc tốt, member có khả năng làm việc nhóm.
    • Thường chỉ hiệu quả cao đối với các dự án mới

    Tổng kết

    Phía trên là nội dung chia sẻ về cách tối ưu khi làm ứng dụng hỗ trợ đa ngôn ngữ, mình hi vọng bài viết có thể giúp mọi người phần nào trong quá trình thực hiện các ứng dụng có hỗ trợ đa ngôn ngữ. Cảm ơn mọi người đã dành thời gian đọc bài viết này.

  • HƯỚNG DẪN THỰC HIỆN ỨNG DỤNG MOBILE HỖ TRỢ ĐA NGÔN NGỮ

    HƯỚNG DẪN THỰC HIỆN ỨNG DỤNG MOBILE HỖ TRỢ ĐA NGÔN NGỮ

    Trong thời đại công nghệ 4.0, các công ty đua nhau chuyển đổi số, vì vậy có rất nhiều những ứng dụng di động được phát triển để giúp tiếp cận người dùng một cách dễ dàng hơn. Để những ứng dụng có thể vươn xa ra tầm thế giới, tiếp cận được với những người dùng nước ngoài, thì ứng dụng đó cần phải hỗ trợ đa ngôn ngữ. Vì vậy hôm nay mình sẽ hướng dẫn các bạn một số cách thực hiện một ứng dụng iOS hỗ trợ đa ngôn ngữ.

    Cài đặt dự án hỗ trợ đa ngôn ngữ

    Bước 1: Thực hiện thêm ngôn ngữ hỗ trợ bằng cách Chọn Project -> Info(thông tin) -> Bấm nút +

    Thêm ngôn ngữ
    Bỏ tích ở các file storyboard để xCode không gen ra các file String cho các files storyboard

    Bước 2: Tạo file String để chứa nội dung theo các ngôn ngữ New File… -> tìm string và chọn Strings File -> Next

    Bước 3: Localize file string vừa mới tạo Chọn File vừa tạo -> Bấm vào nút Localize… -> Popup hiển thị lên thì chọn Localize

    Bước 4: Tích vào ngôn ngữ bạn hỗ trợ, Chọn File String localize vừa được tạo -> Ở menu bên phải mục Localization tích vào những ngôn ngữ mà ứng dụng của bạn hỗ trợ.

    Chọn ngôn ngữ hỗ trợ

    Vậy là việc cài đặt đa ngôn ngữ cho ứng dụng của bạn đã hoàn thành.

    Thực hiện đa ngôn ngữ với các chuỗi (Localize String)

    1. Để tránh các lỗi sai chính tả và việc thực hiện đa ngôn ngữ trở nên dễ dàng hơn thì chúng ta sẽ tạo ra một enum Localization để liệt kê các item/string dưới dạng keyword để sử dụng như sau:
    
    enum Localization {
        static let helloWorld: String = "Hello"
        static let buttonChangeLanguageTitle: String = "btn.changeLanguage"
    }

    Với mỗi 1 key của string chúng ta cần tạo ra value tương ứng với nó ở trong file Localizable mà chúng ta đã tạo.

    Thêm key/value cho các file ngôn ngữ tương ứng:

    Thêm key/value cho ngôn ngữ Tiếng Anh
    Thêm key/value cho ngôn ngữ Tiếng Việt

    NOTE:
    – Kết thúc của dòng code phải là dấu chấm phẩy “;” nếu 1 dòng code bị thiếu nó sẽ khiến ứng dụng của bạn hiển thị không đúng ngôn ngữ.
    – Key phải trùng với giá trị của enum Localization

    2. Tạo một file chung để quản lí ngôn ngữ như đoạn code dưới đây

    // for manage language
    class LanguageManager {
        static let shared = LanguageManager()
        private init(){}
        
        // save language to UserDefault
        func changeLanguage(_ language: Language) {
            UserDefaults.standard.set(language.rawValue, forKey: "APP_LANGUAGE")
        }
        
        /// Get language of set on app, if nil use device language
        /// - Returns: current language of application
        func getAppLanguage() -> Language {
            if let language = UserDefaults.standard.value(forKey: "APP_LANGUAGE") as? String {
                return Language(rawValue: language) ?? .english
            } else {
                // default lan is english
                let currentLanguage: String = Locale.current.languageCode ?? Language.english.rawValue
                return Language(rawValue: currentLanguage) ?? .english
            }
        }
        
        // define enum language
        enum Language: String {
            case vietnamese = "vi"
            case english = "en"
        }
    }

    Ở đây mình tạo ra một file quản lí ngôn ngữ nhằm mục đích tập trung tất cả những tính năng liên quan tới ngôn ngữ: thay đổi, lấy ra ngôn ngữ …

    Chúng ta cần lưu ngôn ngữ của ứng dụng vào Local Storage để khi người dùng tắt ứng dụng vào lại thay đổi vẫn được áp dụng. Trong trường hợp này mình chọn cách dùng UserDefault vì những lí do sau:
    – Dễ sử dụng
    – Dữ liệu cần lưu không yêu cầu bảo mật
    – Dữ liệu lưu có dung lượng nhỏ

    3. Vậy là chúng ta đã tạo xong file quản lí ngôn ngữ. Tiếp đến để việc sử dụng localization dễ dàng chúng ta sẽ tạo ra một var localized trong extension String như sau:

    
    extension String {
        var localized: String {
            let currentLanguage = LanguageManager.shared.getAppLanguage().rawValue
            guard let bundlePath = Bundle.main.path(forResource: currentLanguage, ofType: "lproj"), let bundle = Bundle(path: bundlePath) else {
                return self
            }
            return NSLocalizedString(self, tableName: nil, bundle: bundle, value: "", comment: "")
        }
    }
    

    Sau khi tạo xong extension thì việc thực hiện code localized trở nên rất dễ dàng. Khi cần sử dụng chúng ta chỉ cần .localized là xong.

    4. Demo và cách sử dụng:

    Trong ví dụ này mình sẽ tạo ra 1 label hiển thị text và một nút để thay đổi ngôn ngữ của ứng dụng. Sau đó mình tạo ra một func setDataForUI() nhằm mục đích set tất cả các data liên quan tới localization ở đây. Nếu sau đó đổi ngôn ngữ ta chỉ cần gọi lại hàm này để thực hiện set lại.

        private func setDataForUI() {
            lbHello.text = Localization.helloWorld.localized
            btnChangeLanguage.setTitle(Localization.buttonChangeLanguageTitle.localized, for: .normal)
        }
    
        @IBAction func changeLanguage(_ sender: Any) {
            if LanguageManager.shared.getAppLanguage() == .english {
                LanguageManager.shared.changeLanguage(.vietnamese)
            } else {
                LanguageManager.shared.changeLanguage(.english)
            }
            setDataForUI()
        }

    Kết quả

    Vậy là chúng ta đã hoàn thành việc thực hiện localize String cho ứng dụng. Mình hi vọng bài viết này có thể giúp được chút gì đó cho các bạn. Cảm ơn các bạn đã đọc bài viết này, chúc các bạn thành công!

  • Design Pattern: Builder Pattern trong iOS

    Design Pattern: Builder Pattern trong iOS

    Design Pattern: Builder pattern trong iOS

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

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

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

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

    Nội dung

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

    Builder pattern là gì?

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

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

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

    Vấn đề

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

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

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

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

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

    Class diagram

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

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

    Demo

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

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

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

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

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

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

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

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

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

    Áp dụng trong iOS

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

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

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

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

    Tạo một builder và protocol

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

    Sử dụng

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

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

    Tổng kết

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

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

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

    Table of contents

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

    Đặt tên biến

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

    Not Preferred

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

    Preferred

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

    Not Preferred

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

    Preferred

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

    Not Preferred

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

    Preferred

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

    Not Preferred

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

    Preferred

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

    Đặt tên hàm

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

    Not Preferred

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

    Preferred

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

    Preferred

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

    Preferred

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

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

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

    Spacing

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

    Not Preferred

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

    Preferred

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

    Not Preferred

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

    Preferred

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

    Not Preferred

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

    Preferred

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

    Not Preferred

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

    Preferred

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

    Comment

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

    Access Control

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

    Not Preferred

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

    Preferred

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

    Not Preferred

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

    Preferred

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

    Self & Closure

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

    Not Preferred

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

    Preferred

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

    To be continue…

  • How To Create A Framework In Swift

    How To Create A Framework In Swift

    Updated for Swift 5

    Vì sao nên sử dụng framework

    • Việc sử dụng các tính chất hướng đối tượng trong lập trình rất phổ biến trong đó có tính chất kế thừa
    • Nhưng với việc swiftUI được ra đời và với SwiftUI thì View nó là struct, nên không thể kế thừa
    • Vậy có cách nào để thay thế được kế thừa trong swift?
    • Đối với riêng SwiftUI nói riêng hay việc sử dụng các framework iOS nói chung: Việc sử dụng tính chất kế thừa sẽ có những issue như swiftUI đang sử dụng View là struct ( struct k có các tính chất hướng đối tượng như class). Vâyviệc Customize 1 class để tái sử dụng như Class MainView: CustomizeView hay các phương thức như override hay class cha có các property gì thì class con cũng sẽ có các property đấy

    Tạo framework như thế nào?

    • Từ Xcode 11, Apple đã công bố tạo một framework được gọi là XCFramework
      • Mở Xcode, File > New > Project và chọn Framework

    • Triển khai mã code cho framwork tại đây
    • Tiếp theo sử dụng xcodebuild archive để tạo kho lưu trữ

      xcodebuild archive -scheme PROJECTNAME_HERE -destination=”iOS” -archivePath /tmp/xcf/ios.xcarchive -derivedDataPath /tmp/iphoneos -sdk iphoneos SKIP_INSTALL=NO BUILD_LIBRARIES_FOR_DISTRIBUTION=YES

      xcodebuild archive -scheme PROJECTNAME_HERE -destination=”iOS Simulator” -archivePath /tmp/xcf/iossimulator.xcarchive -derivedDataPath /tmp/iphoneos -sdk iphonesimulator SKIP_INSTALL=NO BUILD_LIBRARIES_FOR_DISTRIBUTION=YES destination: tùy chọn xác định nền tảng mục tiêu và các thiết bị.archivePath: đường dẫn tới thư mục cần tạo framework

    • Sau khi chạy 2 lệnh ở trên thành công
    • Tiếp theo sử dung câu lệnh này để đóng gói framwork

      xcodebuild -create-xcframework -framework /tmp/xcf/ios.xcarchive/Products/Library/Frameworks/PROJECTNAME_HERE.framework -framework /tmp/xcf/iossimulator.xcarchive/Products/Library/Frameworks/PROJECTNAME_HERE.framework -output FRAMEWORK_NAME.xcframework

      File đã được gen ra thành công:

    • Add framework đã tạo vào trong project cần dùng và để sang Embed & Sign 
    • Import framework vào class cần sử dụng

    (more…)

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

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

    Table of contents

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

    Magic number & Duplicate code

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

    Not Preferred

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

    Preferred

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

    Code Organization

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

    Not Preferred

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

    Preferred

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

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

    Scene Delegate

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

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

    Computed Property

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

    Preferred

    var diameter: Double {
      return radius * 2
    }
    

    Not Preferred

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

    Optional

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

    Preferred

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

    Not Preferred

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

    Preferred

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

    Not Preferred

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

    Multi-line String

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

    Preferred

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

    Not Preferred

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

    Bonus

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

    Preferred

    let swift = "not a scripting language"
    

    Not Preferred

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

    Preferred

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

    Not Preferred

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

    Preferred

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

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

    Preferred

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

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

    Preferred

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

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

    Not Preferred

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

    Reference

  • Method Swizzling in Swift

    Method Swizzling in Swift

    Table of Contents

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

    Problems?

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

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

    Method swizzling comes to rescue

    Defination

    So what is method swizzling?

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

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

    Swizzling

    Use swizzling to solve the problem:

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

    Swizzling CocoaTouch class

    To swizzle, you just need to follow some steps:

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

    Let’s swizzle:

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

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

    Next, create a function where the swizzle takes place.

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

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

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

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

    Swizzling custom Swift class

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

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

    And the results:

    Note

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

    Example:

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

    References:

  • Swift – Basic to advanced Closure

    Swift – Basic to advanced Closure

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


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

    Closure cơ bản

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

    Closure nâng cao

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

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

    Tổng kết


    Basic closure

    1. Tạo closure cơ bản

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

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

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

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

    2. Tạo tham số cho closure

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


    Advanced closure

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

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

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

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

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

    2. Trả về closure từ function

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

    Tổng kết

    Về bản chất

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