Category: Swift

  • [Swift] Extensions

    [Swift] Extensions

    Extensions – có tác dung đúng như tên gọi của nó, là dùng để mở rộng những class, struct, enum hoặc protocol đã được define.
    Nó cho phép bạn mở rộng cả những class, struct mà bạn không có quyền truy cập để sửa source code như Int, String,.. hay UICollectionView, UITableView,…
    Bài viết này sẽ nói về extensions trong swift và những cách sử dụng căn bản.

    Content

    • Extension syntax
    • Initializes
    • Methods
    • Conform to protocol, delegate

    Extension syntax:

    Khai báo extensions với extension keyword:

    extension SomeType {
        // new functionality to add to SomeType goes here
    }

    Công dụng của extension:

    • Add thêm các property, computed property.
    • Add thêm các methods.
    • Cung cấp các initializers mới.
    • Định nghĩa subscripts
    • Định nghĩa và sử dụng các nested type
    • Extend 1 class để làm class đó conform các protocol, delegate.

    Initialize:

    • Extension có thể dùng để thêm các initialize mới cho các kiểu dữ liệu đã được define.
    • Đối với class, ở extension thì chỉ có thể thêm các convenience init mới, không được thêm các init hoặc deinit:
    designated init và deinit không được đặt tại extension của 1 class
    • Đối với struct thì có thể thêm các init, deinit vào extension:

    Methods:

    • Sử dụng extension để thêm các func mới cho 1 existing type:
    class Human {
        func eat() {
            print("Eating")
        }
    }
    
    extension Human {
        func play() {
            print("Playing")
        }
        
        func sleep() {
            print("Sleeping")
        }
    }
    
    let me = Human()
    me.eat()
    me.play()
    me.sleep()
    
    • Đối với value type như struct và enum, nếu func được thêm vào mà làm thay đổi giá trị của property của struct, enum thì func đó phải được define là mutating.
      Ở ví dụ dưới đây, vì func changeName thay đổi property name, nên func đó phải đc define là mutating bằng cách thêm mutating keyword vào đằng trước func.
    struct Dog {
        var name: String
    }
    
    extension Dog {
        mutating func changeName(newName: String) {
            self.name = newName
        }
    }
    
    • Có thể dùng extension để add thêm các func cho những source code mà bạn không được truy cập đến:

    Ở đây, bạn add thêm 1 func square để tính gía trị bình phương cho struct Int.
    Vì vậy, bất cứ 1 instance nào có kiểu dữ liệu là Int đều có func square mà bạn add thêm.

    Conform to protocol, delegate

    • 1 công dụng nữa của extension là extend 1 class để làm cho class đó conform 1 protocol, delegate:

    Thông thường, có thể làm 1 class conform protocol bằng cách viết:

    class AController: UIViewController, UICollectionViewDataSource. UICollectionViewDelegate {
    
    }

    Hoặc sử dụng extension:

    class AController: UIViewController {
    
    }
    
    // MARK: - UICollectionViewDataSource
    extension AController: UICollectionViewDataSource {
    
    }
    
    // MARK: - UICollectionViewDelegate
    extension AController: UICollectionViewDelegate {
    
    }

    Note: Nên tách ra thành nhiều extension khi extend 1 class để làm class đó conform protocol, delegate để clear và dễ dàng đọc code hơn.

    Kết luận:

    • Dùng extension để làm cho source code dễ đọc và clear hơn.
    • Dùng extension để add thêm các func cho những class như ViewController, TableView, … hoặc struct như Int, String,… để có thể tái sử dụng, tránh duplicate code.

    Ngoài ra, extension còn dùng để add thêm các nested type, các subscrip, cách làm cũng tương tự như khi add thêm method, property.

    Có thể tham khảm thêm tại: https://docs.swift.org/swift-book/LanguageGuide/Extensions.html

  • Access Control in Swift

    Access Control in Swift

    Content

    1. Giới thiệu về Acess Control
    2. Các loại access control
    3. Subclassing
    4. Getter/Setter
    5. Protocols

    Các loại access control

    Swift cung cấp 5 loại access control:

    Open access:

    • Là loại access có độ truy cập cao nhất.
    • Nếu 1 entity có access level là open, thì nó có thể được truy cập từ bất cứ nơi nào trong module mà thực thể đó được định nghĩa, và cả từ 1 module khác bên ngoài mà đang import module đó.
    • Khi truy cập từ module bên ngoài hoặc bên trong thì đều có thể kế thừa, override,… lại entity đó. > Note: Access level open thường được sử dụng cho các framework, dể bạn có thể dễ dàng kế thừa, override, sử dụng các phương thức của framework đó từ trong project của bạn.

    Public access

    • Nếu 1entity có access level là open, thì nó có thể được truy cập từ bất cứ nơi nào trong module mà thực thể đó được định nghĩa, và cả từ 1 module khác bên ngoài mà đang import module đó.
    • Level access thấp hơn open bởi khi truy cập từ module bên ngoài thì không cho phép kế thừa, override,… lại entity đó.

    Internal access

    • 1 entity có access level là internal thì sẽ chỉ được sử dụng tại bất cứ nơi nào trong module định nghĩa entity đó, nhưng không thể sử dụng bên ngoài module định nghĩa nó.
    • 1 entity nếu nếu không được set access level cụ thể thì mặc định access level là internal.
    name và func printName có access level mặc định là internal.

    Fileprivate

    • 1 entity có access level là fileprivate thì sẽ hạn chế quyền truy cập của module định nghĩa tới entity đó. Entity đó chỉ cho phép truy cập tới nó từ bên trong swift file định nghĩa nó.
    class A có property name kiểu fileprivate, property age là kiểu internal.

    class B không thể truy cập được đến property name của A bởi A và B nằm ở 2 file swift khác nhau và property name có access level là fileprivate.
    -> property name chỉ có thể truy cập được trong classA.swift file.

    Private

    • Là access level có độ truy cập thấp nhất.
    • 1 entity nếu có access level = private thì sẽ chỉ có thể truy cập được từ class khai báo entity đó, hoặc từ extension của class đó nhưng phải trong cùng 1 swift file. > Bạn nên dùng private access để giấu các phương thức, các biến cụ thể khỏi bên ngoài khi các phương thức, biến đó chỉ được dùng từ bên trong nơi định nghĩa chúng.
    classA.swift
     class A {
      private var name = ""
     }
     
     class C {
        let a = A()
     
        func printNameOfA() {
            // Không thể truy cập được property name ở đây vì C 
              không phải class định nghĩa name.
        }
     }
     
     extension A {
         func printName(){
            print(name)
         }
     }
     
     classB.swift
     extension A {
        func printName() {
           // Không thể truy cập được property name ở đây vì extension 
              của A không được khai báo cùng swift file với property name.
        }
     }

    Subclassing

    • bạn có thể subclass bất cứ class nào có thể truy cập được trong phạm vi truy cập.
    • 1 subclass thì không thể có access level cao hơn so với super class.
    • Khi override lại các phương thức, class con có thể override lại với 1 access level mới cao hơn so với access level của phương thức của super class.

    Getter/Setter

    • Getters và setter mặc định nhận access level của các biến, property,… mà chúng thuộc về.
    • Bạn có thể làm cho setter có access level thấp hơn getter bằng cách cung cấp access level riêng cho setter.
    Ở đây. bạn tạo access level private cho property name nên không thể gán 1 giá trị mới cho property name từ bên ngoài class.

    Protocols

    • Các func, properties bên trong protocol sẽ chỉ có thẻ có cùng access level cùng với protocol đó.
    • Nếu bạn tạo ra 1 protocol mới kế thừa từ 1 protocol đã có, thì protocol mới phải có access level thấp hơn so với protocol đã có.
    • 1 class có thể conform to 1 protocol có access level thấp hơn class đó.

    Hi vọng bài viết đã tổng quát lại được những kiến thức hữu ích về access control trong swift.

  • The Application’s Life Cycle

    The Application’s Life Cycle

    Application’s life cycle – Vòng đời của 1 chương trình. Đây là 1 phần cơ bản nhưng cực kỳ quan trọng trong việc lập trình một ứng dụng.  Tuy có thể coi là 1 kỹ năng trấn phái nhưng không phải ai cũng nắm được rõ và đẩy đủ về vòng đời này (minh chứng là vẫn bị tester bắt nhiều bug về các case abnormal liên quan đến các state của app). Chính vì thế mình viết bài này để có thể giới thiệu 1 cách chi tiết về vòng đời của app và cách sử dụng để tránh những lỗi không đáng có. 

    iOS App life cycle - Brian - Medium


    Một application sẽ có các trạng thái như sau: 
    Not running: Là trạng thái application chưa được bắt đầu hoặc đã chạy nhưng bị terminated bởi system. 
    Inactive: Application đang chạy ở Foreground nhưng không nhận bất cứ sự kiện tương tác nào và cũng không thể xử lý các sự kiện (có thể là bị một vài sự kiện tác động vào trong quá trình chạy, ví dụ như có cuộc gọi đến hay tin nhắn chẳng hạn). 1 app cũng có thể ở trong trạng thái này khi chuyển từ state này sang state khác. 
    Active: Application đang chạy ở Foreground và đang nhận các sự kiện bình thường. Cách duy nhất để đến trạng thái Active là thông qua Inactive. Ở trạng thái này, khi người dùng tương tác với UI, họ có thể nhìn thấy phản hồi cho những hành động của họ.
    Background: Application đang chạy ở background và đang thực thi code. Ở trạng thái này UI của app không được hiển thị nhưng mà nó lại vẫn đang chạy (nếu có đăng ký background task với OS). Hầu hết các app chuyển trạng thái sang suspended thông qua trạng thái này.
    Suspended: Application đang chạy ở background nhưng không thể thực thi code. Thường thì sẽ do chính system sẽ tự động đưa app về trạng thái này và lúc đó app vẫn đang trong memory. Trong trường hợp low memory, hệ thống có thể sẽ tự kill app của mình khi app đang ở trạng thái suspended mà không thông báo gì. 
    Lưu ý rằng: Theo chuẩn của Apple thì chỉ hệ thống mới có thể kill app. 


    Về cơ bản thì 1 application có các trạng thái như trên, và trong app chúng ta cũng có các event tương ứng để được notify khi bắt đầu hay đã vào các trạng thái trên. Các hàm đó được list trong AppDelegate. Các trạng thái chuyển đổi qua lại được gọi là transition giữa các trạng thái.
    – application:willFinishLaunchingWithOptions  ——   Method này được gọi sau khi app của chúng ta khởi chạy thành công. Nó là method đầu tiên được chạy từ app delegate. Chúng ta có thể thực thi các đoạn code nếu khởi chạy thành công.
    – application:didFinishLaunchingWithOptions    ——   Method này được gọi trước khi window của app được hiển thị. Bạn có thể hoàn thiện giao diện của mình và cung cấp root viewcontroller cho window.
    applicationDidBecomeActive ——   Method này được gọi để báo cho app của bạn biết khi nó chuyển trạng thái từ In-Active sang Active hoặc hệ thống và user khơi động app hoặc trong trường hợp user bỏ quan các gián đoạn làm app ngay lập tức chuyển sang In-Active (như là có cuộc gọi đến hoặc tin nhắn). Bạn nên dùng method này để chạy lại các tác vụ đang bị dừng (hoặc chưa chạy) khi app bắt đầu chạy lại.
    applicationWillResignActive ——   Method này được gọi để báo cho app biết rằng nó sắp chuyển từ trạng thái Active sang In-Active . Nó xãy ra khi trường hợp bị gián đoạn (có cuộc gọi tới hoặc SMS) hay là khi user tắt app đi. Bạn nên dùng method này để dừng các task đang chạy hoặc vô hiệu hoá timer trong app, hoặc nhiều thứ khác 
    applicationDidEnterBackground  ——   Method này được gọi để báo cho app biết nó đang không chạy ở dưới foreground. Bạn có khoảng tầm 5 giây để thực thi các task . Trong trường hợp bạn muốn có nhiều thời gian hơn để xử lý, bạn có thể yêu cầu hệ thống cấp cho thời gian thực thi bằng cách gọi hàm beginBackgroundTask(expirationHandler:) . Nếu như method của bạn không được thực thi và trả về trước thời gian hết hạn thì app sẽ bị hệ thống chấm dứt và xoá khỏi bộ nhớ.
    applicationWillEnterForeground  ——   Method này được gọi như là 1 phần trong việc chuyển trạng thái từ Background sang Active. Bạn nên dùng method này để hoàn thành các thay đổi đối với app trước khi nó xuống Background. applicationDidBecomeActive sẽ được gọi ngay khi method này đã hoàn thành việc chuyển trạng thái của app từ In-Active sang Active.
    applicationWillTerminate  ——   Method này được gọi khi app của bạn sắp bị hệ thống khai tử khỏi bộ nhớ. Bạn nên dùng method này để thực thi các tác vụ dọn dẹp. Bạn có tầm khoảng 5 giây để thực thi tác vụ. Nếu hàm của bạn không trả về trước thời gian hết hạn, hệ thống sẽ tự động khai tử app kèm cã task đang thực thi của bạn khỏi bộ nhớ. Method này cũng được gọi trong trường hợp app đang chạy ở dưới background( không bị suspended) nhưng hệ thống lại cần phải huỷ nó vì vài lí do gì đó. Bạn không nên đợi applicationWillTerminate được gọi rồi mới lưu lại data. Trong 1 vài trường hợp hi hữu, applicationWillTerminate sẽ không được gọi trước khi app bị khai tử (Vd như trong trường hợp máy của bạn reboot lại thì method này sẽ không được gọi).

    Đó là tất cả về iOS Application’s life cycle.
    Cảm ơn mọi người đã theo dõi bài viết. Hi vọng bài viết này có thể giúp ích cho các bạn.
    Mọi ý kiến đóng góp các bạn vui lòng comment ở bên dưới để mình có thể hoàn thiện hơn ở các bài viết sắp tới.

    Thanks all from with love <3
    KhanhVD1.

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

  • ARC in Swift

    ARC in Swift

    Việc quản lí bộ nhớ trong mỗi ứng dụng là rất quan trọng. Nếu ứng dụng của bạn sử dụng dữ liệu bừa bãi 1 cách không cần thiết thì sẽ làm memory của ứng dụng tăng cao, và trong trường hợp xấu có thể dẫn đến crash.
    Swift sử dụng ARC để hỗ trợ developer quản lí ứng dụng của app. Ở bài viết này mình sẽ nói về ARC trong swift.

    Contents:

    • ARC là gì
    • Cách ARC hoạt động
    • Strong reference
    • Xử lí strong reference
    • weak reference

    ARC là gì

    • ARC là viết tắt của Automatic Reference Counting. Swift sử dụng ARC để giúp bạn việc quản lý bộ nhớ của app.
    • ARC sẽ tự động giải phóng bộ nhớ được sử dụng bởi 1 biến khi biến đó không còn được sử dung nữa.
    • Tuy nhiên, để ARC có thể biết được khi nào biến đó không còn dc sử dụng, thì ARC phải biết về các relations giữa biến đó vs các biến khác trong code của bạn.

    Note: ARC chỉ áp dụng cho các biến kiểu class(reference type).

    Cách ARC hoạt động

    • Mỗi khi bạn tạo 1 biến, ARC sẽ tự chia sẻ 1 phần bộ nhớ để lưu trữ biến đó. Khi biến đó không còn được sử dụng, ARC giải phóng biến, lấy lại bộ nhớ để dùng cho những mục đích khác
    • Để xác định 1 biến không còn được sử dụng, ARC sẽ đếm số lượng strong reference hiện tại của biến đó đối với các biến khác. Chỉ khi số lượng strong reference = 0 thì biến đó mới được giải phóng bộ nhớ.

    Vậy strong reference là gì?

    Strong reference

    Mỗi khi bạn gán 1 biến kiểu class cho 1 property, constant hoặc variable, thì những thứ đó tạo 1 strong reference đến biến đó.

    class Car {
        var price: Double
        init(price: Double) {
            self.price = price
        }
        
        deinit {
            print("Car is being deinitialized")
        }
    }
    
    var hondaCar: Car? = Car(price: 50000.0)

    Ở đây, bạn gán biến Car(price: 50000.0) cho varialbe hondaCar
    -> hondaCar đã tạo 1 strong reference đến Car(price: 50000.0)

    Thêm dòng code sau và run thử trên playground:

    hondaCar = nil

    Kết quả thu được ở màn hình console như sau:

    Nếu bạn set honedaCar = nil -> Khi đó Car(price:50000) sẽ không còn strong reference nào trỏ đến -> ARC sẽ giải phóng bộ nhớ cho Car.
    Tiếp tục thử chạy đoạn code sau và quan sát màn hình console:

    Chạy đoạn code trên, bạn sẽ không thấy được print ra "Car is being deinitialized"…
    Vì các biến car1, car2, car3 là reference type, nên ta có sơ đồ relation ship như sau:

    Khi đó nếu chỉ set car1, car2 = nil, thì Car vẫn còn 1 strong reference từ car3 trỏ đến -> ARC sẽ không giải phóng bộ nhớ cho Car trong trường hợp này.

    Strong reference giữa các class

    Trong ví dụ trên, sẽ tạo ra 1 sơ đồ relation giữa các instance như sau:

    • Việc set property departmentowner của 2 instance tuangotham đã tạo ra 2 strong reference trỏ đến nhau.
    • Khi đó Person và Departmen, mỗi thứ sẽ có 2 strong refernce trỏ đến. Vì vậy, khi bạn chỉ set tuan = nilgotham = nil thì Person và Departmen vẫn sẽ không được xóa khỏi bộ nhớ dù không cần dùng đến nữa.
      -> Trường hợp này gọi là retain cycle -> Tạo ra rò rỉ bộ nhớ (memory leaks).

    Xử lí strong reference

    Swift cung cấp 2 cách để xử lí strong reference cycle khi bạn làm việc với các biến kiểu class:

    • weak reference
    • unowned reference

    Weak reference

    • weak refernce: thay vì tạo 1 strong reference, nó sẽ chỉ tạo 1 weak -> ARC sẽ không làm tăng số lượng strong reference trỏ tới biến đó.
    • Sử dụng weak reference khi biến còn lại có thời gian tồn tại ngắn hơn(thường bị giải phóng bộ nhớ trước)
    class Person {
        weak var department: Department?
        
        deinit {
            print("Person is being deinitialized")
        }
    }

    Sửa lại ví dụ trên bằng cách sửa department thành kiểu weak( vì department có thời gian tồn tại ngắn hơn người).
    Khi đó, sơ đồ relation ship sẽ thành:

    • set gotham = nil -> Departmen k còn strong reference nào trò đến -> được giải phóng khỏi bộ nhớ, khi đó strong reference của Departmen trò đến Person cũng sẽ bị mất.
    • set tuan = nil -> Person không còn strong refernce nào trỏ đến -> ARC giải phóng khỏi bộ nhớ.
      Kết quả thu được:

    Unowned reference:

    • Tương tự như weak refernce, nó sẽ không tạo ra 1 strong reference.
    • Khác weak reference ở chỗ, unowned dùng cho những biến có thời gian tồn tại lâu hơn.

    Note: nếu bạn truy cập đến 1 biến unowned sau khi nó đã được giải phóng khỏi bộ nhớ -> app sẽ crash. Vì vậy hãy cẩn thận khi dùng unowned.
    Sửa ví dụ trên thành như sau và sẽ vẫn thu được kết quả tương tự so với khi dùng weak:

    class Department {
        unowned var owner: Person?
        
        deinit {
            print("Department is being deinitialized")
        }
    }

    Hi vọng bài viết trên đã đem lại cho bạn cái nhìn tổng quan về ARC và cách sử dụng weak/unowned để tránh retain cycle giữa các class.

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

  • Deep dive into Memory Leaks Swift

    Deep dive into Memory Leaks Swift

    Đa số với mỗi lập trình viên đều đã gặp phải những vấn đề về memory leaks.
    Ở bài viết này, mình sẽ đi sâu vào memory Leaks và cách xử lí.
    Bài viết này đòi hỏi sự hiểu biết về weak/strong reference, retain cycle và ARC trong swift.

    Contents:

    • Memory leaks là gì?
    • Xử lí memory leaks bằng weak/unowned
    • Non-escaping closure vs escaping closure
    • Delay Deallocation
    • Optional self vs Unwrapped self
    • Example

    Memory leaks là gì?

    • Memory leaks là 1 phần bộ nhớ bị chiếm vĩnh viễn và không được giải phóng mặc dù không cần dùng đến -> Dẫn đến không thể tái sử dụng phần bộ nhớ này.
    • Thường xảy ra do retain cycle.

    Memory leaks gây ra những gì:

    • memory của ứng dụng tăng cao không cần thiết -> dẫn đến memory warning và có thể crash.
    • Những object bị leaks sẽ không bị hủy bỏ -> object đó sẽ luôn lắng nghe thông báo và sẽ thực hiện phản ứng mỗi khi nhận thông báo -> Dẫn đến sai lệch kết quả, có thể đặc biệt nghiêm trọng nếu ảnh hưởng đến database.

    Memory leaks demo

    Khởi tạo 2 View Controller như sau:

    • VC1 có 1 button để push sang VC2.
    • VC2 có hàm deinit để print ra "VC2 was deallocate from memory" khi VC2 được giải phóng bộ nhớ.

    Build thử, tap vào button để push sang VC2, và tap vào nút back để quay lại VC1. Quan sát màn hình console, có thể thấy không có gì được print ra.

    Ở đây, VC2 không được giải phóng bộ nhớ bởi VC2 giữ 1 strong reference đến closure completion, closure completion cũng giữ 1 strong reference đến VC2 -> Tạo ra 1 retain cycle -> Memory leaks.

    Xử lí memory leaks bằng weak/unowned

    Cách giải quyết mà mọi người thường dùng nhất là sử dụng weak/unowned.
    Vậy sự khác biệt giữa weak và unowned là gì?

    • Giống: Weak và unonwed tạo ra 1 weak reference thay vì 1 strong reference để loại bỏ retain cycle -> Bộ nhớ sẽ dc giải phóng ngay lập tức khi không cần dùng đến.
    • Khác:
    WeakUnowned
    – Có thể nilKhông thể nil

    Tuy nhiên, unowned cũng giống như việc force unwrapping self và cố gắng truy cập đến contents của nó ngay cả sau khi self đã được giải phóng -> dẫn đến crash.

    Crash do truy cập đến self trong khi self đã được xóa khỏi bộ nhớ

    Vì vậy ta thường thấy weak self được sử dụng nhiều hơn. Thay đoạn code viewDidLoad ở VC2 bằng:

    override func viewDidLoad() {
        super.viewDidLoad()
        completion = { [weak self] in
            self?.view.backgroundColor = .red
        }
    }

    Build và chạy thử. Sau khi pop từ VC2 về VC1, màn hình console đã hiện "VC2 was dellocated from memory" -> Memory leaks đã được giải quyết.

    Tuy nhiên câu hỏi ở đây là, liệu có cần sử dụng weak cho mọi closure?

    Non-escaping closure vs escaping closure

    Để trả lời câu hỏi trên, có 1 vài điều trước hết bạn cần phải biết.

    • non-escaping closure (ví dụ higher-order functions như compactMap): Được thực hiện trong 1 phạm vi thân hàm nhất định, được thực hiện ngay lập tức và sau khi thực hiện thì được giải phóng, không được lưu lại.
    • escaping closure: Được lưu lại, có thể truyền đi như 1 biến, và có thể được thực hiện lại vào 1 thời điểm khác trong tương lai.

    Closure sẽ tạo ra 1 strong reference đối với những thứ được đóng gói bên trong closure, trong ví dụ ở đây là self.

    • Đối với non-escaping closure: strong reference này sẽ chỉ tồn tại trong thời gian closure đó dc thực hiện -> Khi closure thực hiện xong, strong reference biến mất -> self không còn strong reference nào trỏ đến nên được giải phóng khỏi bộ nhớ.
    • Đối với escaping closure: Nếu escaping closure này đóng gói 1 self bên trong, thì strong reference này sẽ tồn tại mãi mãi -> tạo ra retain cycle. -> self không được giải phóng.
      Demo 2: Thay đoạn code viewDidLoad của VC2 thành và run thử.
    override func viewDidLoad() {
        super.viewDidLoad()
        let nonEscapingClosure = {
            self.view.backgroundColor = .red
        }
    }
    Tuy closure này đóng gói self bên trong nhưng khi back về VC1, VC2 vẫn được giải phóng khỏi bộ nhớ.

    Ở đây bạn tạo ra 1 non-escaping closure, nó tạo ra 1 strong reference tới self, khi kết thúc thân hàm, strong reference này sẽ biến mất -> Không bị retain cycle. Vì vậy, không cần dùng weak self trong trường hợp này.

    Delay Deallocation

    Gỉa sử có 1 func download image từ internet như sau:

    Ở đây URLSession.shared.dataTask là 1 non-escaping closure cần nhiều thời gian để chạy.

    Non-escaping closure không yêu cầu bạn phải dùng weak self để tránh retain cycle, tuy nhiên với những closure cần nhiều thời gian chạy như trên, thì sẽ tạo ra 1 khoảng thời gian delay trước khi VC2 được deallocate rất lớn -> Nếu ng dùng push và pop vào VC2 liên tục thì vẫn tạo ra tăng memory.

    Note: Cân nhắc việc sử dụng weak self đối với non-escaping closure!

    Optional self vs Unwrapped self

    Dùng weak self sẽ làm cho self thành kiểu optional. Khi đó sẽ thường có 2 kiểu giải quyết:

    • Dùng optional self: self?
    • Dùng unwrapped self

    Vậy 2 cách này có khác gì nhau?

    • Khi unwrapped self, thì sẽ chỉ kiểm tra xem self có tồn tại 1 lần duy nhất ở đầu thân hàm, nếu self khác nil thì sẽ tạo 1 strong reference tồn tại trong thời gian chạy hàm. Khi hàm kết thúc, strong reference biến mất, khi đó VC2 mới được deallocated mặc dù đã pop về VC1. -> Vẫn tạo ra 1 delay deallocated, không có retain cycle.
    • Dùng self? thì sẽ check mỗi lần gọi đến self. Nếu self đã dc giải phóng khỏi bộ nhớ thì trình biên dịch sẽ bỏ qua dòng code đó. -> Không tạo ra delay deallocated, không có retain cycle.

    Examples

    Grand Central Dispatch

    • GCD được khởi tạo và thực hiện ngay lập tức, không được lưu lại nên là non-escaping closure, không cần dùng weak self.
      -> Đó là lí do không cần dùng weak self ở main.async
    DispatchQueue.main.async {
        self.view.backgroundColor = .yellow
    }

    UIView.Animate

    UIView.animate(withDuration: 0, animations: {
        self.view.backgroundColor = .yellow
    }, completion: nil)

    Tương tự như GCD, closure trong UIView.animate cũng là 1 non-escaping closure nên không cần weak self.

    Timer

    let _ = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (_) in
         self.view.backgroundColor = .yellow
    }

    Timer sẽ tạo ra 1 strong reference và retain cycle nếu thỏa mãn 2 điều kiện:

    • Timer chạy lặp lại liên tục.
    • Timer capture lại self trong block closure

    -> Nếu thoả mãn 2 điều kiện trên thì phải dùng weak self đối với Timer.

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

  • Basic Test in Swift (P1)

    Basic Test in Swift (P1)

    Trong lập trình, để có thể tự kiểm tra app để hoạt động chính xác hay chưa, thì ta phải test hết toàn bộ các case xảy ra. Tuy nhiên, nếu làm bằng 1 cách test thủ công thì rất tốn thời gian.
    Để có thể test tự động, thì có thể dùng Unit test. Vậy ở bài viết này, mình sẽ giới thiệu về cách sử dụng Unit test trong lập trình iOS.

    Nội dung bài viết

    • Unit test là gì?
    • Giới thiệu về unit test trong iOS
    • Useful Test
    • Khởi tạo Test Target
    • Run the test
    • Functional test Demo

    Unit test là gì?

    • Là phương pháp dùng để kiểm tra tính đúng đắn của một đơn vị source code. Một Unit (đơn vị) source code là phần nhỏ nhất có thể test được của chương trình, thường là một phương thức trong một lớp hoặc một tập hợp các phương thức thực hiện một mục đích thiết yếu.
    • Bạn viết các test case để Xcode tiến hành test các test case đó.

    Giới thiệu về Unit test trong iOS

    Xcode cung cấp 3 kiểu test chính:

    • Functional test: tập trung vào test các func.
    • Performance tests: tập trung vào đo lương thời gian app của bạn thực thi xong các task trên các loại thiết bị khác nhau.
    • User Interface tests (UI Tests): tập trung vào những tác vụ của người dùng trên UI.

    Note:

    • Functional test & performance test: Là những đoạn test mà bạn tự viết để test sự hoạt động của các func trong app.
    • UI Test: Ghi lại những thứ mà bạn tương tác trên UI của app.

    Useful Test

    1 test case được coi là useful khi:

    • Test case phải có khả năng fail: Nếu test đó không thể fail, thì việc test sẽ rất vô giá trị, bạn nên xốa nó đi.
    • Test case phải có khả năng success: Nếu test đó không thể success, thì việc test sẽ rất vô giá trị, tương tự ở trên, bạn nên xốa nó đi.
    • Test case phải được refactor và được viết đơn giản

    Khởi tạo 1 Test Target

    Để viết được test, trước hết cần tạo 1 Unit test target để viết test:

    Điền tên cho class rồi chọn Next.

    1. Xcode imports sẵn cho bạn frameworks XCTest và class được tạo là subclass của XCTestCase.
    2. func setUp(): dùng để thiết lập lại trạng thái trước mỗi lần test.
    3. func tearDown(): dùng để thực hiện dọn dẹp sau khi mỗi lần test kết thúc.
    4. measure: dùng để đo thời gian thực hiện xong việc test -> giúp test performance.

    Trình tự mỗi khi thực hiện test 1 test case như sau:

    Run the Tests:

    Có 3 cách để run test:

    • Product ▸ Test or Command-U: Cách này sẽ run tất cả test classes trong project.
    • Click vào button mũi tên ở Test navigator.
    • Click chọn nút hình kim cương để run 1 test case cụ thể.

    Basic Functional Test Demo

    Ở ví dụ này, mình sẽ tiến hành việc test xem dữ liệu học sinh ở file json đã chính xác chưa.

    Tạo 1 class Person:

    class Person: Decodable {
        let name: String
        let age: Int
        
        init(name: String, age: Int) {
            self.name = name
            self.age = age
        }
    }
    

    Tạo 1 class Service chứa các logic thực hiện việc lấy dữ liệu:

    class Service {
        func getListStudent() -> [Person] {
            var students: [Person] = []
            guard let url = Bundle.main.url(forResource: "config", withExtension: ".json") else {
                return students
            }
            
            
            do {
                let data = try Data(contentsOf: url)
                let results = try JSONDecoder().decode([Person].self, from: data)
                students += results
            } catch {
                print("Can get data, error: \(error)")
            }
            return students
        }
    }

    Tiến hành việc viết test thông tin cho sinh viên đầu tiên:

    1. @tesable import + tên project: Cho phép unit tests truy cập đến toàn bộ các dữ liệu kiểu internal của project.
    2. Khai báo 1 biến kiểu service.
    3. Khởi tạo sut mỗi lần bắt đầu thực hiện test 1 test case.
    4. Set sut = nil mỗi khi kết thúc việc test 1 test case.
    5. Viết func test để test thông tin học sinh 1.
    6. Viết các hàm để test.

    Note: Khi viết func để test thì phải tên func phải luôn bắt đầu với từ test để Xcode có thể biết đó là 1 test.

    Thực hiện tiến hành test func, nhận được kết quả dữ liệu cần kiểm tra đã chính xác:

    Để đảm bảo đây là 1 Userful test, thực hiện sửa đổi name của sinh viên 1 trong file json thành "Cuong1" và tiến hành test lại func, nhận được kết quả sau:

    Việc test thất bại, đồng thời XCode cũng thông báo ra kết quả để biết sai ở đâu.

    Ở phần tiếp theo, mình sẽ nói về performance test, UI Test và 1 vài thứ hay ho mà Xcode support cho việc test.