Author: DaoNM2

  • Adobe xD to SWIFT

    Adobe xD to SWIFT

    Xin chào! Bài viết này mình muốn chia sẻ cho mọi người về một số cách để code SWIFT giống với Adobe xD và hạn chế phần nào việc bị bắt bug UI không đáng.

    Trong nỗi trăn trở ở mỗi dự án mobile có hàng trăm, hàng ngàn các bug UI được log. Tự nhiên mình lại nghĩ phải làm việc gì đó để giúp cho anh em code UI ngon hơn, đỡ tạo ra bug UI hơn. Vì vậy mình đã viết bài viết này hi vọng sẽ giúp anh em được phần nào trong việc tránh dính phải những bug UI.

    Khi làm việc với xD các anh em thường bỏ qua các chỉ số của xD mà hay tự thực hiện code để nhìn sao cho giống UI nhất có thể, nếu không hiểu rõ bản chất nó khiến cho anh em mất khá nhiều thời gian để có thể làm giống được với file thiết kế cụ thể ở đây là file Adobe xD.

    Drop shadow

    Để giúp mọi người làm việc dễ dàng hơn nên mình đã tạo ra một hàm trong CALayer để giúp anh em đổ bóng bao chuẩn, bao giống xD :v Việc của anh em là lấy chỉ số ở xD và truyền vào func để setup là xong.

    extension CALayer {
        
        /// make shadow like Adobe xD, all prameters using same value with Adobe xD
        /// - Parameters:
        ///   - color: shadow color
        ///   - opacity: alpha of shadow color (0-100)
        ///   - x: x
        ///   - y: y
        ///   - b: shadow radius
        func dropShadowLikeXD(color: UIColor = .black,
                              opacity: Int = 50,
                              x: CGFloat = 0,
                              y: CGFloat = 3,
                              b: CGFloat = 6) {
            masksToBounds = false
            shadowColor = color.cgColor
            shadowOpacity = Float(opacity) / 100.0
            shadowOffset = CGSize(width: x, height: y)
            shadowRadius = b / 2.0
            shadowPath = nil
        }
    }

    Để anh em dễ hiểu hơn thì mình xin giải thích như sau:
    – color: đây là shadow color, nó là màu shadow trên xD, cái này khá đơn giản mọi người chỉ cần lấy màu trên xD và fill vào là xong
    – opacity: đây là độ trong suốt của shadow, trên Adobe xD không có thuộc tính này mà giá trị này sẽ là Opacity của shadow color
    – x: độ lệch của shadow so với view, tính từ trái qua phải. x > 0 thì shadow lệch qua phải và ngược lại
    – y: độ lệch của shadow so với view, tính từ trên xuống dưới, x > thì shadow lệch xuống dưới và ngược lại.
    – b: là thuộc tính blur trên xD, nhưng trong Swift không có thuộc tính này, mà chỉ có shadowRadius nó là bán kính của shadow, và nó bằng 1/2 blur trên xD.
    – masksToBounds: thuộc tính này bằng true sẽ không thể tạo được shadow vì nó sẽ cắt mất view shadow đi. vì trong hàm mình đã set lại giá trị này bằng false.

    Lưu ý: Để vừa đổ bóng được kết hợp với bo góc chúng ta cần thực hiện bo góc trước khi gọi hàm dropShadowLikeXD()

    Border

    Border trong Adobe xD cho phép custom khá nhiều thuộc tính, tuy nhiên mấy ông Dev tạo ra CALayer của Apple lại chỉ cho set mỗi 2 thuộc tính là borderWidth và borderColor. Vì vậy để làm giống Adobe xD chúng ta sẽ mất công hơn 1 chút. Cụ thể chúng ta sẽ cần thêm 1 enum và một func trong extension của UIView như sau:

    extension UIView {
        enum BorderStrokeStyle {
            case inner 
            case outer
            case center
        }
        
        func borderLikeXD(size: CGFloat = 1,
                          color: UIColor = .black,
                          dash: CGFloat = 0,
                          cap: CAShapeLayerLineCap = .butt,
                          join: CAShapeLayerLineJoin = .miter,
                          stroke: BorderStrokeStyle = .inner) {
            if dash <= 0 {
                layer.borderColor = color.cgColor
                layer.borderWidth = size
            } else {
                let newShapeLayer  = CAShapeLayer()
                newShapeLayer.strokeColor = color.cgColor
                newShapeLayer.lineWidth = size
                newShapeLayer.lineDashPattern = [NSNumber(value: dash), NSNumber(value: dash)]
                newShapeLayer.frame = self.bounds
                newShapeLayer.fillColor = nil
                newShapeLayer.lineCap = cap
                newShapeLayer.lineJoin = join
                switch stroke {
                case .inner:
                    let innerBounds = CGRect(x: bounds.origin.x + size / 2, y: bounds.origin.y + size / 2, width: bounds.size.width - size, height: bounds.size.height - size)
                    newShapeLayer.path = UIBezierPath(roundedRect: innerBounds, cornerRadius: layer.cornerRadius).cgPath
                case .outer:
                    let outerBounds = CGRect(x: bounds.origin.x - size / 2, y: bounds.origin.y - size / 2, width: bounds.size.width + size, height: bounds.size.height + size)
                    newShapeLayer.path = UIBezierPath(roundedRect: outerBounds, cornerRadius: layer.cornerRadius).cgPath
                case .center:
                    newShapeLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
                }
                
                self.layer.addSublayer(newShapeLayer)
                
            }
        }
    }

    Lưu ý: Do hàm này thực hiện thêm mới sublayer nên mọi người không nên gọi nó thực hiện ở func có thể gọi nhiều lần như: viewWillAppear(), viewDidAppear() …

    Line spacing

    Do định nghĩa về line spacing của Apple và Adobe xD khác nhau nên chúng ta không thể sử dụng cùng chỉ số được, vì vậy chúng ta cần tạo ra một phương thức để sửa lại công thức sao cho khớp với Adobe xD.

    Adobe xD định nghĩa line spacing: là khoảng cách từ Top của dòng trên so với Top của dòng dưới liền kề.

    Apple định nghĩa line spacing: là khoảng cách giữa Bot của dòng trên so với Top của dòng dưới liền kề.

    Line spacing

    Chúng ta có thể nhận ra sự chênh lệch giá trị line spacing của Apple so với Adobe chính là chiều cao của 1 dòng. Vậy nên mình có tạo ra một func giúp mọi người set lại giá trị line spacing giống xD mà không phải đau đầu tính toán nữa.

    extension UILabel {
        /// Set line spacing for label
        ///
        /// - Parameter lineSpacing: Line spacing
        func setLineSpacing(_ lineSpacing: CGFloat) {
            // Check label text empty
            guard let labelText: String = self.text,
                  let font = self.font else {
                return
            }
            let constraintRect: CGSize = CGSize(width: self.bounds.width, height: .greatestFiniteMagnitude)
            let boundingBox: CGRect = "Ok".boundingRect(with: constraintRect,
                                                        options: .usesLineFragmentOrigin,
                                                        attributes: [NSAttributedString.Key.font: font],
                                                        context: nil)
            let heightLabel: CGFloat = ceil(boundingBox.height)
            let paragraphStyle: NSMutableParagraphStyle = NSMutableParagraphStyle()
            // line spacing on xD - height of one line
            paragraphStyle.lineSpacing = lineSpacing - heightLabel
            
            let attributedString: NSMutableAttributedString
            if let labelattributedText: NSAttributedString = self.attributedText {
                attributedString = NSMutableAttributedString(attributedString: labelattributedText)
            } else {
                attributedString = NSMutableAttributedString(string: labelText)
            }
            
            // Line spacing attribute
            attributedString.addAttribute(NSAttributedString.Key.paragraphStyle,
                                          value: paragraphStyle,
                                          range: NSRange(location: 0,
                                                         length: attributedString.length))
            
            self.attributedText = attributedString
        }
    }

    Trên đây là những gì mình muốn chia sẻ lại cho mọi người, hi vọng nó sẽ giúp được mọi người phần nào trong công việc. Nếu mọi người có câu hỏi hay thắc mắc gì có thể đặt câu hỏi ở dưới comment mình sẽ cố gắng giải đáp những thắc mắc của mọi người.

    Xin cảm ơn mọi người đã đọc bài viết của mình!

  • iOS/Swift: Lưu dữ liệu bằng Keychain

    iOS/Swift: Lưu dữ liệu bằng Keychain

    Đôi khi phát triển ứng dụng chúng ta sẽ phải lưu lại những dữ liệu nhạy cảm như thông tin người dùng, thẻ ngân hàng … Vậy lúc đó chúng ta sẽ phải cần đến Keychain để lưu các dữ liệu đó. Vậy hôm nay mình sẽ giới thiệu với các bạn phương pháp lưu dữ liệu sử dụng Keychain.

    Tại sao không dùng các phương pháp lưu dữ liệu khác?

    Có thể các bạn đã biết, những thông tin nhạy cảm của người dùng như: thông tin tài khoản(username, pass, token …), thông tin thẻ ngân hàng … nếu để lộ ra ngoài sẽ làm ảnh hưởng rất lớn tời người dùng. Vì vậy nếu chúng ta sử dụng các cách lưu dữ liệu thông thường không có tính bảo mật như: File, Property List(UserDefaults), CoreData, Realm … thì người ta hoàn toàn có thể lấy được nội dung của ứng dụng một cách đơn giản. Về cơ bản các kiểu lưu dữ liệu kia đều sử dụng 1 file để lưu trữ data ở đó mà không sử dụng bất kỳ phương pháp mã hóa nào, vì vậy nó quá dễ dàng để các tin tặc có thể lấy được những dữ liệu bạn lưu trong các file đó.

    Sử dụng Keychain Services API

    Để lưu trữ những dữ liệu nhạy cảm cần tính bảo mật Apple đã cung cấp cho chúng ta Security Services. Keychain services API sẽ giúp chúng ta lưu trữ các dữ liệu này ở trong một encrypted database được gọi là Keychain.

    Để lưu Password vào keychain mình sẽ làm như sau:

        func save(_ password: String, for account: String) {
            let password = password.data(using: String.Encoding.utf8)!
            let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
                                        kSecAttrAccount as String: account,
                                        kSecValueData as String: password]
            let status = SecItemAdd(query as CFDictionary, nil)
            guard status == errSecSuccess else { return print("save error")}
        }

    Keychain service cung cấp một số kiểu để xác định nơi lưu trữ phù hợp. kSecClassGenericPassword là một trong số đó, keychain services sẽ hiểu items này là password và cần được mã hóa. Ngoài ra còn có các kiểu khác các bạn tìm hiểu ở ĐÂY.

    SecItemAdd(_:_:) để lưu dữ liệu vào keychain

    Để lấy lại dữ liệu chúng ta đã lưu chúng ta làm như sau:

        func getPassword(for account: String) -> String? {
            let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
                                        kSecAttrAccount as String: account,
                                        kSecMatchLimit as String: kSecMatchLimitOne,
                                        kSecReturnData as String: kCFBooleanTrue]
    
            var retrivedData: AnyObject? = nil
            let _ = SecItemCopyMatching(query as CFDictionary, &retrivedData)
    
    
            guard let data = retrivedData as? Data else {return nil}
            return String(data: data, encoding: String.Encoding.utf8)
        }

    Để lấy dữ liệu ra chúng ta sử dụng SecItemCopyMatching() truyền vào đó query mà mình muốn.

    Đối với các bạn lần đầu làm việc với Keychain API thì nó cũng khá lằng nhằng. Nhưng hiện nay đã có khá nhiều thư viện ngon hỗ trợ việc code của các bạn trở nên dễ dàng hơn như: Keychain-swift, Keychain Access

    Mã hóa thông tin

    Ngoài Password ra đôi khi chúng ta cũng cần phải mã hóa một số loại thông tin nhạy cảm khác. Việc mã hóa bằng tay khá phức tạp, vì vậy để hỗ trợ cho việc mã hóa mình thường dùng CryptoSwift.

    Tổng kết

    Mình hi vọng bài viết có thể giúp các bạn khi các bạn gặp phải các bài toán cần phải lưu những dữ liệu nhạy cảm. Khi đó nhớ chọn Keychain nhé. Chúc các bạn thành công.

    Bài viết tiếp theo mình sẽ chia sẻ thêm một tính năng rất hay của keychain đó là Keychain Sharing

  • iOS/Swift: Lưu dữ liệu sử dụng Property List

    iOS/Swift: Lưu dữ liệu sử dụng Property List

    Lời mở đầu

    Cách lưu dữ liệu sử dụng file Property List khá phổ biến trong lập trình ứng dụng iOS, nó thường được dùng để lưu các dữ liệu nhỏ và không cần tính bảo mật cao. Bài viết này mình sẽ chia sẻ với các bạn một số cách để thao tác với Property List.

    Info.plist

    Tất cả các project khi bạn mới tạo XCode sẽ tự tạo ra một file Info.plist, nó làm nhiệm vụ lưu lại các thông tin dự án của bạn. Từ project name, version cho đến các setting cũng như mô tả API của Apple.

    Các thông tin trong file Info.plist sẽ được các API của Apple truy cập vào để lấy thông tin hiển thị lên ứng dụng. (VD: Hiển thị message cho pop up xin quyền truy cập Camera …) cũng như việc xác nhận thông tin của Apple khi bạn Submit ứng dụng lên App Store Connect. Vì vậy file này chỉ nên lưu những thông tin cài đặt của dự án. Nếu bạn muốn lưu dữ liệu dạng này ta có thể tạo một file mới và lưu vào đó.

    Tạo file Property List mới

    Bước 1: Chuột phải vào thư mục bạn muốn lưu file -> New File…

    Bước 2: Đánh ô tìm kiếm phía trên với keyword “Property List” -> Next -> Đặt tên file

    Vậy là bạn đã tạo thành công file Property List. Giờ bạn có thể mở file và điền thông tin mình muốn lưu vào file đó.

    Tuy nhiên nếu sử dụng XCode thì chúng ta chỉ có thể lưu được các dữ liệu như:

    • String
    • Number
    • Bool
    • Data
    • Date
    • Array
    • Dictionary

    Chúng ta cũng có thể lưu kiểu custom object vào file Property List nhưng phải sử dụng code để encode dữ liệu cần lưu sang Data rồi khi sử dụng thì chúng ta decode nó về dữ liệu ban đầu.

    Tạo file Property List bằng code

    Chúng ta có thể tạo file Property List một cách đơn giản hơn bằng cách như sau:

        func save(key: String, value: Any) {
            let myData: NSDictionary = [key: value]
            let fileManager = FileManager.default
            // Đường dẫn lưu file và tên file của bạn
            let path = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("MyInfo3.plist")
            // Viết vào file nếu file không tồn tại nó sẽ tự tạo file mới.
            myData.write(to: path, atomically: true)
            print(path)// in ra console để thấy đường dẫn lưu file
        }

    Truy cập vào file Property List bằng code

    Lấy data từ file Property list

        func getMyPlist(key: String) -> Any? {
            // Lấy ra đường dẫn vào file Property List của bạn
            let path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("MyInfo.plist")
            // Sử dụng NSDIctionary để lấy nội dung từ URL
            guard let myDict = NSDictionary(contentsOf: path) else { return nil }
            print(path)
            // Trả về giá trị theo key trong Dictionary
            return myDict[key]
        }

    Lưu dữ liệu vào file Property List

        func saveMyPlist(key: String, value: Any) {
            let fileManager = FileManager.default
            // Tạo đường dẫn file
            let path = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("MyInfo.plist")
            guard let myDict = NSMutableDictionary(contentsOf: path) else {
                // nếu file chưa tồn tại chúng ta sẽ ghi vào file với bộ key/value đầu tiên.
                let myData: NSDictionary = [key: value]
                myData.write(to: path, atomically: true)
                return
            }
            // nếu file đã tồn tại chúng ta sẽ update dữ liệu
            myDict[key] = value
            myDict.write(to: path, atomically: true)
            print(path)
        }

    Xóa một key trong Property List

        func removeMyPlist(key: String) {
            let fileManager = FileManager.default
            // Tạo đường dẫn file
            let path = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("MyInfo.plist")
            guard let myDict = NSMutableDictionary(contentsOf: path) else { return }
            // nếu file đã tồn tại chúng ta sẽ update dữ liệu
            myDict.removeObject(forKey: key)
            myDict.write(to: path, atomically: true)
        }

    Để tương tác dễ hơn với plist bắt buộc chúng ta phải tạo ra các func để quản lý các file đó. Nhưng nếu các bạn không muốn làm những thứ lằng nhằng đó thì chúng ta có thể chuyển qua dùng một file Property List có sẵn mà Apple đã cung cấp đó là sử dụng UserDefault

    UserDefault

    UserDefault là một singleton class nhằm mục đích giúp các nhà phát triển có thể dễ dàng lưu dữ liệu vào file Plist hơn. Mặc định khi bạn sử dụng UserDefault nó sẽ tạo ra một file Plist trong dự án. Từ đó ta có thể thêm, sửa, xóa các key nằm trong file Plist đó dựa trên các hàm mà UserDefault cung cấp.

    UserDefault tương tác trên Property List nên nó cũng phải tuân thủ theo quy tắc của file đó. Vì vậy thông thường chung ta chỉ có thể gán được các giá trị mà file Plist cho phép.

    Để lưu/lấy giá trị bằng UserDefault ta làm như sau:

            let udf = UserDefaults.standard//Tạo instance
            udf.set("11", forKey: "1")// gán giá trị 11 cho key "1"
            udf.synchronize()//nó block thread đang gọi cho đến khi giá trị được gán hoàn tất
            print(udf.value(forKey: "1"))// lấy dữ liệu của key 1 và in ra console

    Như vậy với UserDefault chúng ta có thể làm việc với file Plist một cách đơn giản. Tuy nhiên, đôi khi chúng ta cũng phải lưu lại kiểu dữ liệu mà Plist không hỗ trợ. Lúc này chúng ta cần phải encode nó về Data để có thể lưu được dạng này.

    Lưu ý

    Chúng ta không nên lưu những dữ liệu quá nặng trong Plist, vì file này sẽ tiêu tốn bộ nhớ RAM khi bạn chạy ứng dụng.

    Lưu custom object

    Tạo mới class

    class Employee: NSObject, NSCoding {
        let name: String
        let dob: String
        // hàm khởi tạo
        init(name: String, dob: String) {
            self.name = name
            self.dob = dob
        }
        // init với NSCoder bước này để decode
        required convenience init(coder aDecoder: NSCoder) {
            let name = aDecoder.decodeObject(forKey: "name") as! String
            let dob = aDecoder.decodeObject(forKey: "dob") as! String
            self.init(name: name, dob: dob)
        }
        // hàm này để encode
        func encode(with aCoder: NSCoder) {
            aCoder.encode(name, forKey: "name")
            aCoder.encode(dob, forKey: "dob")
        }
    }

    Lưu Employee vào UserDefault sử dụng NSKeyedArchiver

            let employee = Employee(name: "Dino", dob: "19/09/1999")
            let udf = UserDefaults.standard
            do {
                let encodeData = try NSKeyedArchiver.archivedData(withRootObject: employee, requiringSecureCoding: false)
                udf.set(encodeData, forKey: "emp")
                udf.synchronize()
            } catch {
                print(error)
            }

    Lấy ra Employee từ UserDefault

    let decode = udf.data(forKey: "emp")
    let decodeEmp = NSKeyedUnarchiver.unarchiveObject(with: decode!) as? Employee

    Tổng kêt

    Mình hi vọng bài viết này có thể giúp các bạn sử dụng Property List tốt hơn. Chúc các bạn thành công.

  • iOS: Một số cách lưu dữ liệu phổ biến

    iOS: Một số cách lưu dữ liệu phổ biến

    Lời mở đầu

    Đối với các ứng dụng iOS, việc lưu dữ liệu là việc bắt buộc. Việc lưu dữ liệu rất cần thiết đối với các ứng dụng từ lớn cho tới nhỏ. Bạn có thể lưu dữ liệu lớn như các dữ liệu lấy được từ các API, cho đến các trạng thái hoặc các cài đặt của ứng dụng. Bài viết này mình sẽ giới thiệu tổng quát về các cách chúng ta có thể lưu dữ liệu trong iOS. Bài viết này chỉ mang tính giới thiệu cho từng loại thường dùng cho việc gì, các cách thao tác trên từng loại mình sẽ tách ra làm các bài riêng cho các bạn dễ hiểu và đỡ bị ngợp.

    1. Lưu dữ liệu bằng file

    Các dữ liệu cơ bản thường gặp khi sử dụng ứng dụng như String(VD: json, xml ..) và Data(VD: image)

    Đối với những kiểu dữ liệu cơ bản này chúng ta có thể thực hiện thao tác lưu(save), lấy(get) dữ liệu thông qua các hàm đã được cung cấp sẵn trong class đó.

    Chúng ta thường dùng cách này để lưu những dữ liệu như:

    • Lưu lại log của ứng dụng
    • Lưu lại hình ảnh, cache data
    • Lưu lại nội dung có dữ liệu lớn
    • Lưu lại nội dung dạng json hoặc xml

    Về cơ bản thì với cách này các bạn có thể lưu được rất nhiều thứ và nó có thể được quản lý, sắp xếp tùy ý. Do đuôi file không bị cố định nên bạn có thể lưu nội dung lên file có đuôi mình muốn và mở bằng các ứng dụng hỗ trợ.

    Để quản lý thư mục lưu file, chúng ta nên sử dụng NSFileManager. Ngoài ra NSFileManager cũng cung cấp cho chúng ta các hàm để save(lưu), get(lấy) dữ liệu. Nhưng các hàm này bản thân các class như String và Data cũng có rồi nên NSFileManager thường được dùng để quán lý thư mục.

    Hướng dẫn lưu dữ liệu ra file

    2. Lưu dữ liệu phân cấp

    Property Lists

    Là các file có đuôi .plist, dùng để lưu dữ liệu có dạng như một hệ thông phân cấp kiểu Array hoặc Dictionary với dung lượng không quá lớn.

    Trong iOS, Property List chỉ lưu được một số kiểu dữ liệu cơ bản như:

    • String
    • Data
    • Date
    • Number
    • Bool
    • Array
    • Dictionary

    Ngoài ra chúng ta cũng có thể lưu được các object. Để làm điều đó chúng ta cần mã hóa(encode) nó về dạng Data rồi khi cần dùng thì giải mã(decode) dữ liệu trở lại.

    NSUserDefault

    NSUserdefault là một file Property List, đã được Apple viết sẵn 1 class dạng singleton. Nó giúp các iOS developer thuận tiện và tiết kiệm thời gian hơn trọng việc lưu/lấy dữ liệu cần thiết ở file mà NSUserdefault đã tạo sẵn cho mỗi project của bạn.

    Property List thường được dùng để:

    • Lưu dữ liệu đơn giản có dung lượng thấp
    • Lưu lại các setting của ứng dụng
    • Lưu lại các trạng thái của ứng dụng

    3. Lưu dữ liệu sử dụng SQLite

    Đối với các ứng dụng có nhiều dữ liệu phức tạp, với các mối quan hệ chồng chéo giữa các dữ liệu thì việc sử dụng cách 1 với 2 không thể đáp ứng được các yêu cầu về tốc độ tìm kiếm, sắp xếp và thậm chí là hiệu năng về việc dọc và lưu dữ liệu mà ứng dụng yêu cầu. Với những dạng dữ liệu như này chúng ta nên trang bị cho ứng dụng một phương thức tốt hơn đó chính là SQLite.

    SQLite

    SQLite là một hệ thống cơ sở dữ liệu quan hệ nhỏ gọn,hoàn chỉnh có thể cài đặt bên trong các ứng dụng khác.

    Đặc điểm của SQLite là gọn, nhẹ và đơn giản. Không cần cài đặt, không cần cấu hình hay khởi động mà có thể sử dụng ngay. Dữ liệu database cũng được lưu ở một file duy nhất. Đối với các ứng dụng mobile SQLite rất thích hợp để sử dụng.

    SQLite thường được sử dụng khi nào:

    • Khi bạn có một khối dữ liệu lớn cần phải lưu lại
    • Khi bạn có một tập các dữ liệu liên quan móc nối với nhau
    • Khi bạn muốn lưu lại các dữ liệu mà API trả về

    Một số thư viện hay sử dụng

    • FMBD(Flying Meat Database): Là một thư viện hỗ trợ bạn trong việc thao tác với SQLite . Nói cách khác FMDB là một Objecttive-C wrapper của SQLite. Công việc của FMDB là giúp bạn thoải mái hơn trọng việc thực hiện các câu lệnh truy vấn trong SQLite.
    • CoreData: Là một framework được Apple xây dụng để hỗ trợ chúng ta thao tác với SQLite Database theo hướng đối tượng mà không phải quan tâm tới các câu lệnh truy vấn của SQLite. Nó sẽ coi các bản ghi trong SQLite Database như một object, table như class.

    Nên sử dụng FMBD(Flying Meat Database) hay CoreData

    CoreData là một Framework được Apple phát triển để làm việc với SQLite Database vì vậy việc sử dụng CoreData sẽ được khuyến khích hơn cả. CoreData giúp việc viết và đọc code rõ ràng hơn việc sử dụng FMDB để làm việc trực tiếp với SQLite. Nó tốt cho việc bảo trì cũng như phát triển code sau này. Ngoài ra Apple còn hỗ trợ cả việc cache dữ liệu, tích hợp CoreData với UITableView, một class mà hầu hết các ứng dụng đều sử dụng.

    Bạn đọc dữ liệu từ database lấy ra một list các object và hiển thị nó bằng UITableView nó thực sự rất tiện lợi đối với các iOS Dev.

    4. Keychain

    Keychain là một lưu trữ bảo mật với những dữ liệu nhỏ gọn, nhạy cảm như mật khẩu, số tài khoản ngân hàng, mã bảo mật hay một số thông tin mà ta muốn bảo mật không muốn cho người khác biết được.

    Ngoài ra Apple cũng cung cấp một dịch vụ Keychain Sharing để chia sẻ keychain giữa các ứng dụng của cùng một nhà phát triển. Ví dụ như Facebook và Messenger, nếu ta đăng nhập ở Facebook và lưu password vào Keychain thì sang Messenger chúng ta có thể sử dụng password để đăng nhập nhanh ở ứng dụng này.

    5. Realm

    Realm là một database sử dụng core C++ với tham vọng thay thế SQLite với các ưu điểm về tốc độ và dễ sử dụng. Hiện nay Realm rất được ưa chuộng trên thế giới vì nó có rất nhiều ưu điểm vuợt trội so với những cách còn lại.

    Ưu điểm của Realm so với CoreData:

    • Dễ sử dụng hơn
    • Tốc độ query nhanh hơn
    • Quản lý dữ liệu dễ dàng trực quan hơn
    • Tài liệu tham khảo tốt
    • Open source

    Khi nào nên sử dụng Realm cho dự án của bạn:

    • Khi project của bạn cần lưu lại số lượng lớn các bản ghi
    • Khi project của bạn có nhiều dữ liệu móc nối với nhau, có mối quan hệ phức tạp
    • Khi dự án của bạn cần lưu lượng lớn các thông tin từ API

    Tổng kết

    Bài viết này mình đã giới thiệu với các bạn về một số cách lưu dữ liệu phổ biến trong swift. Mỗi khi các bạn muốn lưu một loại dữ liệu nào hãy cân nhắc kiểu dữ liệu nó là gì, mức độ quan trọng của nó như nào. Từ đó bạn có thể chọn được cách lưu dữ liệu tối ưu nhất với loại dữ liệu đó. Các bài viết tiếp theo mình sẽ giới thiệu với các bạn cách triển khai lưu dữ liệu cho từng cách.

    Cảm ơn các bạn đã theo dõi bài viết! Chúc các bạn thành công.

  • Design Pattern: Singleton trong Swift

    Design Pattern: Singleton trong Swift

    Singleton là một design pattern rất phổ biến trong phát triển phần mềm. Singleton rất đơn giản, phổ biến và dễ sử dụng. Chính vì những ưu điểm đó mà nó được rất nhiều người sử dụng.

    Singleton pattern là gì?

    Singleton pattern là một pattern đảm bảo rằng một (class) chỉ có một thể hiện(instance) duy nhất.

    Trong Swift các bạn có thể đã được sử dụng khá nhiều các singleton khi làm việc với các framework của Apple. Dưới đây là một số ví dụ cho singleton:

    // Shared URL Session
    let sharedURLSession = URLSession.shared
    
    // Default File Manager
    let defaultFileManager = FileManager.default
    
    // Standard User Defaults
    let standardUserDefaults = UserDefaults.standard

    Ưu điểm và nhược điểm của Singleton pattern

    Ưu điểm:

    • Đảm bảo một class chỉ có 1 instance duy nhất được khởi tạo.
    • Ẩn các các hàm khởi tạo của class. (Đảm bảo không thể khởi tạo từ bên ngoài)
    • Có thể truy cập ở bất cứ nơi nào trên project

    Nhược điểm

    • Khó kiểm soát và quản lý
    • Hạn chế số lượng instance của class
    • Khi chỉ cho phép một trường hợp cụ thể của một class

    Nên sử dụng Singleton pattern khi nào?

    Singleton pattern là một pattern rất hữu ích. Đôi khi bạn muốn chắc chắn rằng chỉ có một instance của một class được khởi tạo và ứng dụng của bạn chỉ sử dụng instance đó.

    Ví dụ: Ứng dụng của bạn có tính năng bật tắt nhạc nền khi người dùng mở ứng dụng thì nhạc nền sẽ tự động chạy và nếu người dùng muốn tắt thì phải vào setting để tắt nó. Bạn chắc chắn phải sử dụng duy nhất một instance trong trường hợp này, bởi vì bạn không thể tạo một instance để mở và một instance để tắt vì nó là 2 instance độc lập nên không thực hiện việc cho nhau được. Vì vậy chúng ta sẽ cần đến Singleton pattern trong trường hợp này để giải quyết bài toán gọn gàng và nhẹ nhàng.

    Tuy rằng Singleton pattern khá dễ sử dụng và hữu dụng nhưng bạn cần cân nhắc khi sử dụng nó tránh lạm dụng dẫn đến việc khó kiểm soát code cũng như debug.

    Các bạn nên sử dụng Singleton khi mà bài toán của các bạn yêu cầu phải có một đối tượng để quản lí các nguồn tài nguyên hoặc theo dõi trạng thái của hệ thống mà nó ảnh hưởng đến nhiều nơi trong dự án.

    Sử dụng class thông thường

    class LocationManager{
    //MARK: - Location Permission
        func requestForLocation(){
            //Code Process
            print("Location granted")
        }
        
    }
    // Truy cập vào class
    let location = LocationManager() // khởi tạo class
    location.requestForLocation()    // gọi func

    Đây là trường hợp thông thường của 1 class không sử dụng Singleton pattern để truy cập vào các func trong class. Mỗi lần muốn sử dụng một func trong class chúng ta phải khởi tạo một instance cho nó sau đó mới có thể gọi func đó được. Để giải quyết vấn đề này chúng ta tạo ra một Singleton class với static instance.

    Tạo Singleton class

    class LocationManager {
        
        static let shared = LocationManager()
        
        init(){}
        
        func requestForLocation() {
            //Code Process
            print("Location granted")
        }
        
    }
    // Truy cập func trong class sử dụng Singleton Pattern 
    LocationManager.shared.requestForLocation()
    
    // Bạn vẫn có thể truy cập vào class một cách thông thường
    let location = LocationManager()
    location.requestForLocation()
    

    Cách tốt hơn để tạo Singleton class

    class LocationManager {
        
        static let shared = LocationManager()
        
        var locationGranted: Bool?
        // thay đổi hàm init thành private để chỉ có thể khởi tạo trong class này.
        private init(){}
        
        func requestForLocation() {
            //Code Process  
            locationGranted = true     
            print("Location granted")
        }
        
    }
    // Gọi hàm trong class bằng việc sử dụng đoạn code dưới
    LocationManager.shared.requestForLocation()

    Lúc này chúng ta đã thay đổi access level của hàm khởi tạo từ internal sang private. Vì vậy nếu chúng ta cố tính sử dụng LocationManager() XCode sẽ báo lỗi: ‘LocationManager’ initializer is inaccessible due to ‘private’ protection level

    Sử dụng Singleton class

    Bây giờ, bất kể chỗ nào trong project bạn muốn sử dụng hàm requestForLocation của LocationManager bạn cũng chì cần sử dụng như sau:

    LocationManager.shared.requestForLocation()

    Sử dụng biến toàn cục (Global variables)

    Đây là cách đơn giản nhất để ứng dụng Singleton pattern:

    let sharedNetworkManager = NetworkManager(baseURL: API.baseURL)
    
    class NetworkManager {
    
        // MARK: - Properties
    
        let baseURL: URL
    
        // Initialization
    
        init(baseURL: URL) {
            self.baseURL = baseURL
        }
    }

    Bằng cách khai báo một biến toàn cục, bấn kì object nào cũng có thể truy cập dến biến toàn cục đó. Trong Swift, biên toàn cục được khai báo một cách trì hoãn cho đến khi nó được gọi ra để sử dụng lần đầu tiên. Việc khởi tạo trong swift cũng dựa trên hàm dispatch_one, điều này sẽ có ích khi bạn chỉ muốn khởi tạo object của mình một lần duy nhất.

    Tuy nhiên khi sử dụng cách này bạn sẽ gặp vấn đề là code của bạn sẽ khó hiểu và khó để debug hơn. Và hàm khởi tạo của class NetworkManager thì không thể khai báo là private.

    Lưu ý

    Mình không khuyến khích các bạn sử dụng cách tạo global variable vì các nhược điểm của nó.

    Tổng kết

    Vậy là giờ các bạn đã hiểu cách tạo Singleton class trong dự án của bạn. Nó khá đơn giản, dễ sử dụng và tốn ít thời gian để tạo. Nó có một số ưu điểm và nhược điểm. Nếu bạn sử dụng nhiều Singleton pattern trong dự án của mình thì sẽ khó có thể quản lý vòng đời của Singleton class của bạn. Ngoài ra, nó duy trì một trạng thái chia sẻ toàn cầu có thể thay đổi. Tuy rằng nó có các ưu điểm rõ rệt, nhưng hãy tránh lạm dụng Singleton pattern thay vào đó tốt hơn chúng ta nên sử dụng Dependency injection.

  • iOS: Apple Notification Center Service

    iOS: Apple Notification Center Service

    Ngày xưa khi sử dụng đồng hồ đeo tay kết nối với điện thoại di động, mỗi khi có tin nhắn hay cuộc gọi đến thì các thiết bị này đều hiển thị được thông tin đó lên màn hình. Lúc đó mình cũng khá băn khoăn tại sao đồng hồ không có sim mà sao nó lại nhận được tin nhắn, cuộc gọi đến. Cho đến gần đây mình có làm một dự án liên quan đến tính năng này mình mới hiểu được tại sao nó lại làm được điều đó. Bài viết này mình sẽ giới thiệu với các bạn về một dịch vụ của Apple có thể làm được điều đó Apple Notification Center Service.

    Giới thiệu

    Mục đích của Apple Notification Center Service (ANCS) là cung cấp cho các phụ kiên Bluetooth (kết nối với thiết bị iOS thông qua Bluetooth low-energy) một cách đơn giản và thuận tiện để truy cập nhiều loại thông báo được tạo trên thiết bị iOS.

    ANCS được thiết kế theo ba nguyên tắc: đơn giản, hiệu quả và khả năng mở rộng. Do dó các phụ kiện khác nhau từ đền LED đơn giản đến phức tạp.

    Sự phụ thuộc(Dependencies)

    ANCS không có phụ thuộc, ngoài bộ tiêu chuẩn phụ của Generic Attribute Profile(GATT). Một phụ kiện sẽ đóng vai trò là GATT Client có thể truy cập và sử dụng các dịch vụ khác do thiết bị iOS cung cấp trong khi sử dụng ANCS.

    Thuật ngữ

    • Apple Notification Center Service được viết tắt là ANCS
    • Notification Provider: nhà xuất bản dịch vụ ANCS ở đây là các thiết bị iOS được viết tắt là NP
    • Notification Consumer: Là những thiết bị ngoại vi được viết tắt là NC
    • Một thông báo hiển thị trên thiết bị iOS trong Notification center se được gọi là iOS Notification
    • Một thông báo được gửi bởi GATT characteristic đưới dạng tin nhắn không đồng bộ sẽ được gọi là GATT Notification.

    Apple Notification Center Service

    APNS là dịch vụ chính có UUID là 7905F431-B5CE-4E99-A40F-4B1E122D00D0.
    Chỉ có duy nhất một instance của ANCS có thể đại diện cho NP. Do tính chất của iOS, ANCS không được đảm bảo luôn hiển thị. Do đó, NC nên tìm kiếm và đăng ký(subcribe) Service Changed characteristic của GATT để theo dõi việc publishing và unpublishing của ANCS bất cứ lúc nào.

    Service characteristics

    Cơ bản thì ANCS có 2 characteristics như sau:

    • Notification Source: UUID 9FBF120D-6301-42D9-8C58-25E699A21DBD (notifiable)
    • Control Point: UUID 69D1D8F3-45E1-49A8-9821-9BBDFDAAD9D9 (writeable with response)
    • Data Source: UUID 22EAC6E9-24D6-4BB5-BE44-B36ACE7C7BFB (notifiable)

    Tất cả các characteristic này yêu cầu phải có authorization để truy cập. Hỗ trợ cho Notification Source characteristic là bắt buộc, trong khi Control Point characteristic làm nhiệm vụ hỗ trợ, còn Data Source characteristic thì là tùy chon (optional).

    Lưu ý:

    Có thể có nhiều hơn 3 characteristics trong ANCS đã kể ở trên. Nó có nghĩa là NC có thể bỏ qua bất kể characteristic nào mà nó không nhận ra.

    Notification Source

    Notification Source characteristic là characteristic mà NC được thông báo:

    • Sự xuất hiện iOS Notification mới trên NP
    • Sự sửa đổi một iOS Notification trên NP
    • Sự xóa bỏ một iOS Notification trên NP

    GATT Notification có thể được gửi ngay khi NC đăng ký Notification Source characteristic. Do đó, NC phải ở trong trạng thái có thể châpps nhận và xử lý đúng các message này trước khi đăng kí vào characteristic này.

    Định dạng của GATT Notification được thể hiện theo hình dưới đây:

    GATT Notification được gửi thông qua Notification Source characteristic nó chứa các thông tin như sau:

    • EventID: Trường này thông báo cho phụ kiện xem iOS Notification đã được thêm, chỉnh sửa hoặc được xóa. Các giá trị được định nghĩa trong EventID Values.
    • EventFlags: Một bitmask có bit được đặt thông báo cho một đặc thù của NC với iOS Notification. Ví dụ: Nếu một iOS Notification được coi là quan trọng thì NC có thể muốn hiển thị giao diện người dùng tích cực hơn để đảm bảo người dùng được cảnh báo chính xác. Các bit được liệt kê cho trường này được định nghĩa trong Event Flags.
    • CategoryID: Một giá trị số cung cấp một danh mục trong đó iOS Notification có thể được phân loại. NP sẽ nỗ lực hết sức để cung cấp danh mục chính xác cho mỗi iOS Notification. Nó được định nghĩa trong CategoryID Values.
    • CategoryCount: Số lượng iOS notification đang hoạt động trong category đã cho. Ví dụ: Nếu hai email chưa đọc đang nằm trong hòm thư đến email của người dùng và một email mới được đẩy lến thiết bị iOS của người dùng thì giá trị của CategoryCount = 3.
    • NotificationUID: 32 bit số định danh duy nhất(UID) cho iOS notification. Giá trị này được sử dụng để điều khiển trong các lệnh được gửi đến Control Point characteristic để tương tác với iOS Notification.

    Thời gian tồn tại của iOS Notification có thể được suy đoán ngầm thông qua chuỗi Notification Source. GATT notifications được tạo ra bởi NP:

    Control Point

    Một NC có thể muốn tương tác với một iOS Notification. Nó có thể muốn lấy thêm thông tin về nó bao gồm nội dung của nó hoắc nó có thể muốn thự hiện hành động trên nó. Việc truy suất các thuộc tính này được thực hiện thông qua Control Point và Data Source characteristic.

    Một NC có thể đưa ra yêu cầu để nhận thêm thông tin về iOS Notification bằng việc viết lệnh cụ thể tới Control Point characteristic. Nếu việc ghi vào Control Point characteristic thành công, NP sẽ phản hồi kịp thời yêu cầu đó thông qua luồng của GATT notification trên Data Source characteristic.

    Một NC có thể thực hiện các hành động được xác đinh trước trên iOS Notification bằng cách viết các lênh cụ thể vào Control Point characteristic.

    Get notification Attributes

    Lệnh lấy Notification Atributes cho phép NC truy xuất các thuộc tính của iOS Notification cụ thể.

    Nội dung của lệnh lấy Notification Attribute như sau:

    • CommandID: nên được gán là 0 (CommandIDGetNotificationAttributes)
    • NotificationUID: 32 bit đại diện cho UID của iOS Notification mà khách hàng muốn biết thông tin.
    • AttributeIDs: Là danh sách các thuộc tính mà NC muốn lấy. Một số thuộc tính có thể cần phải được theo dõi bởi tham số độ dài 16 bit chỉ định số byte tối ta của thuộc tính NC muốn truy xuất.

    Định dạng của phản hồi từ lệnh Get Notification Attributes như sau:

    • CommandID: để là 0(CommandIDGetNotificationAttributes)
    • NotificationUID: 32 bit định nghĩa UID của iOS Notification
    • AttributeList: Danh sách các AttributionID có độ dài 16 bit. Một thuộc tính là một cuỗi có độ dài tính bằng byte được cung cấp tring bộ dữ liệu, Không được NULL. Nếu thuộc tính được yêu cầu là trống(empty) hoặc thiếu cho iOS Notification, độ dài của nó được đặt = 0. Dữ liệu sẽ luôn theo thứ tự các AttributionID của lệnh Get Notification Attributes

    Nếu phản hồi lớn hơn đơn vị truyền tối đâ GATT(MTU) đã đàm phán, nó sẽ được NP chia thành nhiều mảnh. NC phải tổng hợp lại phản hồi bằng cách ghép từng đoạn. Phản hồi hoàn tất khi các bộ dữ liệu hoàn chỉnh cho từng thuộc tính được yêu cầu đã được nhận.

    Get App Attribute

    Lệnh Get App Atrributes cho phép NC truy xuất các thuộc tính của một ứng dụng cụ thể được cài đặt trên NP, định dạng của nó thể hiện như hình dưới:

    Nội dung của lênh Get App Attribute như sau:

    • CommandID: cần gán là 1 (CommandIDGetAppAttributes)
    • AppIdentifier: là chuỗi định danh ứng dụng muốn lấy thông tin. Không được null
    • AttributeIDs: Danh sác các thuộc tính mà NC muốn truy xuất.

    Nội dung phản hồi của lệnh Get App Attributes như sau:

    • CommandID: là 1(CommandIDGetAppAttributes)
    • AppIdentifier: là chuỗi định danh ứng dụng muốn lấy thông tin. Không được null
    • AttributeList: Danh sách AttributeIDs có 16 bit. Một thuộc tính là một cuỗi có độ dài tính bằng byte được cung cấp trong bộ dữ liệu, Không được NULL. Nếu thuộc tính được yêu cầu là trống(empty) hoặc thiếu cho App, độ dài của nó được đặt = 0. Thứ tự dữ liệu sẽ giống với lệnh.

    Cúng giống như lệnh Get Notification Attributes. Nếu phản hồi lớn hơn đơn vị truyền tối đâ GATT(MTU) đã đàm phán, nó sẽ được NP chia thành nhiều mảnh. NC phải tổng hợp lại phản hồi bằng cách ghép từng đoạn. Phản hồi hoàn tất khi các bộ dữ liệu hoàn chỉnh cho từng thuộc tính được yêu cầu đã được nhận.

    Perform Notification Action

    Lệnh thực hiện hành động Notification cho phép NC thực hiện hành động được xác định trước trên một Notification cụ thể của iOS Notification. Lệnh thực hiện Notification Action có các trường sau:

    BytesNameDescription
    1CommandIDlà 2 (CommandIDPerformNotificationAction).
    2-5NotificationUID32-bit đại diện cho UID của iOS Notification
    6ActionIDID của hành động mà bạn muốn thực hiện trên iOS Notification

    Không có dữ liệu nào được tạo ra trên Data Source characteristic khi lệnh này gặp vấn đề, cho dù nó có thành công hay không.

    Notification Actions

    Bắt đầu từ iOS 8.0, NP có thể thông báo cho NC về các hành động tềm năng có liên quan đến iOS Notification. Thay mặt người dùng, NC có thể yêu cầu NP để thực hiện một hành động liên quan tới một iOS Notification cụ thể.

    NC được thông báo về sự tồn tại của các hành động có thể thực hiện trên một iOS Notification bằng việc phát hiện sự hiện diện của các cờ(flag) được đặt trong trường EventFlags của GATT Notification được tạo bởi Notification Source characteristic.

    • EventFlagPositiveAction: là hành động tích cực lên iOS notification
    • EventFlagNegativeAction: là hành động tiêu cực lên iOS notification

    Các hành động thực thế được thực hiên bởi NP thay mặt NC được xác định bơi NP và thay đổi tùy thuộc vào iOS Notification mà chúng được thực hiện trên nó. Ví dụ: Có một cuộc gọi đến, hành động Trả lời thì là hành động tích cực(EventFlagPositiveAction), còn Từ Chối là hành động tiêu cực dựa trên iOS notification đó.

    NC phải giả định hay cố gắng đoán trước hành động chính xác được thực hiện trên iOS Notification, bởi vì những hành động này dựa trên thông tin không có sẵn cho nó, cũng như cá yếu tố khác như phiên bản ANCS do NP triển khai. NP đảm bảo rằng hành động tích cực và tiêu cực có liên quan đến kết quả, mà nó không làm cho người dùng bất ngờ.

    Nếu iOS Notification được hiển thị, hành động tích cực và tiêu cưc có thể hiển thị cho người dùng dựa trên kí tự X hoặc màu sắc.

    NC có thể truy suất các nhãn mô tả ngắn gọn các thành động thực tế được liên kết với một iOS Notification bằng việc truy xuất thuộc tính thông báo mới được giới thiệu trong iOS 8.0

    • NotificationAttributeIDPositiveActionLabel: nhãn(label) được sử dụng để mô tả hành động tích cực có thể được thực hiện trên iOS Notification.
    • NotificationAttributeIDNegativeActionLabel: nhãn(label) được sử dụng để mô tả hành động tiêu cực có thể được thực hiện trên iOS Notification.

    Sessions

    Một ANCS session bắt đầu khi NC đăng ký Notification Source characteristic trên NP và kết thúc khi NC hủy đăng ký khỏi dùng một chẩcteristic hoặc mất kết nối tới NP. Vì ANCS không được thiết kế để trở thành dich vụ đồng bộ hoàn chỉnh, nên nó không giữ lại các trạng thái qua các session(phiên). Do đó, tất cả các Identifiers (như NotificationUID, và AppIdentifier) và tất cả dữ liệu được trao đổi giữa NC và NP chỉ có hiệu lực trong một session cụ thể.

    Khi một session cụ thể kết thúc, NC nên xóa bỏ bất kể identifier và dữ liệu mà nó thu thập và lưu lại trong session đó. Khi một session mới được bắt đầu, NP sẽ cố gắng hết sức để thông báo cho NC về bất kỳ iOS Notification hiện có nào trên hệ thống. NC có thể sử dụng thông tin này để xây dụng mô hình để sử dụng phần còn lại của session.

    Attribute Fetching và Caching

    Apple đặc biệt khuyến nghị NC chủ fetch các thuộc tính khi cần và có thể đáp ứng với hành động của người dùng. Ví dụ nếu một NC chọn hiển thị iOS Notification đang hoạt động trong một danh sách đơn giản và chỉ hiển thị chi tiết cụ thể của iOS Notification khi được người dùng chọn, sau đó việc truy xuất thuộc tính của iOS notification này có thể được kích hoạt lazily.

    Tring một session, Apple khuyến nghị NC xây dựng bộ đệm(cache) của App Attribute cho mỗi app identifier mà nó gặp phải. Việc xây dựng cache cho phép NC tránh truy xuất cùng một App Attribute nhiều lần, vì vậy nó giúp tiết kiệm thời gian và mức tiêu hao pin.

    Error codes

    • Unknown command (0xA0): NP không nhận ra CommandID
    • Invalid command (0xA1): command không đúng định dạng
    • Invalid parameter (0xA2): một trong những tham số không tồn tại trên NP
    • Action failed (0xA2): hành động không được thực hiện

    Nếu NP trả lời với một lỗi, nó sữ không tạo ra bất kỳ GATT Notification nào trên Data Source characteristic cho command(lệnh) tương ứng.

    Example Diagrams

    Hai hình dưới đây cho thấy các ví dụ về tương tác phổ biến giữa NP và NC.

    Hình dưới đây cho thấy chuỗi các command và phản hồi điển hình cần thiết để thiết lập ANCS trên NC:

    Hình dưới đây thì hiển thị các chuỗi command và phản hồi cần thiết để có thêm thông tin về iOS Notification để hiển thị trên NC:

    Tổng kết

    Vậy là bài viết này chúng ta đã đi qua một loạt các thông tin của ANCS như ANCS dùng làm gì, nó có những service characteristic nào, các characteristic có định dạng như nào và rất nhiều thông tin liên quan đến việc triển khai ANCS.

    Bài viết tiếp theo liên quan đến ANCS mình sẽ đi vào ví dụ code cụ thể để các bạn có thể hiểu rõ hơn về cách hoạt động cũng như chức năng của ANCS.

  • iOS/Swift: NFC là gì? Cách quét thẻ NFC sử dụng CoreNFC

    iOS/Swift: NFC là gì? Cách quét thẻ NFC sử dụng CoreNFC

    Lời mở đầu

    Hôm nay mình sẽ giới thiệu với các bạn về công nghệ NFC. Công nghệ từng được dự đoán có thể thay đổi thế giới :D. Vậy NFC là gì? Nó được sử dụng như nào? Chúng ta sẽ đi vào chi tiết dưới đây.

    NFC là gì?

    NFC là viết tắt của Near-Field communications được hiểu là chuẩn kết nối không dây trong khoảng cách gần. NFC được phát triển dựa trên nguyên lý nhận dạng bằng tín hiệu tần số vô tuyến có tốc độ truyền tải dữ liệu tối đa 424 Kbps. Khoảng cách truyền dữ liệu tối đa của nó là khoảng 4 cm.

    Công dụng của NFC là để truyền dữ liệu ở phạm vi gần một cách nhanh chóng và an toàn. Hiện nay một số nước người ta cũng ứng dụng vào việc thanh toán hàng hóa cũng như đọc thông tin sản phẩm.

    Để sử dụng NFC chúng ta cần có thiết bị đọc (thường là điện thoại di động) và thiết bị đích(có thể là một điện thoại khác, 1 thẻ NFC…)

    NFC rất phổ biến ở các dòng điện thoại chạy hệ điều hành Android. Ngày NFC nổi đình nổi đám Apple vẫn không tin tưởng công nghệ này. Cho đến tận Iphone 7 NFC mới được apple thêm vào thiết bị của họ.

    Cách sử dụng CoreNFC Framework để quét NFC Tag

    Cho đến gần đây thì có vẻ Apple đã chú ý hơn tới công nghệ này. Vì vậy nó chỉ được hỗ trợ từ Iphone 7 trở lên và iOS >= 11.

    CoreNFC có nhiệm vụ phát hiện thẻ NFC và đọc thông điệp có chứa dữ liệu NDEF và lưu dữ liệu vào thẻ có thể ghi dữ liệu.

    Cài đặt môi trường

    Enable Near Field Communication Tag Reading

    Bấm vào Capability > Tìm Near Field Communication Tag Reading và add

    Lưu ý: Near Field communication Tag Reading chỉ hỗ trợ cho các tài khoản trả tiền. Chi tiêt xem tại: https://help.apple.com/developer-account/#/dev21218dfd6

    Thêm CoreNFC Framework

    Lưu ý bạn nên để CoreNFC Frameworkoptional. Nếu không nó sẽ bị crash app khi chạy trên các thiết bị không hỗ trợ.

    Thêm NFC description trong Info.plist

    Việc này nhằm mục đích mô tả cho người dùng biết sơ qua về tính năng này. Và khi upload ứng dụng lên store không bị apple reject.

    Vậy là việc setup môi trường đã xong. Chúng ta sẽ đi vào code thôi.

    Đầu tiên để sử dụng được chúng ta cần Import CoreNFC vào ViewController của bạn.

    import CoreNFC

    Thêm một biến global để lưu thông tin session reader

    var session: NFCNDEFReaderSession?

    Thêm 2 dòng code dưới đây vào viewDidLoad() trong trường hợp này mình đang để mỗi lần vào màn hình này là nó bắt đầu scan luôn. Các bạn có thể để 2 dòng này trong đoạn code mà các bạn mong muốn để chủ động hơn:

    session = NFCNDEFReaderSession(delegate: self, queue: DispatchQueue.main, invalidateAfterFirstRead: false)
    session?.begin()

    Lúc này XCode sẽ báo lỗi ViewController của bạn. Nó yêu cầu bạn phải conform NFCNDEFReaderSessionDelegate protocol.

    class ViewController: UIViewController, NFCNDEFReaderSessionDelegate 

    XCode tiếp tục báo lỗi, nó báo bạn đang thiếu các phương thức bắt buộc của NFCNDEFReaderSessionDelegate vì vậy ta thêm 2 func dưới đây:

    func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
        for message in messages {
            for record in message.records {
                if let string = String(data: record.payload, encoding: .ascii) {
                    print(string)
                }
            }
        }
    }
    
    func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
    
    }
    • didDetectNDEFs messages: Hàm này được gọi khi thiết bị của bạn tìm thấy và đọc được thẻ NFC ở gần đó. Từ hàm này chúng ta có thể sử dụng dữ liệu mà thẻ NFC cung cấp.
    • didInvalidateWithError: Hàm này trả về khi có lỗi xảy ra

    Vậy là chúng ta đã hoàn thành việc coding đọc thẻ NFC. Hãy build ứng dụng lên và trải nghiệm 😀

    Lưu ý: Trong quá trình test cũng như trải nghiệm tính năng này mình thấy các thiết bị của Apple có tầm khá ngắn. Và để quét được NFC tag các bạn phải dùng mặt trước của điện thoại. Mặt sau quét rất khó thành công.

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

  • iOS: Giảm thời gian khởi động ứng dụng

    iOS: Giảm thời gian khởi động ứng dụng

    Để tăng trải nghiệm của người dùng, khiến người dùng cảm thấy ứng dụng của chúng ta chạy nhanh hơn thì một trong những việc đó là giảm thời gian khởi động của chúng ta. Để người dùng có thể trải nghiệm tính năng chính một cách nhanh nhất.

    Refer

    Lời mở dầu

    Apple khuyến cáo chúng ta nên lập trình sao cho tổng thời gian khởi động ứng dụng dưới 400ms và bạn phải thực hiện trong vòng chưa đầy 20s nếu không muốn hệ thống tự dừng ứng dụng của bạn. Tuy nhiên thì vì nhiều lý do app chúng ta thường khởi động lâu hơn thế. Bên cạnh việc xử lý những tác vụ tiền khởi động ở trong AppDelegate thì chúng ta cũng cần phải biết cách debug việc khởi động chậm và biết được những gì đã xảy ra trước khi app chạy vào AppDelegate. Dưới đây là một số mẹo từ hội nghị WWDC 2016.

    Pre-main time

    Rất nhiều xảy ra trước khi hệ thống thực hiện chạy hàm main() của ứng dụng và gọi các hàm trong App Delegate như applicationWillFinishLaunching. Trước iOS 10, rất khó để chúng ta tìm hiểu tại sao một ứng dụng lại khởi động chậm vì những lí do khác ngoài code của bạn.

    Hiện tại từ iOS chúng ta có thể làm việc đó dễ dàng hơn băng việc thêm DYLD_PRINT_STATISTICS = 1 vào trong Environment variables của project scheme

    Chạy thử ứng dụng trên iPhone 8 ios 13 ta được kết quả như sau:

    Khi kiểm tra việc khởi động chậm chúng ta nên chọn thiết bị chậm nhất mà ứng dụng hỗ trợ.

    Kết quả cho chúng ta thấy tổng thời gian tính đến thời điểm hệ thông gọi hàm main() của ứng dụng theo sau đó là breakdown của các main steps.

    Các bạn xem clip này để tìm hiểu sâu hơn  WWDC 2016 Session 406 Optimizing App Startup Time

    Dưới đây là một số lưu ý mà mình đã đúc kết từ các tài liệu và clip trên để cải thiện thời gian khởi động ứng dụng:

    dylib loading time

    dynamic loader có nhiệm vụ tìm và đọc các thư viện(dylibs) được ứng dụng sử dụng. Mỗi thư viện có thể có các phụ thuộc(Dependencies). Cơ chế load framework hệ thống của Apple là rất tối ưu, tuy nhên thì khi load những thư viện được nhúng vào ứng dụng của bạn thì lại không như vậy. Để tăng tốc các dylib thì Apple khuyến cáo bạn bên hạn chế sử dụng các thư viện bên ngoài hoặc gộp chúng lại với nhau làm một framework.

    Rebase/binding time

    Là khoảng thời gian ứng dụng rebale và binding các pointer.

    • Ứng dụng chứa nhiều Objective-C class, selector hay category có thể tốn thêm thời gian cho việc khởi động ứng dụng.
    • Để tăng tốc rebase/binding time bạn cần sử dụng ít các pointer fix-ups hơn.
    • Nếu bạn sử dụng C++ thì nên sử dụng ít đi các virtual functions.
    • Trong Swift sử dụng Struct cũng thường nhanh hơn

    ObjC setup time

    Objective-c runtime cần thiết lập một vài tác vụ cho việc đăng ký class, category và phân biệt các selector. Vì vậy bất kể những cải tiến nào bạn thực hiện để rebase/binding time cũng sẽ áp dụng cho thời gian thiết lập này.

    Initializer time

    Là lúc các hàm khởi tạo chạy, nếu bạn sử dụng phương thức Objective-C +load(deprecated) hãy thay thế nó bằng +initialize.

    Các bạn cũng có thể sử dụng Instruments để theo dõi các chỉ số này.

    Sau cùng thệ thống sẽ gọi hàm main() tiếp đến gọi UIApplicationMain() và các phương thức trong AppDelegate.

    Loading framework làm tăng thời gian khởi động

    Để kiểm chứng chúng ta cùng theo dõi thử nghiệm dưới đây:

    Trước khi thêm thư viện:

    Total pre-main time: 408.97 milliseconds (100.0%)
         dylib loading time: 383.84 milliseconds (93.8%)
        rebase/binding time:   7.86 milliseconds (1.9%)
            ObjC setup time:   6.82 milliseconds (1.6%)
           initializer time:  10.36 milliseconds (2.5%)
           slowest intializers :
             libSystem.B.dylib :   2.33 milliseconds (0.5%)

    Sau khi thêm 10 thư viện Swift sử dụng Cocoapods

    Total pre-main time: 682.90 milliseconds (100.0%)
         dylib loading time: 631.17 milliseconds (92.4%)
        rebase/binding time:  17.06 milliseconds (2.4%)
            ObjC setup time:  17.47 milliseconds (2.5%)
           initializer time:  17.09 milliseconds (2.5%)
           slowest intializers :
             libSystem.B.dylib :   6.05 milliseconds (0.8%)

    Thời gian dylib loading time tăng lên từ 380ms đến 631ms. Các bạn có thể trực tiếp thử nghiệm trên chính ứng dụng của mình để thấy sự khác biệt.

    Tổng kết

    Bài viết trên mình đã chia sẻ cho các bạn về một số mẹo làm giảm thời gian khởi động ứng dụng. Giúp tăng trải nghiệm người dùng. Chúc các bạn thành công.

  • iOS/Swift: Một số kỹ thuật truyền dữ liệu phổ biến trong swift

    iOS/Swift: Một số kỹ thuật truyền dữ liệu phổ biến trong swift

    Lời mở đầu

    Trong Swift, chúng ta có khá nhiều cách để truyền dữ liệu qua lại giữa các đối tượng. Bài viết này mình muốn chia sẻ với các bạn về một số kỹ thuật phổ biến và ưu nhược điểm của nó. Bài viết này mình sẽ đề cập đến 3 cách phổ biến đó là Delegation, Closure và NotificationCenter Observation

    Bài toán

    Màn hình của mình cần làm có một UITableView, trong UITableView này chứa các UITableViewCell các cell thì có chứa 2 UIButton Dark và Light. Việc của mình phải làm là mỗi khi người dùng tương tác với button ở trong cell thì UIViewController sẽ bắt được sự kiện và hiển thị lên màn hình thông tin cell và hành động mà người dùng vừa tương tác.

    Các bạn tải về project bắt đầu này để tiện hơn trong quá trình thực hành nhé:

    Delegation

    Delegation là một design pattern cho phép đối tượng gửi thông điệp(data, message …) đến đối tượng khác khi có một sự kiện xảy ra. Ví dụ ta có 2 đối tượng A và B, trên B thực hiện hành động gửi thông điệp sang A để A thực hiện hành động dựa trên kết quả hành động trên B.

    Do chúng bài toán của chúng ta cần bắt hành động của người dùng khi họ action lên các button trên cell vì thế trong trường hợp này trong UITableViewCell chúng ta cần tạo protocol cho MyTableViewCell.swift cụ thể như sau:

    protocol ActionOnCell: class {
        func didTapDark(indexPath: IndexPath?)
        func didTapLight(indexPath: IndexPath?)
    }

    Ở đây mình tạo ra protocol định nghĩa các action trên cell. Khi người dùng bấm vào button Dark nó sẽ call protocol này tương tự với Button Light.

    Tiếp đến chúng ta tạo thêm 2 variables để lưu delegate và indexPath:

    var indexPath: IndexPath?
    weak var delegate: ActionOnCell?
    • indexPath để lưu lại cell mà ngươi dùng tương tác.
    • delegate để biết đối tượng nào đang conform protocol của MyTableViewCell

    Đối với delegate chúng ta cần khai báo weak để tránh tham chiếu strong dẫn đến Retain cycle, không giải phóng được các object này vì nó đang tham chiếu tới nhau.

    Tiếp đến chúng ta cần gọi các protocol tương ứng khi người dùng tương tác lên các button trên cell:

        @IBAction func dark(_ sender: Any) {
            if let delegate = delegate {
                delegate.didTapDark(indexPath: indexPath)
            }
        }
    
        @IBAction func light(_ sender: Any) {
            if let delegate = delegate {
                delegate.didTapLight(indexPath: indexPath)
            }
        }

    Vậy là chúng ta đã setup protocol cho MyTableViewCell.swift giờ tất cả những UIViewController nào sử dụng cell này đều có thể conform protocol của nó để bắt được event khi người dùng tương tác lên các button cell.

    Giờ chúng ta quay lại file ViewController.swift kéo xuống hàm cellForRowAt indexPath của tableview và update như sau:

        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "\(MyTableViewCell.self)", for: indexPath) as! MyTableViewCell
            cell.indexPath = indexPath
            cell.delegate = self
            return cell
        }

    Chúng ta gán lại giá trị indexPath cho các cell. Và set delegate cho Cell là self(viewController hiện tại)
    Lúc này XCode sẽ báo lỗi Cannot assign value of type ‘ViewController’ to type ‘ActionOnCell?’ vì chúng ta chưa conform protocol ActionOnCell trên ViewController.

    Vì vậy chúng ta sẽ tạo 1 extension của ViewController và conform ActionOnCell

    extension ViewController: ActionOnCell {
        func didTapDark(indexPath: IndexPath?) {
            showDark(indexPath: indexPath)
        }
    
        func didTapLight(indexPath: IndexPath?) {
            showLight(indexPath: indexPath)
        }
    }

    Khi người dùng tap vào button trên cell nó sẽ call protocol tương ứng và trả dữ liệu về cho viewController vì vậy bạn có thể sử dụng dữ liệu mà bạn muốn để update dữ liệu lên màn hình.

    Các bạn Build project sẽ nhận được kết quả như sau:

    NotificationCenter observation

    Là một cơ chế gửi thông báo cho phép truyền phát thông tin đến các đối tượng đã đăng ký quan sát notification đó.

    Chúng ta sẽ dựa trên 2 cơ chế post và observer của NotificationCenter để gửi và nhận dữ liệu giữa các đối tượng. Đối với notification thì chúng ta có thể sử dụng gửi thông tin từ một đối tượng đến nhiều đối tượng khác đã đăng ký theo dõi notification đó.

    Cụ thể trong bài toán này chúng ta sẽ làm như sau:
    Tạo 2 notification Name đinh nghĩa việc người dùng bấm vào Button Dark và Light:

    extension Notification.Name {
        static let didTapDarkNotification = NSNotification.Name(rawValue: "didTapDarkNotification")
        static let didTapLightNotification = NSNotification.Name(rawValue: "didTapLightNotification")
    }

    Mở file MyTableViewCell.swift thực hiện việc phát(post) thông báo tại 2 event của 2 button trên cell cụ thể như dưới đây:

        @IBAction func dark(_ sender: Any) {
            NotificationCenter.default.post(name: .didTapDarkNotification, object: indexPath)
        }
    
        @IBAction func light(_ sender: Any) {
            NotificationCenter.default.post(name: .didTapLightNotification, object: indexPath)
        }

    Nhiệm vụ của 2 hàm này là để khi nào người dùng bấm vào nut Dark hoặc Light thì NotificationCenter sẽ gửi đi một notification, những đối tượng nào đang lắng nghe các notification name đó sẽ nhận được thông điệp.

    Vì vậy chúng ta cần add observer cho ViewController.swift để nó nhận được thông tin khi người dùng tương tác lên các button trên cell.
    Chúng ta sẽ làm như sau:

        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
            NotificationCenter.default.addObserver(self, selector: #selector(didTapDark(noti:)), name: .didTapDarkNotification, object: nil)
            NotificationCenter.default.addObserver(self, selector: #selector(didTapLight(noti:)), name: .didTapLightNotification, object: nil)
        }
    
        override func viewWillDisappear(_ animated: Bool) {
            super.viewWillDisappear(animated)
            NotificationCenter.default.removeObserver(self)
        }

    Chúng ta cần đăng ký theo dõi notification ở viewWillApper và Remove theo dõi notification khi viewWillDisappear vì nếu chúng ta không remove view controller sẽ luôn nhận được thông báo khi mà nó còn trong view hierarchy dẫn đến những lỗi không mong muốn.

    // MARK: NotificationCenter handle
    extension ViewController {
        @objc func didTapDark(noti: Notification) {
            showDark(indexPath: noti.object as? IndexPath)
        }
    
        @objc func didTapLight(noti: Notification) {
            showLight(indexPath: noti.object as? IndexPath)
        }
    }

    Giờ build ứng dụng và các bạn sẽ nhận được kết quả tương tự như phần delegation

    Closure

    Nếu các bạn muốn tìm hiểu rõ hơn về Closure hãy đọc tài liệu tham khảo của Apple: https://docs.swift.org/swift-book/LanguageGuide/Closures.html

    Ý tưởng ở đây chúng ta sẽ tạo 1 closure callback có 2 tham số là action type và dữ liệu truyền lại là index path.

    Mở file MyTableViewCell.swift và thêm 1 enum định nghĩa các kiểu action trên cell

    enum ActionType {
        case dark
        case light
    }

    Tạo closure cho MyTableViewCell

    var didTapButton: ((ActionType, IndexPath?) -> Void)?

    Gọi closure khi người dùng bấm vào các nút trên cell

        @IBAction func dark(_ sender: Any) {
            if let didTapButton = didTapButton {
                didTapButton(.dark, indexPath)
            }
        }
    
        @IBAction func light(_ sender: Any) {
            if let didTapButton = didTapButton {
                didTapButton(.light, indexPath)
            }
        }

    Tiếp đến để nhận được event khi người dùng bấm vào các button chúng ta cần gán giá trị cho closure ở nơi mà bạn muốn. Cụ thể trong trường hợp này là ViewController.swift

        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "\(MyTableViewCell.self)", for: indexPath) as! MyTableViewCell
            cell.indexPath = indexPath
            cell.didTapButton = {[weak self] (action, index) in
                guard let self = self else {return}
                if action == .dark {
                    self.showDark(indexPath: index)
                } else {
                    self.showLight(indexPath: index)
                }
            }
            return cell
        }

    Giờ run ứng dụng chúng ta sẽ được kết quả như 2 cách trên.

    Đánh giá

    • Delegation: là cách dùng rất phổ biến và đễ sử dụng nhât khi truyền dữ liệu giữa các đối tượng. Nó dựa vào các protocol để truyền dữ liệu qua lại giữa các đối tượng. Thường dùng trong các trường hợp truyền dữ liệu dạng 1 – 1. Tuy nhiên nếu bạn muốn sử dụng delegate dạng 1 – n hãy tham khảo bài viết này https://magz.techover.io/2019/08/09/swift-design-patterns-multicast-delegate/
    • NotificationCenter: Là dạng truyền nhận dữ liệu 1 – n hoặc 1 – n. Và khi một thông báo được đẩy đi tất cả những đối tượng đã đăng ký nhận thông báo đó sẽ nhận được thông báo. Khi bạn dùng cần phải kiểm soát chặt các notification. Trong mỗi trường hợp khác nhau chúng ta lại có cách đăng ký notification và remove nó ở các chỗ khác nhau.
      VD:
      – Đối với các trường hợp bình thường chúng ta sẽ phải thêm notification ở ViewWillAppear và remove nó ở viewWillDisappear để đảm bảo lần viewWillAppear tiếp theo nó không đăng ký lại dẫn đến bị lặp nhiều lần.
      – Chúng ta không thể để ở viewDidLoad vì nếu remove ở ViewWillDisappear thì khi pop lại sẽ mất notification, nếu không remove thì khi nó vẫn còn trong view hierarchy chưa bị giải phóng nó sẽ vẫn nhận được notification mặc dù chungs ta không mong muốn điều đó.
      Trong tất cả thì cách này là cách rắc rối nhất. Bạn chỉ nên dùng khi muốn thực hiện dạng truyền dữ liệu 1-n
    • Closure: Là cách dùng khá phổ biến hiện tại, vì nó là cách viết khá gọn gàng vì vậy tiết kiệm được thời gian code. Tuy nhiên cách viết của nó khá khó hiểu với người mới. Các bạn cũng có thể nhận thấy sự phổ biến của nó thông qua các hàm completion, callback mà Apple đã sử dụng rất nhiều trong các API của họ.

    Tổng kết

    Mình hi vọng bài viết giúp cho các bạn có thể lựa chọn các cách truyền dữ liệu phù hợp với bài toán của mình. Chúc các bạn thành công!

    Nếu bài viết có vấn đề gì các bạn hãy comment xuống dưới để mình update nhé!

  • iOS/Testflight: Cách sử dụng iOS Beta testing

    iOS/Testflight: Cách sử dụng iOS Beta testing

    Lời mở đầu

    Ở bài viết trước mình đã hướng dẫn cho các bạn cách upload ứng dụng lên store. Nhưng ứng dụng nào trước khi đẩy lên store chúng ta cũng phải có giai đoạn kiểm thử. Điều đó đảm bảo ứng dụng của bạn phát sinh ít lỗi trong quá trình người dùng sử dụng cũng như đảm bảo ứng dụng chạy đúng theo yêu cầu. Vì vậy hôm nay mình sẽ chia sẻ với các bạn tất tần tật về Testflight ứng dụng được Apple phát hành với nhiệm vụ giúp tester dễ dàng tiếp cận bản build hơn.

    Testflight là gì?

    Testflight là một ứng dụng được Apple cung cấp để cho Developer của họ upload ứng dụng lên nhằm mục đích kiểm thử ứng dụng. Ứng dụng này gồm 2 phần: Tính năng quản lí cho developer nằm trong App store connect và ứng dụng Testflight là ứng dụng cho tester.

    Làm thế nào để upload bản build lên Testflight?

    Nếu các bạn chưa biết cách upload bản build của mình lên Testflight hãy đọc bài viết trước của mình nhé: iOS/App store connect: Các bước đẩy ứng dụng của bạn lên App store

    Định nghĩa các loại tester trên Testflight

    • Internal tester: Đây là những App store connect user được phân quyền Admin, App manager, legal, developer … có thể quyền truy cập vào quản lý ứng dụng của bạn. Thường thì đó là những người trong đội phát triển ứng dụng hoặc khách hàng. Số lượng tối đa cho internal tester là 25. Với mỗi lần bạn submit bản build lên App store connect hoàn thành, tất cả các internal tester sẽ nhận được thông báo về phiên bản mới nhất.
    • External tester: Đây là những người dùng không nằm trọng đội phát triển ứng dụng nhưng họn muốn trải nghiệm, kiểm thử ứng dụng của bạn. Các extenal tester không có quyền truy cập vào App store connect. Nhưng họ vẫn có thể tải và cài đặt phiên bản ứng dụng mà bạn muốn họ test. Hiện nay Apple đang giới hạn tối đa external tester là 10,000. Để external tester có thể cài đặt và kiểm thử ứng dụng của bạn thì bạn cần Submit bản build mà bạn muốn cho bên apple review gần giống như việc submit bản build lên app store. Khi bản build đó được approve thì tất cả các External tester sẽ nhận được thông báo và có thể tải và cài đặt ứng dụng dựa trên bản build đó.

    Internal testers

    Để thêm một internal tester bạn cần truy cập vào App store connect đăng nhập và truy cập vào mục Users and Assess

    Bấm vào dấu + để thêm mới một user:

    Điền đầy đủ thông tin và bấm invite để gửi email mời họ.

    Lưu ý: Mục email là tài khoản apple(apple id) vì vậy những người muốn vào nhóm internal tester phải có tài khoản apple. Khi bạn add user này vào đồng nghĩa họ cũng sẽ có quyền truy cập vào App store connect để quản lý ứng dụng của bạn. Vì vậy hãy chú ý mục phân quyền(Role) cho user mà bạn muốn add.

    App store connect sẽ gửi một thư mời tới user mà bạn vừa thêm. User này cần truy cập vào hòm thư của họ để mở thư mời và chọn Active your account.

    Khi đó bạn sẽ được điều hướng sang trang của apple và bạn đăng nhập vào để active tài khoản của bạn. Hoàn thành bước này là bạn đã add tài khoản đó vào App store connect.

    Nhưng để các tài khoản này có thể tải và cài đặt những bản build trên Testflight các bạn cần mời họ vào nhóm App store connect users(Internal tester). Vì vậy chúng ta sẽ chuyển qua mục My app

    Một danh sách các ứng dụng của bạn được hiện ra. Hãy chọn ứng dụng mà bạn đang muốn quản lý. Chuyển sang tab Testflight > App store connect user > bấm dấu + để thêm user vào App store connect user.

    Lúc này một danh sách các user hiện ra. Bạn hãy chọn User lúc trước bạn đã invite ở bước trên và bấm Add để Testflight gửi thư mời cho tài khoản đó.

    Thư mời sẽ nằm trọng hòm thư của người dùng mà bạn vừa gửi. Bấm vào View In Testflight và hoàn thành các bước đơn giản để hoàn tất việc add Internal tester. Thư mời có định dạng như sau:

    Vậy là hoàn tất việc add Internal tester. Từ giờ mỗi khi có bản build mới được upload lên TestFlight là người dùng này sẽ nhận được cả mail và thông báo.

    External testers

    Như đã giới thiệu ở trên, external tester là những user không thể truy cập vào App store connect của bạn nên chúng ta cần cung cấp thông tin bản kiểm thử cũng như cần chọn bản build cho apple review.

    Cập nhật thông tin kiểm thử

    Bạn mở mục Test information và điền đầy đủ các thông tin cho bản kiểm thử này. Nó là bắt buộc nên bạn cần phải điền đầy đủ thông tin cho nó. Nhớ bấm Save để lưu lại thông tin nhé.

    Hãy điền đầy đủ thông tin cho mục Test information

    Tạo nhóm External tester

    Chọn mục Add Group để thêm mới nhóm External testers:

    Đặt tên nhóm > Bấm Create để tạo nhóm tester mới.

    Khi tạo thành công sẽ hiển thị một giao diện như dưới

    Do nhóm vừa tạo nên chưa có tester nào. Để thêm tester chúng ta bấm vào dấu +

    • Add new testers: Đây là cách thêm thủ công, sử dụng khi ban muốn thêm số lượng ít tester
    • Add existing testers: Thêm các tester đã từng test ứng dụng rồi
    • Import from CSV: Sử dụng file CSV để thêm tester, sử dụng khi bạn muốn thêm số lượng lớn các tester

    Ở bài này mình sẽ chỉ nói đến Add New Testers. Bạn điền thông tin của tester và bấm add để invite họ vào group.

    Lúc này 1 thư mời sẽ được gửi tới hòm thư của email đó họ chỉ cần accept là được.

    Chọn bản build dành cho external tester

    Chuyển sang mục Builds và bấm vào dấu + để chọn bản build bạn muốn external tester kiểm thử.

    Một danh sách các bản build được hiển thị, chọn bản mà bạn muốn test. Và bấm Add

    Lúc này bạn đã Add thành công, giờ chờ để apple review.

    Lúc này trạng thái của bản build đang là waiting for review có nghĩa là ứng dụng của bạn đang trong hàng đợi để nhân viên của apple review. Để được approve bản build của bạn phải không vi phạm điều luật apple đã đưa ra trong App Store Review Guideline. Việc review sẽ tốn một khoảng thời gian lớn tùy thuộc vào ứng dụng của bạn và thời gian bạn submit. Tuy nhiên một khi Apple đã approve cho version của bạn thì những bản build tiếp theo sẽ không phải review cho đến khi bạn thay đổi version.

    Sau khi apple họ Approve bản build của bạn, một thông báo sẽ được gửi cho tất cả các tester trong nhóm. Để họ có thể tài và cài ứng dụng lên thiết bị của họ.

    Vậy là chúng ta đã hoàn thành việc tạo nhóm external tester và thêm bản build cho nhóm external tester.

    Tester cần làm gì?

    Tải ứng dụng TestFlight trên App store

    Khi có bản mới thông báo sẽ báo về mail các bạn chỉ cần bấm vào để sử dụng. Khi đó bạn sẽ được chuyển qua ứng dụng TestFlight và có thể Tải cũng như cài đặt ứng dụng.

    Tổng kết

    Bài viết này mình đã chia sẻ với các bạn về cách thêm internal tester, external tester để hỗ trợ việc tester kiểm thử ứng dụng. Hi vọng nó giúp các bạn phần nào bớt bỡ ngỡ khi tiếp cận TestFilght.

    Ngoài ra còn một số cách đẩy bản build cho tester kiểm thử khác không phải của apple cũng khá được ưa chuộng.
    Các bạn có thể tham khảo bài viết: https://magz.techover.io/2019/12/17/cach-upload-1-app-ios-len-deploygate/