Category: Swift

  • iOS/Swift In-App Purchase (P2)

    iOS/Swift In-App Purchase (P2)

    Xin chào mọi người. Phần 1 mình đã hướng dẫn các bạn setup môi trường và setup payments rồi. Bài này mình sẽ tiếp tục với implement code và thực hiện test sau khi implement.

    Implement

    Đầu tiên chúng ta enable In-App Purchase trong Capability

    Bắt đầu implememt code

    Ở bài trước mình đã tạo một item tên là Apple Book và đặt productID là Demo.TechoverInAppPurchase.co.Apple.Book

    Cũng trong bài trước mình có nhấn mạnh productID rất quan trọng và phải matching trong code, bên dưới đây mình sẽ thực hiện điều đó.

    Đầu tiên mình tạo ra một file plist mình để tên là IAPProductIDs, mục đích của file này là để quản lý tất cả các purchase producIDs của ứng dụng, thường thì một ứng dụng sử dụng In-App Purchase sẽ có nhiều item thanh toán, cũng như nhiều giá trị khác nhau, mỗi giá item đó tương ứng với một productID, Hiện tại demo của mình chỉ có 1 item thanh toán vì thế trong file plist của mình chỉ có một item và value của item đó là productID.


    Tiếp theo mình tạo ra một class IAPManager như bên dưới:

    import Foundation
    import StoreKit
    
    class IAPManager: NSObject {
        
        // MARK: - Custom Types
        
        enum IAPManagerError: Error {
            case noProductIDsFound
            case noProductsFound
            case paymentWasCancelled
            case productRequestFailed
        }
        
        // MARK: - Properties
        
        static let shared = IAPManager()
        
        private var onReceiveProductsHandler: ((Result<[SKProduct], IAPManagerError>) -> Void)?
        private var onBuyProductHandler: ((Result<Bool, Error>) -> Void)?
        
        var totalRestoredPurchases = 0
        
        // MARK: - Init
        
        private override init() {
            super.init()
        }
        
        // MARK: - General Methods
        
        fileprivate func getProductIDs() -> [String]? {
            guard let url = Bundle.main.url(forResource: "IAPProductIDs", withExtension: "plist") else { return nil }
            do {
                let data = try Data(contentsOf: url)
                let productIDs = try PropertyListSerialization.propertyList(from: data, options: .mutableContainersAndLeaves, format: nil) as? [String] ?? []
                return productIDs
            } catch {
                print(error.localizedDescription)
                return nil
            }
        }
        
        func getPriceFormatted(for product: SKProduct) -> String? {
            let formatter = NumberFormatter()
            formatter.numberStyle = .currency
            formatter.locale = product.priceLocale
            return formatter.string(from: product.price)
        }
        
        func startObserving() {
            SKPaymentQueue.default().add(self)
        }
        
        func stopObserving() {
            SKPaymentQueue.default().remove(self)
        }
        
        func canMakePayments() -> Bool {
            return SKPaymentQueue.canMakePayments()
        }
        
        // MARK: - Get IAP Products
        
        func getProducts(withHandler productsReceiveHandler: @escaping (_ result: Result<[SKProduct], IAPManagerError>) -> Void) {
            onReceiveProductsHandler = productsReceiveHandler
            
            guard let productIDs = getProductIDs() else {
                productsReceiveHandler(.failure(.noProductIDsFound))
                return
            }
            
            let request = SKProductsRequest(productIdentifiers: Set(productIDs))
            request.delegate = self
            request.start()
        }
    
        // MARK: - Purchase Products
        
        func buy(product: SKProduct, withHandler handler: @escaping ((_ result: Result<Bool, Error>) -> Void)) {
            let payment = SKPayment(product: product)
            SKPaymentQueue.default().add(payment)
            
            onBuyProductHandler = handler
        }
        
        func restorePurchases(withHandler handler: @escaping ((_ result: Result<Bool, Error>) -> Void)) {
            onBuyProductHandler = handler
            totalRestoredPurchases = 0
            SKPaymentQueue.default().restoreCompletedTransactions()
        }
    }
    
    // MARK: - SKPaymentTransactionObserver
    
    extension IAPManager: SKPaymentTransactionObserver {
        func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
            transactions.forEach { (transaction) in
                switch transaction.transactionState {
                case .purchased:
                    onBuyProductHandler?(.success(true))
                    SKPaymentQueue.default().finishTransaction(transaction)
                case .restored:
                    totalRestoredPurchases += 1
                    SKPaymentQueue.default().finishTransaction(transaction)
                case .failed:
                    if let error = transaction.error as? SKError {
                        if error.code != .paymentCancelled {
                            onBuyProductHandler?(.failure(error))
                        } else {
                            onBuyProductHandler?(.failure(IAPManagerError.paymentWasCancelled))
                        }
                        print("IAP Error:", error.localizedDescription)
                    }
                    SKPaymentQueue.default().finishTransaction(transaction)
                case .deferred, .purchasing: break
                @unknown default: break
                }
            }
        }
        
        func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
            if totalRestoredPurchases != 0 {
                onBuyProductHandler?(.success(true))
            } else {
                print("IAP: No purchases to restore!")
                onBuyProductHandler?(.success(false))
            }
        }
        
        func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
            if let error = error as? SKError {
                if error.code == .paymentCancelled {
                    onBuyProductHandler?(.failure(IAPManagerError.paymentWasCancelled))
                } else {
                    print("IAP Restore Error:", error.localizedDescription)
                    onBuyProductHandler?(.failure(error))
                }
            }
        }
    }
    
    // MARK: - SKProductsRequestDelegate
    
    extension IAPManager: SKProductsRequestDelegate {
        func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
            let products = response.products
            
            if !products.isEmpty {
                onReceiveProductsHandler?(.success(products))
            } else {
                onReceiveProductsHandler?(.failure(.noProductsFound))
            }
        }
        
        func request(_ request: SKRequest, didFailWithError error: Error) {
            onReceiveProductsHandler?(.failure(.productRequestFailed))
        }
        
        func requestDidFinish(_ request: SKRequest) {
            // Handle finish
        }
    }
    
    // MARK: - IAPManagerError Localized Error Descriptions
    
    extension IAPManager.IAPManagerError: LocalizedError {
        var errorDescription: String? {
            switch self {
            case .noProductIDsFound:
                return "No In-App Purchase product identifiers were found."
            case .noProductsFound:
                return "No In-App Purchases were found."
            case .productRequestFailed:
                return "Unable to fetch available In-App Purchase products at the moment."
            case .paymentWasCancelled:
                return "In-App Purchase process was cancelled."
            }
        }
    }
    

    Ở class trên mình đã viết đầy đủ các methods như: buy(), restorePurchases()… Cũng như xử lý các error.

    Bây giờ chúng ta sử dụng IAPManager để thực hiện purchase nhé.

    Đầu tiên mình thực hiện layout một để một button có tên là buy ở giữa màn hình:

    Tiếp theo mình thực hiện implement việc purchase trong class ViewController

    import UIKit
    import StoreKit
    
    class ViewController: UIViewController {
        
        private var products: [SKProduct] = []
    
        override func viewDidLoad() {
            super.viewDidLoad()
        }
        
        override func viewWillAppear(_ animated: Bool) {
            IAPManager.shared.getProducts { (result) in
                DispatchQueue.main.async {
                    switch result {
                    case .success(let products):
                        self.products = products
                    case .failure(let error):
                        self.showAlert(message: error.localizedDescription)
                    }
                }
            }
        }
        
        // MARK: - Actions
    
        @IBAction func didBuy(_ sender: UIButton) {
            DispatchQueue.main.async {
                self.verifyBeforeBuy()
            }
        }
        
        // MARK: - Private Methods
        
        private func verifyBeforeBuy() {
            guard let product = product(with: "Demo.TechoverInAppPurchase.co.Apple.Book"),
                  let price = IAPManager.shared.getPriceFormatted(for: product) else {
                return
            }
            
            let alertController = UIAlertController(title: product.localizedTitle,
                                                    message: product.localizedDescription,
                                                    preferredStyle: .alert)
            
            alertController.addAction(UIAlertAction(title: "Buy now for \(price)", style: .default, handler: { (_) in
                self.buy(product: product)
            }))
            
            alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
            self.present(alertController, animated: true, completion: nil)
        }
        
        private func product(with id: String) -> SKProduct? {
            return products.first(where: { $0.productIdentifier == id })
        }
            
        private func buy(product: SKProduct) {
            if !IAPManager.shared.canMakePayments() {
                self.showAlert(message: "In-App Purchases are not allowed in this device.")
            } else {
                IAPManager.shared.buy(product: product) { result in
                    switch result {
                    case .success(let success):
                        if success {
                            self.showAlert(message: "Buy success")
                        } else {
                            self.showAlert(message: "Buy error")
                        }
                    case .failure(let failure):
                        self.showAlert(message: failure.localizedDescription)
                    }
                }
            }
        }
    }
    
    extension ViewController {
        func showAlert(title:String = "",
                       message: String) {
            let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
            self.present(alert, animated: true, completion: nil)
        }
    }
    

    Mình sẽ giải thích flow của việc purchase như sau:

    • Ở viewWillAppear() mình lấy tất cả producIDs đã set trong IAPProductIDs.plist và lưu lại với biến products: [SKProduct]
    • Khi người dùng bấm vào buy button mình thực hiện verify trước khi gọi đến IAPManager.shared.buy(product: product)
    • verifyBeforeBuy(): mình lấy product có id là Demo.TechoverInAppPurchase.co.Apple.Book, từ product đó mình lấy ra price của product đó bằng method getPriceFormatted trong IAPManager và thông báo cho người dùng số tiền cần bỏ ra để mua item này
    • Khi người dùng bấm confirm mua trên alert mình thực hiện kiểm tra xem device có cho phép thực hiện giao dịch hay không bằng method IAPManager.shared.canMakePayments, nếu có thì gọi đến IAPManager.shared.buy và nếu không cho phép thì hiển thị thông báo cho người dùng
    • IAPManager.shared.buy(): sau khi thực hiện method này hệ thống sẽ show một actionsheet để người dùng xác nhận trước khi thực hiện giao dịch
    • Sau khi thực hiện giao dịch nếu thành công sẽ nhận được một thông báo và thất bại thì mình cũng sẽ hiển thị thông báo cho người dùng

    Test In-App Purchase sau khi implement

    Trên thực tế khi thực hiện mỗi giao dịch đều mất tiền, vậy mỗi khi test in-app purchase thì đều mất tiền sao? Câu trả lời là không, vì apple đã support viêc testing thông qua apple sandbox account, điều này giúp developer cũng như tester thực hiện giao dịch mà không mất tiền.

    Chúng ta vào đây để add sandbox account. Mình add account [email protected]

    Tiếp theo chúng ta thực hiện login sandbox account trên device test.

    Setting -> App Store -> Sandbox account -> login

    Sau khi add sandbox account và login, chúng ta thực hiện purchase thì sẽ nhận được thông báo là đang purchase với account sandbox và không mất phí:

    Thông qua phần 1 và bài này mình đã giới thiệu đến các bạn cách để thực hiện in-app purchase trong Swift.
    Mình hi vọng bài viết có thể giúp ích được cho các bạn. Chúc các bạn thành công!

  • iOS/Swift In-App Purchase (P1)

    iOS/Swift In-App Purchase (P1)

    Trên AppStore chắc hẳn các bạn đã từng gặp các ứng dụng phải trả tiền để mua, các ứng dụng gắn quảng cáo hay những ứng dụng sử dụng miễn phí các chức năng cơ bản và sau đó phải bỏ tiền ra để nâng cấp VIP, hay nâng cấp các chức năng cao cấp hơn. Không phải ứng dụng nào cũng có thể sử dụng dịch vụ bên thứ 3 để thanh toán. Còn về phía người dùng chắc chắn họ cũng không muốn bị lừa đảo, chắc chắn ai cũng muốn sử dụng một phương thức thanh toán nhanh chóng, tin cậy và bảo mật.

    Khi thanh toán bất kì một dịch vụ nào chắc chắn bên cung cấp dịch vụ thanh toán sẽ cắt một phần nào đó trên mỗi giao dịch. Không bỏ lỡ cơ hội kiếm tiền dễ dàng đó, Apple cũng cung cấp cho các developer một API thanh toán có tên là In-App Purchase. Với In-App Purchase thì người dùng có thể thanh toán trực tiếp trên tài khoản Apple ID, quá trình thanh toán nhanh chóng, an toàn và đáng tin cậy.

    Trong bài này mình sẽ giới thiệu In-App Purchase đến các bạn và implement nó.

    Các kiểu Thanh toán

    • Consumable : Người dùng có thể thực hiện một giao dịch gì đó trong ứng dụng. Đặc điểm của kiểu thanh toán này là người dùng có thể mua 1 hay nhiều lần không giới hạn về thời gian.
    • Non-Consumable : Đây là kiểu thanh toán mà khi người dùng có thể mua và sử dụng mãi mãi.
    • Auto-Renewable Subscriptions : Đây là một kiểu thanh toán tự động gia hạn sau một khoảng thời gian nhất định. Ví dụ người dùng trả tiền để mua gói học tiếng anh VIP trong 1 tháng và được tự động gia hạn theo định kỳ cho đến khi người dùng huỷ.
    • Non-Renewing Subscriptions : Kiểu thanh toán này giống với Auto-Renewable Subscriptions ở điểm nó bán một loại dịch vụ trả phí giới hạn thời gian, điểm khác biệt duy nhất là nó sẽ không tự động gia hạn khi hết hạn.

    Đến đây chắc hẳn sẽ có bạn thắc mắc là người bán được bao nhiêu, và Apple được bao nhiêu trên mỗi lượt giao dịch?

    Thông thường tỷ lệ ăn chia là 70|30 tức là người bán sẽ được 70% và Apple sẽ được 30%. Kiểu thanh toán mà Apple khuyến khích dùng nhất là Auto-Renewable Subscriptions, riêng kiểu thanh toán này Apple chỉ lấy 15% trên mỗi giao dịch, tức là người bán sẽ nhận được 85%, theo như Apple giải thích là họ muốn giúp người dùng không phải đăng ký lại mỗi lần hết hạn, tuy nhiên có thể mục đích chính ở đây là người dùng sẽ sử dụng dịch vụ lâu dài hơn.

    Về cơ bản lý thuyết chỉ có vậy thôi, giờ chúng ta đi vào phần setup nhé!

    Setup môi trường

    Khi người dùng thực hiện một giao dịch thì tiền đó sẽ về đâu, tính thuế như thế nào?

    Chắc chắn tiền sẽ phải về với người bán dịch vụ trong ứng dụng rồi đúng không? Trước khi public ứng dụng có sử dụng In-App Purchase thì chúng ta cần setup Agreements, tax, và banking.

    Để setup Agreements, tax, và banking các bạn vào itunes-connect và vào Agreements, tax, and banking và setup Paid Application.

    Sau khi bạn vào Agreements, tax, and banking thì giao diện chi tiết sẽ như thế này:

    Mặc định thì Free Apps sẽ được active và các bạn setup với Paid Apps nhé. Vì mình đã setup trước đó rồi nên trạng thái đã được active, mình nhớ là phải mất 24h để Apple verify thông tin mà các bạn khai báo.

    Các bạn có thể sử dụng thẻ ATM nội địa để setup cho mục banking nhé

    Setup Payments

    Tiếp theo chúng ta đến phần setup payments.

    Trong itunes-connect truy cập vào my Apps

    Trường hợp các bạn chưa có app thì tạo app mới.
    Ở đây mình tạo 1 app mới. Để tạo app mới ở itunes-connect thì các bạn cần chọn một Identifier từ list Identifiers vì thế hay chuẩn bị một bundleID và tạo Identifier với BundleID đó. Đây cũng chính là bundleID sử dụng cho ứng dụng của chúng ta luôn.


    Tiếp theo chúng ta vào features -> In-app Purchases


    Tiếp theo chúng ta tạo một In-App Purchase. Ở Demo lần này mình sẽ tạo phương thức thanh toán là Consumable

    • Type: Kiểu thanh toán.
    • Reference Name: Tên này sẽ giúp bạn gợi nhớ và không hiển thị cho người dùng nên hãy chọn cái tên nào mà khi bạn nhìn vào đó hiểu ngay item này nó làm gì.
    • Product ID: Đây là một thông tin rất quan trọng nó là định danh duy nhất cho 1 loại thanh toán của bạn, bạn sẽ cần nó để matching trong code. Ở đây mình đặt tên với bundleID + tên của item thanh toán.

    Sau khi tạo xong thì status của item thanh toán đó là Missing Metadata. Các bạn hay setup tiếp các thông tin tiếp theo cho status chuyển thành Ready to Submit nhé:

    • Availability: chọn các quốc gia có thể thanh toán.
    • Price Schedule: lựa chọn giá bán, mặc định Apple sẽ quy định ra các giá bán, do đó các bạn không thể điền môt giá trị nào đó mà phải chọn trong đây.
    • Localizations: đây chính là phần sẽ hiển thị cho người dùng, bạn sẽ chọn Localizations tuỳ theo từng ngôn ngữ.
    • Review Information: Bạn sẽ phải cung cấp ảnh chụp design chức năng thanh toán trên app tương ứng với những gì bạn đang setup, bên cạnh đó là mô tả và giải thích cho chức năng này, điều này phục vụ cho việc review của apple.

    Sau khi các bạn điền tất cả các thông tin cần thiết thì trạng thái của item thanh toán này sẽ là Ready to Submit

    Đến đây chúng ta đã hoàn thành việc setup môi trường và setup payments rồi. Phần tiếp theo mình sẽ hướng dẫn tiếp phần implement code và thực hiện test sau khi implement!.

    Mình hi vọng bài viết có thể giúp ích được cho các bạn. Chúc các bạn thành công!

  • Deprecate old API in Swift

    Deprecate old API in Swift

    Trong quá trình develop của mình chắc hẳn các bạn đã từng gặp các warning khi sử dụng các methods cũ, cụ thể là method đó đã đổi tên hoặc không còn sử dụng được trên version nào đó.

    Trong quá trình code nếu có mothod nào không sử dụng chắc các bạn comment hoặc xoá code luôn rồi. Thông thường chỉ có các Mothods của Apple hoặc các Framework, Library thì mới có các warning này. Nhưng làm thế nào để họ có thể đánh dấu methods nào dùng ở version nào? Bài này mình cùng tìm hiểu vấn đề này nhé.

    Định nghĩa:

    Deprecate là một cách thông báo đến người dùng method rằng method này không được sử dụng nữa, không còn hữu ích nữa

    Một số nguyên nhân chính dẫn đến deprecate

    • Đổi tên
    • Có một sự thay thế tốt hơn
    • Không còn sử dụng được (Đã lỗi thời)

    Các cách để deprecate một method

    Basic usage

    Chúng ta có thể đánh dấu một method không còn sử dụng nữa bằng thuộc tính @available

    Đây là hình thức đơn giản nhất để đánh dấu bất kì methods nào không còn sử dụng nữa

    Parameter đầu tiên chỉ ra nền tảng mà method này hỗ trợ. Trong trường hợp này, mình sẽ sử dụng dấu hoa thị (*) để cho biết rằng tất cả các nền tảng đều được hỗ trợ.
    Tập chung và parameter thứ 2, ở đây mình truyền vào là deprecated nghĩa là thông báo rằng method này không còn sử dụng được nữa.

    Chúng ta sẽ nhận được một warning nếu cố gắng sử dụng nó:

    Deprecated version

    Bạn có thể chỉ định phiên bản đầu tiên của method không dùng nữa bằng cách chỉ định số phiên bản bên cạnh parameter deprecated.


    Trong ví dụ này, mình đặt deprecated cho oldApi trên iOS 13. Lưu ý rằng chúng ta cần chỉ định nền tảng, trong trường hợp này là iOS. Cũng warning khi sử dụng, giống với việc không chỉ định version nhưng nội dung warning ở đây là chỉ rõ ra rằng method này bị deprecated ở version nào.

    Deprecated message

    Bạn có thể thêm message mô tả rõ hơn về deprecated, thông báo này sẽ hiển thị cùng với cảnh báo mặc định hoặc thông báo lỗi.

    Dưới dây là một ví dụ khi chúng ta thêm message cho deprecated

    Và đây là là warning khi chúng ta sử dụng cho method

    Rename

    Nếu mục đích deprecate của bạn là vì đổi tên thì Swift đã có parameter hỗ trợ cho việc này

    Sử dụng rename là cách hay nhất để thông báo cho người dùng về tên mới

    Xcode có 2 cách để hỗ trợ thông báo cho người dùng về rename

    • Xcode sẽ generate một Fix button cho chúng ta. Khi click và button đó sẽ rename từ method cũ sang method mới.
    • Xcode sẽ tạo một message về việc đổi tên ở trong warning message.

    Đây là một ví dụ từ Apple, họ thay đổi index(of:) thành firstIndex(of:) trên Swift 5

    Obsoleted

    Nếu một method không thể sử dụng được nữa thì bạn nên sử dụng parameter Obsoleted

    Khi sử dụng obsoleted thay vì nhận được warning thì chúng ta sẽ nhận được một error, error message này gồm message đã được thêm vào parameter và message mặc định

    Đây là ví dụ khi bạn dùng obsoleted

    Vì nhận được lỗi nên chắc chắn chương trình của chúng ta không thể chạy được rồi, các bạn phải bỏ đi hoặc sử dụng method khác để chạy chường trình.

    Thông qua bài này mình đã giới thiệu đến các bạn cách để đánh dấu deprecated.
    Mình hi vọng bài viết có thể giúp ích được cho các bạn. Chúc các bạn thành công!

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

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

  • Schedulers in Swift Combine Framework

    Schedulers in Swift Combine Framework

    iOS 13 Apple đã giới thiệu đến các developer một big framework, có tên là Combine. Framework này cung cấp rất nhiều thứ thú vị, trong đó có Schedulers. Trong bài này mình sẽ giới thiệu đến các bạn Schedulers.

    Nội dung:

    • Scheduler trong Combine là gì?
    • Các kiểu của Scheduler trong combine.
    • Làm thế nào để switch schedulers?
    • Làm thế nào để perform asynchronous với Combine?
    • Sự khác nhau giữa receive(on:) và subscribe(on:)?

    Scheduler là gì?

    Scheduler Là cơ chế đồng bộ hoá của framework Combine. Nó xác định Context là gì, thực hiện công việc ở đâu (where), thực hiện công việc khi nào (when)

    • Where: Có nghĩa là Run loop hiện tại, dispatch queue hoặc operation queue.
    • When: Có nghĩa là thời gian ảo, tính theo thời gian của Scheduler, công việc được thực hiện bởi Scheduler phải tuân thủ theo thời gian của Scheduler, có thể không tương ứng với thời gian thực tế của hệ thống.

    Các kiểu của Scheduler trong combine

    Framework Combine cung cấp nhiều kiểu của Schedule khác nhau:

    • DispatchQueue: Thực hiện công việc trên một dispatch queue cụ thể: serial, concurrent, main and global. Thông thường serial, global dùng cho các công việc dưới background, và main queue sử dụng cho việc update UI. từ Xcode 11 GM Seed thì concurrent queues không được khuyến khích sử dụng.
    • OperationQueue: Tương tự như DispatchQueue, sử dụng OperationQueue.main cho các công việc liên quan đến UI, và các queue khác sử dụng cho công việc background. Theo như bài này trên một diễn đàn về Swift thì không khuyến khích sử dụng operationQueue với maxConcurrentOperations lớn hơn 1.
    • RunLoop: Thực hiện công việc trên một RunLoop cụ thể
    • ImmediateScheduler: Thực hiện các hành động đồng bộ ngay lập tức. App sẽ terminate với một fatalError nếu bạn cố gắng thực hiện delay task.

    Sử dụng RunLoop.main, DispatchQueue.main hoặc OperationQueue.main để thực hiện công việc liên quan UI. Không có sự khác biệt giữa chúng.

    Scheduler Mặc định

    Ngay cả khi bạn không chỉ định bất kì scheduler nào, Combine vẫn cung cấp cho bạn một scheduler mặc định, scheduler này sử dụng cùng thead với nơi nó được tạo ra. Ví dụ: nếu bạn bắn một sự kiện nào đó từ background thread thì bạn sẽ nhận được sự kiện đó cùng thread với nơi scheduler được tạo ra là background thread

    1. In ra true nếu nhận được sự kiện ở main thread và false nếu ở thead khác
    2. gửi một sự kiện đi từ main thread
    3. gửi một sự kiện từ global

    Và kết quả in ra ở đây lần lượt là true false. Đồng nghĩa là scheduler ở nơi nhận sự kiện giống với scheduler ở nơi sự kiện được sinh ra.

    Switching Schedulers

    Thông thường các hoạt như call API được xử lý ở background thread để UI không bị block. Sau khi call API thì thường update lại UI và công việc này được thực hiện trên main thread. Cách thức của Combine để thực hiện việc này là schitch schedulers. Nó được thực hiện với sự trợ giúp của hai methods: subscribe(on:) và receive(on:).

    • receive(on:) Method này thay scheduler của tất cả những operators sau nó

      Và đây là kết quả:

      Chúng ta có thể thấy map và sink đứng sau receive và đã thay đổi scheduler bởi receive.

      * Vị trí của receive là rất quan trọng bởi vì nó chỉ làm thay đổi scheduler của những operators đứng sau nó
    • subscribe(on:) Method này thay đổi scheduler của subscribe, cancel, and request operations.

      và đây là kết quả

      Nhìn khá giống với receive nhỉ? Cùng nhau đi đến một ví dụ nữa nhé. Cụ thể bây giờ chúng ta chuyển subscribe xuống một dong

      Lúc này kết quả in ra vẫn là false false

      * Vị trí của subscribe là không quan trọng, chỉ đơn giản là đăng ký event đó ở thread nào thôi.

    Thực hiện Asynchronous với Combine

    Trong thực tế khi lập trình chúng ta thường kết hơp hai methods subscribe(on:) and receive(on:). Cùng đi vào ví dụ cụ thể nhé!

    Ví dụ mình có một công việc cần thời gian dài để hoàn thành

    Khi call với main thread nó sẽ làm UI bị block trong 10s. Hãy nhớ rằng schuduler mặc định sẽ chạy cùng thread ở nơi nó được sinh ra

    Với lần call này thì ‘hello’ sẽ được in ra sau, cụ thể là đợi in ra ‘Received value’ sau đó mới thực hiện in ‘hello’. Block UI như thế này thực sự không cho phép trong lập trình. Cùng đi tiếp để xem hướng giải quyết vấn đề này trong trong Combile nhé!

    Pattern phổ biến để thực hiện asynchronous trong Combile là thực hiện subscribe ở background thread và recive ở main thread

    với lần call này thì ‘hello’ đã được in ra trước. Tuyệt vời! bài toán block UI đã được giải quyết.

    Bài viết này mình đã giới thiếu đến các bạn Scheduler, một phần trong Combile được Apple giới thiệu ở iOS 13

    Mình hi vọng bài viết có thể giúp ích được cho các bạn. Chúc các bạn thành công!

  • Viết Unit Test cho API sử dụng URLProtocol

    Viết Unit Test cho API sử dụng URLProtocol

    Hi mọi người, Chắc hẳn các bạn đã từng làm những dự án yêu cầu viết Unit Test, UI Test. Về cơ bản thì UT là một cách verify lại logic mình viết ra. Tuy nhiên đối với việc viết UT cho API thì việc verify logic lại gặp khó khăn vì kết quả của API phụ thuộc vào Server.

    Bài này mình sẽ giới thiệu về Mock Network cho việc viết UT.

    Bắt đầu thôi!!!

    Đầu tiên chúng ta tìm hiểu URLProtocol là gì

    Định nghĩa

    URLProtocol là một abstract class xử lý việc tải dữ liệu URL dành riêng cho protocol-specific.
    Mỗi khi URL Loading System nhận được yêu cầu load 1 URL, nó sẽ tìm kiếm trình xử lý giao thức đã đăng ký để xử lý yêu cầu. Mỗi trình xử lý cho hệ thống biết liệu nó có thể xử lý một yêu cầu đã cho thông qua phương thức canInit(with request: URLRequest) của nó hay không?
    Tham số cho phương thức này là yêu cầu giao thức được hỏi nếu nó có thể xử lý. Nếu phương thức trả về true, thì hệ thống tải sẽ dựa vào lớp con URLProtocol này để xử lý yêu cầu và bỏ qua tất cả các trình xử lý khác.

    Custom URLProtocol

    Ở đây mình tạo ra một class URLProtocolMock và kế thừa URLProtocol và override các method như sau:

    • mothod canInit() được gọi để kiểm tra xem Giao thức có thể xử lý loại yêu cầu nhất định hay không.
    • nếu canInit là true, sau đó canonicalRequest() được gọi và chúng ta trả ra đúng request hiện tại
    • Sau đó đến phần tiếp nạp. startLoading() sẽ làm tất cả những gì có thể để tìm nạp nội dung, stopLoading() có thể được gọi để hủy hoặc đánh dấu hoàn thành.

    Bây giờ đến việc viết UT cho một function call API, để viết được UT cho function call API thì chúng ta cần có function call API. Mình sử dụng URLSession để thực hiện call một API đơn giản
    API: https://postman-echo.com/get?test=123
    Method: GET

    Unit Test cho Network

    Tiếp theo mình tạo ra class NetworkServiceTests để viết test cho NetworkServices

    • setUpWithError() mothod này được chạy trước khi thực hiện một test case. Ở đây mình mình setup cho URLSession set configuration.protocolClasses = [URLProtocolMock.self] để thực hiện proccess lấy data từ custom URL Protocol đã tạo ở trên
    • tearDownWithError() mothod này được chạy mỗi khi hoàn thành một test case. Vì mình không cần release gì ở đây nên mình không code gì thêm cho nó.

    Sau khi config cho class test, Mình viết một test case cho trường hợp requestPostmanEcho success

    Lưu ý: khi đặt tên test case thì bắt buộc phải có prefix là “test” nhé, ở đây mình đặt tên test case này là test_requestPostmanEcho_success()

    • line 30-33: tạo ra response mong muốn
    • line 35-38: config response và jsonData cho request

    Vì đã config statusCode 200 nên request requestPostmanEcho trả ra kết quả thành công với response là một dictionary. Mình lấy kết quả trả ra và so sánh với giá trị mong muốn. Tất nhiên là thành công rồi!!!

    Mình sẽ viết thêm một test case nữa, Lần này mình vẫn config response thành công (statusCode: 200) tuy nhiên ở jsonString mình sẽ config thành một dạng không phải là json. Lúc này khi gọi đến requestPostmanEcho thì sẽ nhận được kết quả là error responseFailed.

    Mình so sánh kết quả nhận được từ request với kết quả mong muốn là responseFailed. Tất nhiên là nó cũng sẽ thành công rồi

    Trong ví dụ call API của mình thì có một xử lý riêng cho statusCode 404 NotFound. Các bạn cũng có thể viết một test case cho lỗi 404 này và tất nhiên là phải config statusCode về 404 cho test case đó.

    Trên đây mình đã giới thiệu đến các bạn cách viết UT cho API sử dụng URLProtocol. Từ đây các bạn có thể tuỳ biến cho dự án của mình.

    Mình hi vọng bài viết có thể giúp ích được cho các bạn. Chúc các bạn thành công!

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

  • OWASP Top 10 for Mobile

    OWASP Top 10 for Mobile

    OWASP (Open Web Application Security Project) là dự án xây dựng một nền tảng hướng đến việc tăng cường bảo mật cho phần mềm. Bắt nguồn là cho các dự án Web app, Back end. Tuy nhiên hiện nay thì bảo mật cho các dự án Mobile cũng đã trở nên cấp thiết hơn rất nhiều, và series bài viết này sẽ hướng đến tìm hiểu về các vấn đề về security, phương thức tấn công, các phương pháp phòng tránh, các checklist để tạo ra một project mobile an toàn hơn.

    OWASP Top 10 for Mobile

    1. Improper Platform Usage

    Improper Platform Usage tức là sử dụng các chức năng (features) của nền tảng mobile (iOS/Android) một cách không phù hợp.

    Cụ thể, từng OS system sẽ cung cấp cho developers các tính năng (capabilities, features) có thể sử dụng để phát triển ứng dụng. Nếu developers không sử dụng các tính năng này để phát triển, hoặc sử dụng chúng một cách không chính xác, thì sẽ được gọi là improper use (sử dụng không đúng cách).

    Ví dụ: với iOS thì lưu các thông tin quan trọng của user như passworld hay token thì không được sủ dựng UserDefault để lưu mà cần lưu bằng Keychain, tuy nhiên, việc sử dụng sai config của Keychain cũng tiềm ẩn rủi ro bị tấn công lộ mật khẩu. Hoặc với những quyền truy cập vào thông tin user như Location, HealthKit, Photos cũng không được sử dụng một cách thiếu cân nhắc.

    2. Insecure Data Storage

    Insecure data storage, lưu trữ dữ liệu một cách không an toàn, không đơn thuần chỉ là cách lưu trữ data (ảnh, text, sql data…) một cách không an toàn. Mà còn có thể là log file, cookie file, cached file (URL, browser, third party data…). Một ví dụ điển hình là các developer rất hay sử dụng log trong quá trình phát triển phần mềm, rất dễ tiềm ẩn nguy cơ "lỡ tay" log ra các thông tin nhạy cảm mà quên không loại bỏ, dẫn đến việc bị lọt ra môi trường product.

    Những item cần cân nhắc khi nghĩ đến nguy cơ lộ data:

    • Cách OS cache data, cache image, có thể là cả logging, buffer, thậm chí key-press (đặc biệt là OS mở như Android)
    • Cách các framework được sử dụng trong dự án cache data
    • Cách các thư viện open source cache data, gửi / nhận data

    3. Insecure Communication

    Các ứng dụng mobile thì luôn cần phải transfer data, có thể là giữa client – server, giữa các devices với nhau. Và trong quá trình gửi nhận data, nếu không tuân thủ các quy tắc bảo mật thì có thể bị kẻ xấu tấn công ăn cắp các thông tin nhạy cảm. Đó có thể là password, thông tin account, hoặc các thông tin private của user.

    Việc tấn công ăn cắp thông tin có thể thông qua wifi mà devices đang truy cập, các router nhà mạng hoặc các thiết bị ngoại vi BLE, NFC…

    4. Insecure Authentication

    Insecure Authentication mô tả việc thực hiện xác thực user kém bảo mật dẫn đến người khác có thể lợi dụng để tấn công vào backend nhằm ăn cắp thông tin hoặc thực hiện các request tổn hại đến hệ thống. Vấn đề này có thể do việc thiết kế backend thiếu security như việc request không có access token, hoặc từ bản thân mobile app đã thực hiện việc authenticate offline sơ sài (ví dụ như set passcode ngắn để truy cập vào chức năng quan trọng, hoặc lưu password của user) dẫn đến việc kẻ tấn công có thể ăn cắp thông tin và truy cập vào server.

    5. Insufficient Cryptography

    Việc bảo mật dữ liệu trong mobile app thường áp dụng mã hóa. Tuy nhiên, trong một vài trường hợp thì mã hõa vẫn sẽ tiềm ân rủi ro bị tấn công ăn cắp dữ liệu. Một vài khả năng có thể kể đến như:

    • Quá ỷ lại vào mã hóa của OS (Built-In Code Encryption Processes), ví dụ iOS bản thân nó cũng sẽ mã hóa ứng dụng, tuy nhiên nếu devices bị jail break thì cũng có khả năng decode ứng dụng để lấy dữ liệu.
    • Sử dụng một cơ chế mã hóa đã lỗi thời hoặc tự viết lại thuật toán mã hóa.
    • Lưu trữ encrytion key một cách thiếu bảo mật.

    6. Insecure Authorization

    • Authentication: xác thực user
    • Authorization: xác thực quyền của user

    Việc thiếu sót trong việc xác thực quyền hạn của user trong hệ thống có thể dẫn đến sai sót trong việc cho phép user được truy cập vào những tài nguyên không được cho phép, hoặc có tính bảo mật cao. Dẫn đến việc mất mát các thông tin nhạy cảm của server hoặc bị thực thi những quyền có tính chất ảnh hưởng lớn đến hệ thống. Các vấn đề có thể xảy ra trong trường hợp này như:

    • Server không check quyền của user khi request lên, mà hoàn toàn dựa vào request của client, ví dụ người tấn công có thể lợi dụng bằng cách thêm các param vào GET/POST request để lừa server rằng "tao là admin" và có thể truy cập vào các thông tin nhạy cảm.
    • Một số trường hợp có thể server cung cấp các API dành riêng cho "admin" và nghĩ rằng các user bình thường sẽ không biết các API này nên không cần phải check quyền, tuy nhiên việc này không hề đảm bảo đến việc sẽ bảo mật được các API này mà không lộ ra ngoài cho các user không có quyền khác.

    7. Poor Code Quality

    Poor code quality là các lỗi bảo mật liên quan đến bản thân ngôn ngữ lập trình, có thể kể đến như lỗi:

    • Buffer overflows: các lỗi buffer overflows thì hay xảy ra với các thư viện viết bởi C, C++, và iOS cùng Android đề sử dụng các thư viện C, C++ trong hệ thống, do đó kẻ xấu có thể tấn công vào các thư viện này và qua đó thực thi các đoạn lệnh hoặc thậm chí cài mã độc vào ứng dụng.
    • Format string vulnerabilities: những lỗi dạng này thường là kẻ tấn công cố tình input các đoạn string mã hóa hoặc format đặc biệt vào ứng dụng, hoặc các câu lệnh để tấn công vào ứng dụng. Qua đó có thể gây ra các lỗi như crash ứng dụng, view data trong stack, view thông tin trong memory, thậm chí thực thi source code
    • Tấn công vào third party hoặc tấn công thông qua webview của OS: sử dụng các thư viện kém bảo mật cũng tiềm ẩn nguy cơ bị tấn công. Ngoài ra, trong các version OS cũ, cả iOS và Android đều bộc lộ rất nhiều lỗi liên quan đến webview như thực thi các đoạn javascript nhằm ăn cắp thông tin user.

    8: Code Tampering

    Các ứng dụng mobile, về cơ bản là sẽ nằm trên máy của user, do đó nên ứng dụng mobile rất dễ bị tấn công thay đổi nội dung source code để thực hiện các mục đích xấu. Có thể kể đến các kịch bản tấn công như: kẻ xấu sẽ cài đặt ứng dụng của bạn lên device đã bị jail, qua đó có thể xem được nội dung bên trong của ứng dụng như assets, resource. Thậm chí thay đổi source code hoặc các API được call. Mục đích của việc tấn công này có thể là unlock các chức năng ẩn, hoặc trả phí, hoặc ăn cắp thông tin. Một vài trường hợp có thể là cài mã độc vào, thay đổi asset, resource, sau đó lại distribute ứng dụng lên các kho ứng dụng lậu nhằm ăn cắp thông tin của người tải về. Trước đây thì mình hay jail device để cheat game, hoặc các ứng dụng ngân hàng cũng có thể là mục tiêu yêu thích để bị tấn công dạng này. -> Hãy check Jail/Root khi khởi động ứng dụng.

    9. Reverse Engineering

    Kẻ tấn công sẽ down ứng dụng và sử dụng các tool để tấn con giải mã source code nhằm các mục đích như ăn cắp thông tin trong string table/plist, đọc source code, tìm hiểu thuật toán hoặc các thư viện mà app sử dụng. Với kiểu tấn công này thì mục tiêu tấn công thường là

    • Ăn cắp thông tin về backend server, như url, path, cert nếu có
    • Ăn cắp các key của thuật toán mã hóa
    • Ăn cắp các thông tin liên quan đến sở hữu trí tuệ

    10. Extraneous Functionality

    Các chức năng không liên quan đến ứng dụng?

    Khi ứng dụng được phát triển, trên thực tế nó có rất nhiều các chức năng không thuộc functional requirement và non-functional requirement, nhưng là không thể thiếu trong quá trình phát triển ứng dụng như:

    • Log file, log console để debug những thông tin API, thông tin user hoặc thông tin của ứng dụng để debugging trong quá trình phát triển
    • Các switch flag để on off các chức năng nào đó trong ứng dụng, có thể là chức năng của admin hoặc chức năng liên quan đến A-B testing…
    • Các API path nhằm debug nhưng quên không loại bỏ khi lên môi trường production.

    Các chức năng này nếu bị khai thác có thể làm lộ thông tin về ứng dụng cũng như thông tin về user.

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