Category: iOS

  • MVVM with swift P1

    MVVM with swift P1

    Bài viết này, mình sẽ nói về MVVM.
    Bài viết khá dài, nên sẽ được chia thành 2 phần.

    Content:

    Phần 1:

    • Mô hình MVVM?
    • Demo:
      • MVVM with Closure/Callback
      • Optimize MVVM with Closure/Callback

    Phần 2:

    • Demo:
      • MVVM with RxSwift
    • Tác dụng của MVVM
    • MVVM vs MVP?
    • Kết luận

    MVVM là gì?

    MVVM gồm 3 phần:

    Model:

    Tương tự Model Layer của MVC, MVP.

    View:

    Tương tự so với View Layer của MVP.

    ViewModel:

    • Đây là phần khác biệt của MVVM so với MVP.
    • ViewModel nằm giữa Model và View, có tác dụng là cầu nối để giao tiếp giữa Model và View.
    • ViewModel còn có tác dụng xử lí các logic convert, format data trước khi View hiển thị data đó cho người dùng.
    • Ngoài ra, ViewModel cũng có thể chứa các logic như update database, xử lí networking, … .Tuy nhiên, nên tách các logic này ra thành các class khác để đảm bảo nguyên tắc Single Responsibility Principle.

    Note:

    • View không tương tác trực tiếp với Model. View chỉ có thể tương tác với Model thông qua ViewModel.
    • ViewModel không biết gì về View.
    • MVVM giao tiếp giữa ViewModel và View bằng cách Observer (thường được gọi là binding data).
    • ViewModel cũng không được import UIKit để tách biệt phần logic ra khỏi phần UI.

    Demo MVVM with Closure/Callback:

    Model Layer

    import Foundation
    
    struct Category {
        let id: Int
        var name: String
    }
    

    ViewModel Layer:

    Giờ thì hãy tạo 1 ViewModel để thực hiện các logic xử lí data trước khi View hiển thị cho người dùng:

    import Foundation
    
    class HomeViewModel {
        private var category: Category
        
        // 1
        var displayName: String {
            return transformName(category.name)
        }
        
        // 2
        var onSuccess: ((String) -> Void)?
        
        init(category: Category) {
            self.category = category
        }
        
        // 3
        func transformName(_ name: String) -> String {
            let newName = name.enumerated().map { (index, character) -> String in
                if index % 2 == 0 {
                    return character.uppercased()
                } else {
                    return character.lowercased()
                }
            }
            
            return newName.joined()
        }
        
        // 4
        func changeCategoryName(_ newName: String) {
            category.name = newName
            onSuccess?(transformName(newName))
        }
    }
    
    1. Thuộc tính displayName có giá trị là tên của category sau khi đã được transform.
    2. Tạo 1 closure để phát event.
    3. func thực hiện logic transform name trước khi hiển thị cho người dùng.
    4. func thực hiện update name cho category, sau khi update thành công thì phát ra event chứa giá trị mới.

    View Layer

    import UIKit
    
    class HomeView: UIViewController {
        @IBOutlet private weak var categoryNameLabel: UILabel!
        
        // 1
        private var viewModel: HomeViewModel?
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            // 2
            let category = Category(id: 1, name: "Hollywood")
            viewModel = HomeViewModel(category: category)
            categoryNameLabel.text = viewModel?.displayName
            
            // 3
            viewModel?.onSuccess = {
                self.categoryNameLabel.text = $0
            }
            
        }
    
        @IBAction func didTapButtonChangeCategory(_ sender: Any) {
            viewModel?.changeCategoryName("vietnam")
        }
    }
    1. Khai báo 1 viewModel cho View.
    2. Khởi tạo viewModel và hiển thị name sau khi transform lên giao diện để hiển thị cho người dùng.
    3. Thực hiện observer event được phát ra từ viewModel và update UI.

    Optimize Closure?

    Với cách sử dụng closure ở trên thì bạn đã tạo ra 1 binding giữa View và Model. Nhưng nếu có nhiều thuộc tính phải transform, thì chúng ta phải tạo ra nhiều closure ở ViewModel.
    Liệu có cách nào có thể optimize chúng không?

    Câu trả lời là sử dụng Generic. Tạo ra 1 class generic để tự động phát ra 1 event khi được set 1 value mới:

    class Binding<T> {
        typealias Listener = (T) -> Void
        var listener: Listener?
        
        var value: T {
            didSet {
                listener?(value)
            }
        }
        
        init(value: T) {
            self.value = value
        }
        
        func bind(listener: Listener?) {
            self.listener = listener
            self.listener?(value)
        }
    }

    ViewModel Layer khi đó sẽ được update lại như sau:

    class HomeViewModel {
        var category: Category
        // 1
        public var displayName: Binding<String>
        
        init(category: Category) {
            self.category = category
            
            let newName = category.name.enumerated().map { (index, character) -> String in
                if index % 2 == 0 {
                    return character.uppercased()
                } else {
                    return character.lowercased()
                }
            }
            self.displayName = Binding<String>(value: newName.joined())
        }
        
        func changeCategoryName(_ newName: String) {
            category.name = newName
            // 1
            displayName.value = transformName(newName)
        }
        
        func transformName(_ name: String) -> String {
            let newName = name.enumerated().map { (index, character) -> String in
                if index % 2 == 0 {
                    return character.uppercased()
                } else {
                    return character.lowercased()
                }
            }
            
            return newName.joined()
        }
    }
    1. displayName được sửa lại thành kiểu Binding<String>
    2. Mỗi khi thay đổi tên, chỉ cần set lại value cho displayName, thì displayName sẽ tự động phát ra 1 event chứa newName.

    View Layer sau khi optimize sẽ trở thành:

    class HomeView: UIViewController {
        @IBOutlet private weak var categoryNameLabel: UILabel!
        private var viewModel: HomeViewModel?
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            let category = Category(id: 1, name: "Hollywood")
            viewModel = HomeViewModel(category: category)
            
            // 1
            viewModel?.displayName.bind(listener: {
                self.categoryNameLabel.text = $0
            })
            
        }
    
        @IBAction func didTapButtonChangeCategory(_ sender: Any) {
            viewModel?.changeCategoryName("vietnam")
        }
    }
    1. Thực hiện observer event được phát ra từ displayname của ViewModel và thực hiện update UI với giá trị mới nhận được.

    Note: Để tránh bài viết quá dài, thì mình sẽ không add ảnh kết quả run demo vào đây.

    Kết luận:

    Qua phần 1, có thể thấy 1 lợi ích của MVVM so với MVP là không cần tạo delegate để giao tiếp.
    Phần 1 sẽ chỉ dừng ở đây thôi, mình sẽ cùng tìm hiểu rõ hơn MVVM ở phần 2.

  • MVP with Swift

    MVP with Swift

    Bài viết này, mình sẽ trình bày về MVP.

    Content

    • Tổng quan về MVC
    • Những vấn đề của MVC
    • Ý tưởng của MVP
    • Áp dụng MVP

    Tổng quan về MVC:

    Chắc hẳn các bạn đã quen với MVC: 1 trong những architecture pattern phổ biến nhất.
    MVC bao gồm 3 phần chính:

    Model

    Model Layer là nơi lưu trữ data của bạn.
    Model layer bao gồm:

    • Model Objects (hiển nhiên rồi).
    • Ngoài ra còn có 1 vài class và object khác có thể trong Model như các class xử lí Network code, Persistence code(Lưu trữ data đến database, CoreData,…), Parsing Code(parsing network response to model,…), các class Helper, Extensions, …

    View

    Là nơi hiển thị giao diện cho người dùng.
    VD: Xib file, UIView class, Core Animation, … Nói chung là những thứ liênn quan đến UIKit

    Controller

    Là phần tương tác với View và Model. Controller nhận action/events từ view để update Model và View.

    Những điều cần chú í ở MVC:

    • View chỉ dùng để update UI, không chứa các logic.
    • View không được tương tác trực tiếp với Model.
    • View không được làm bất cứ điều gì mà không liên quan đến chính nó.

    Vấn đề của MVC?:

    • Controller chứa rất nhiều logic như update UI, xử lí các logic user action, logic networking, … tuy điều này có thể được giảm thiểu bởi tách các logic như Network, Lưu database,… sang các class khác.
    • Khó test các logic của controller bởi controller liên kết chặt chẽ với View.
    • Dự án càng lớn, code controller càng nhiều, dẫn đến khó maintain, bảo trì.

    Ý tưởng của MVP:

    MVP gồm 3 phần:

    View:

    Bao gồm Views và View Controller, đùng để tương tác, xử lí UI và nhận các event của user.

    Presenter:

    Chịu trách nhiệm xử lí logic, gồm các logic xử lí user action, logic networking, tương tác database…
    Các event của user sẽ được gửi đến Presenter để Presenter xử lí logic tương ứng, sau đó sẽ tương tác, yêu cầu View update UI bằng cách sử dụng delegate.

    Model:

    Tương tự như MVC.

    Note: Tầng Presenter phải không phụ thuộc vào UIKit, để tách biệt với View. Qua đó gíup dễ viết test cho phần logic hơn.

    Giờ hãy bắt tay vào demo:

    Demo:

    Model Entity:

    import Foundation
    
    struct Category {
        let id: Int
        let name: String
    }
    

    Class Service chịu trách nhiệm tương tác với Model:

    import Foundation
    
    protocol ICategoryService {
        func getCategoryById(id: Int) -> Category?
    }
    
    class CategoryService: ICategoryService {
        func getCategoryById(id: Int) -> Category? {
            let categories = [Category(id: 1, name: "Movies"),
                              Category(id: 2, name: "Books"),
                              Category(id: 3, name: "Computer")]
            
            let category = categories.filter({
                $0.id == id
            }).first
            
            return category
        }
    }

    Class Presenter: Chịu trách nhiệm xử lí logic

    import Foundation
    
    protocol IHomeViewDelegate: NSObjectProtocol {
        func updateUI(_ categoryName: String)
    }
    
    class HomeViewPresenter {
        // 1
        weak var delegate: IHomeViewDelegate?
        let categoryService: ICategoryService!
        
        init(view: IHomeViewDelegate, service: ICategoryService) {
            delegate = view
            categoryService = service
        }
        
        func searchCategoryById(id: Int) {
            let result = categoryService.getCategoryById(id: id)
            if let categoryName = result?.name {
                // 2
               delegate?.updateUI(categoryName)
            }
        }
    }
    1. Khai báo delegate để presenter có thể dùng để tương tác với View.
      Ở đây, delegate và category được khai báo kiểu protocol và được khởi tạo trong hàm init để tránh bị dependency.
    2. Sau khi thực hiện xong việc truy vấn database, presenter dùng delegate để yêu cầu View update UI.
    Class View:
    import UIKit
    
    class HomeView: UIViewController {
        @IBOutlet private weak var categoryNameLabel: UILabel!
        
        private var presenter: HomeViewPresenter!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            presenter = HomeViewPresenter(view: self, service: CategoryService())
        }
        
        // 1
        @IBAction func didTapButtonSearch(_ sender: Any) {
            presenter.searchCategoryById(id: 1)
        }
    }
    
    extension HomeView: IHomeViewDelegate {
        // 2
        func updateUI(_ categoryName: String) {
            categoryNameLabel.text = categoryName
        }
    }
    1. Khi user tap button, View sẽ chuyển action đến presenter và yêu cầu presenter thực hiện logic.
    2. Update UI khi được presenter yêu cầu.

    Lợi ích của MVP:

    MVP đã giúp tách biệt logic khỏi View, từ đó mang đến các lợi ích:

    • Dễ dàng đọc hiểu code.
    • Dễ dàng viết Unit test cho logic.
    • Dễ maintain, bảo trì.

    Kết luận:

    • MVP chỉ giải quyết việc tách biệt logic khỏi View. Tuy nhiên, những phần logic như truy vấn database, networking, … thì chưa được xử lí. Những logic như vậy có thể được đặt ở presenter, hoặc tạo 1 class riêng ở Model tùy theo bản thân bạn.
      Tuy nhiên, nên tách biệt các phần logic Database, networking, … ra các class riêng để tuân thủ nguyên tắc Single Responsibility Principle của SOLID.
    • Theo mô hình MVP, Presenter không được import UIKit để tránh logic bị liên quan đến View.
  • Result type trong xử lý Networking

    Result type trong xử lý Networking

    1. Giới Thiệu

    Chào mọi người, mình là Lâm.

    Hôm này mình xin viết 1 bài về Result type trong Swift và cách nó kết hợp với xử lý Networking để viết ra những đoạn code ngắn gọn, súc tích, dễ đọc, dễ maintain. Trước Swift 5 thì nó là 1 custom type phải tự viết, tuy nhiên sau đấy chắc mn thấy nó hay nên từ Swift 5 thì nó đã trở thành 1 type build-in của Swift luôn (https://developer.apple.com/documentation/swift/result) .

    Có thể thấy ngôn ngữ Swift phát triển liên tục và nhiều trong số đấy là từ những ý tưởng đóng góp của cộng đồng developer

    2. Ý tưởng

    Khi xử lý những tác vụ trong lập trình, chúng ta luôn mong muốn là khi input 1 cái gì đấy vào, thì output của nó ra sẽ chỉ là 1 trong 2 case: success hoặc fail. Như vậy thì luồng xử lý của chúng ta sau đấy sẽ rất clear và gọn gàng.
    Tuy nhiên success thì có nhiều kiểu giá trị trả về, fail thì cũng có nhiều kiểu lỗi bắn ra, vậy thì mình cần phải có 1 type để uniform được các trường hợp, quy về 1. Thì đấy chính là mục đích của Result type.

    Ý tưởng của Result type rất đơn giản, nó là 1 generic Enum, gồm 2 generic là Success và Failure, và có 2 case: 1 cho success và 1 cho failure.
    – Case success sẽ chứa value mà chúng ta expect.
    – Case failure sẽ chứa error bắn ra.

    enum Result < Success, Failure: Error > {
        case success(Success)
        case failure(Failure)
    }

    3. Áp dụng

    Nói về xử lý Networking, ví dụ trong mô hình MVC, thường chúng ta sẽ tạo ra 1 func thế này ở Model:

    func loadData(url: URL, completion: @escaping (Data?, Error?) -> Void) {
        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
            if let error = error {
                completion(nil, error)
            }
             if let data = data {
                 completion(data, nil)
            }
        }
        task.resume()
    
    }


    Rồi ở bên ViewController chúng ta sẽ hứng và xử lý tiếp như thế này:

    model.loadData(url: url) { [weak self] (data, error) in
        guard let self = self else { return }
        if let error = error {
            // handle error...
        }
         if let data = data {
            // handle response...
        }
    }

    Có 2 cái ko hay ở đây:
    – Cái ko hay thứ 1 là với completion: (Data?, Error?) chúng ta sẽ có tổng cộng 4 case xảy ra:
    + (data, error)
    + (data, nil)
    + (nil, error)
    + (nil, nil)
    Case đầu tiên (data, error) và case cuối cùng (nil, nil) là 2 case mà sẽ không bao giờ mong muốn. Vì output ko thể nào vừa có data lại vừa có error, hay output ko có gì cả, như vậy thì sẽ rất confuse.

    – Cái ko hay thứ 2 là ở đầu bên kia (ViewController) khi nhận response từ Model sẽ phải xử lý 2 parameter với type là optional (data và error), rồi lại if/else 1 hồi để handle đủ case.

    Bây giờ nếu chúng ta sử dụng Result type vào thì nó sẽ như thế này:

    func loadData(url: URL, handler: @escaping (Result<Data, Error>) -> Void) {
    
            let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
                if let error = error {
                    completion(.failure(error))
                }
                if let data = data {
                    completion(.success(data))
                }
            }
        }

    và đầu bên kia xử lý sẽ như thế này:

            model.loadData(url: url) { [weak self] result in
                guard let self = self else { return }
                switch result {
                case .failure(let error):
                    // handle error...
                case .success(let data):
                    // handle success...
                }
            }

    sẽ chỉ phải xử lý 1 parameter là result: Result, và Result chỉ có 2 case là success/fail, đúng với những gì cta mong muốn, code nhìn clear hơn nhiều, thay đống if/else bằng switch/case cũng làm code dễ đọc hơn.

    4. Mở rộng

    Về cơ bản là như vậy, mở rộng hơn thì chúng ta có thể define chi tiết hơn về lỗi.
    Nhìn lại declaration:

    enum Result < Success, Failure: Error > {
        case success(Success)
        case failure(Failure)
    }

    chúng ta thấy Failure đang đơn giản adopt vào protocol Error. Error thì rất common, nó khá là chung chung, gọi là lỗi gì vứt vào đây cũng được. Bây giờ muốn specific hơn về lỗi, cta có thể tạo 1 custom type, vì Result của cta nhận vào generic là 1 Error nên custom type của cta chỉ cần adopt vào protocol Error là được:

    enum VideoError: Error {
        case networkFailure(Error)
        case exceedSize
        case invalidFormat
    }

    khi gọi API:

        func loadData(url: URL, completion: @escaping (Result<Data, VideoError>) -> Void) {
    
            let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
    
                if let error = error {
                    completion(.failure(.networkFailure(error)))
                    return
                }
    
                if exeedSize {
                    completion(.failure(.exceedSize))
                    return
                }
    
                if invalidFormat {
                    completion(.failure(.invalidFormat))
                    return
                }
    
                if let data = data {
                    completion(.success(data))
                }
            }
        }

    và đầu bên kia xử lý:

           model.loadData(url: url) { [weak self] result in
                guard let self = self else { return }
                switch result {
                case .success(let data):
                    // handle success...
                case .failure(.networkFailure(let error)):
                    // handle networking error...
                case .failure(.exceedSize):
                    // handle exceedSize...
                case .failure(.invalidFormat):
                    // handle invalid format...
                }
            }

    có thể thấy luồng đi rất clear.

    Bên trên là Result mình tự viết, đối với built-in của Swift 5 thì type còn có 1 số những methods:
    get: để lấy ra success value
    map/flatMap: để transform success value thành 1 dạng khác
    mapError/flatMapError: để transform failure error thành 1 dạng khác
    ==, != : để so sánh xem 2 result có giống nhau hay ko
    Mn có thể đọc chi tiết trong docs:
    https://developer.apple.com/documentation/swift/result

    Nói chung nó sẽ hỗ trợ để mình xử lý thêm cho data/error trả về (hình dung mình có thể viết 1 cái extension để decode success value thành object luôn chẳng hạn).

    OK, hy vọng mn cảm thấy hữu ích.
    Thanks for reading.

  • Generic Cache từ NSCache

    Generic Cache từ NSCache

    Chào mọi người, mình là Lâm.

    Trước đây mình có đọc 1 bài viết hay về việc tự custom 1 class để phục vụ cho việc cache dữ liệu (nguồn bài viết: https://www.swiftbysundell.com/articles/caching-in-swift/ )
    Mình đã áp dụng nó vào 1 số project mini của mình, thấy hữu ích nên ở bài viết này mình xin được trình bày lại, cũng như có 1 số phân tích của bản thân để đâu đấy có thể giúp clear hơn, giúp mn tiết kiệm thời gian đọc hơn.

    Note: source code mình có để ở link git bên dưới bài viết.

    Tóm tắt:

    • Cache là 1 chủ đề quen thuộc của mobile app, cache dữ liệu để ko phải load lại nhiều lần, tối ưu cho app, or khi không có mạng thì mình vẫn xem được những nội dung đã cache.

    • Ở bài viết này mình sẽ trình bày cách viết 1 class Cache based trên 1 class build-in của iOS là NSCache (https://developer.apple.com/documentation/foundation/nscache).
      NSCache nó có 1 cái rất hay là nó sẽ tự động remove record khỏi cache khi bộ nhớ của app bị warning, hay nói đơn giản app đang ăn quá nhiều RAM, vì nó là build-in class nên việc này chắc chắn là đã được optimize rồi, nó còn 1 số optimize khác nữa mn đọc trong document refer ở bên trên.

    • Câu hỏi là sao ko dùng luôn NSCache mà phải viết 1 class trên nó làm gì? Lý do là bởi vì NSCache nó là class thuộc về Objective-C,  khi cache nó yêu cầu key phải là 1 NSObject, và value phải là 1 class instance (ko cần phải là NSObject nhưng phải là instance của 1 class).
    • ( NSString là 1 NSObject, và MyClass là 1 custom class)
    • Swift thì lại hay dùng struct, bây giờ muốn cache được cả class, struct… thì phải đi đường vòng 1 chút. Ý tưởng là mình sẽ wrap key/value thực sự của mình vào trong NSObject/Class để có thể adapt được với NSCache.

    1. Tạo Custom class:

    Tạo 1 class tên Cache, với 2 generics là Key và Value. Key phải là Hashable thì nó với làm key được, vì nó phải là unique.

    Tiếp theo tạo 2 inner class để wrap key/value:


    Class WrappedKey là 1 subclass của NSObject để nó có thể trở thành key của NSCache, có property key bên trong là key thực sự mà mình sẽ dùng, type của key là Key được resolve từ outer class là Cache.

    Đến đây mn cũng có thể hình dung vì sao WrappedKey phải là inner class của Cache, vì nó phải resolved cái generic Key của Cache, nếu nó là 1 class nằm bên ngoài Cache thì nó sẽ ko nhìn thấy generic của Cache.  Tiếp theo, mình sẽ override lại property hash:Int và func isEqual(_ object: Any) của NSObject và implement lại dưới cái key của mình (vì đây với là cái key thực sự).

    Tương tự với class Entry (hay có thể gọi là WrappedValue, gọi Entry nghe ngắn hơn). Entry thì ko cần phải unique nên ko cần override lại hash, và cũng ko cần là subclass của NSObject, cứ wrap value của nó vào là được.

    Note:

    2. Cache’s methods:

    OK, với 3 class được dựng ở trên thì mình có thể viết 1 số API được rồi:

    nên viết 1 subscript để lúc sử dụng code nó ngắn gọn, đẹp hơn.

    3. Thêm constraint cho Cache

    Bây giờ mình muốn có thêm nhiều control hơn nữa với cache của mình:

    • Cache 1 số lượng record nhất định
    • Cache trong 1 khoảng thời gian nhất định

    NSCache có 1 property gọi là countLimit: Int, muốn cache bao nhiêu record thì mình gán giá trị cho nó, khi cache vượt quá số record này thì nó sẽ tự động remove record cũ đi. NSCache còn có 1 property gọi là totalCostLimit để đặt ra threshold về số byte sẽ được cache, tuy nhiên vì nó đã auto handle memory rồi nên cái này chắc cũng hiếm dùng.
    Về thời gian cache, mình sẽ tạo biến entryLifeTime để giữ lượng thời gian, tính bằng giây (TimeInterval).

    cần phải update lại Entry, lúc này Entry của ngoài giữ value còn giữ cả expirationDate nữa, mỗi entry sẽ có expirationDate của nó:

    Sau đấy update lại method của Cache:

    • Với method insert, trước khi insert mình sẽ tính thêm expirationDate cho entry
    • Với method value(forKey: ), mình sẽ check xem entry đấy đã bị expired hay chưa, nếu rồi thì remove nó.

    4. Persistent Cache

    OK, khá ngon rồi, nhưng mà cache hiện vẫn đang chỉ là in-memory, tức là trên RAM thôi. Bây giờ mong muốn là phải save được nó xuống disk, để sau này lấy ra.
    Vấn đề của NSCache là nó ko public ra cho mình thông tin keys  –> dẫn đến việc giả sử mình có save được cache rồi, lúc lấy lại ra cũng ko thể lấy được value, vì mình ko có keys –> mình phải tự control list key của mình bằng cách tạo thêm 1 inner class KeyTracker.


    class này sẽ giữ 1 set keys, và nó sẽ là delegate cho NSCache. Sau này khi NSCache tự động remove record, nó sẽ bắn về delegate method là cache(_ cache: , willEvictObject: Any), response của mình với method này là sẽ remove key tương ứng với record đấy ra khỏi set keys.

    Tuy nhiên delegate này trả về cho mình object sẽ bị remove (ở đây là Entry), làm sao mình biết được là key nào đang associated với object đấy? Mình đang bị lack data,  ở Entry mình cần phải giữ thêm key associated với nó.

    và update method khi insert data (add thêm key vào khi init entry):

    OK, vậy là đã xử lý xong vấn đề về keys, bước cuối để có thể save được cache là cache của mình phải serialize được thành data để lưu, và lúc mình lấy cục data đấy ra thì mình phải deserialize ngược lại được thành object,  hay nói đơn giản là encode/decode. Vì mình làm việc với json từ API, nên dùng Codable ở đây là hợp lý nhất.

    Đầu tiên phải serialize được Entry, vì vậy nên sẽ Entry sẽ phải là Codable.  Tuy nhiên mình nếu fix như vậy thì generic của mình sẽ bị cứng, khi tạo cache sẽ require Entry bắt buộc phải là Codable, ko hay. Thế nên ở đây sẽ dùng kỹ thuật gọi là protocol conditional conform, hay nói đơn giản là 1 type sẽ chỉ conform vào protocol dưới 1 số điều kiện nhất định.

    Ở đây mình chỉ muốn Entry conform vào Codable dưới điều kiện là Key và Value của nó Codable:

    Trong quá trình encode/decode, sẽ cần cả insert và retrieve những entries hiện tại, nên sẽ tạo 2 methods để handle việc này, nên đặt tên khác 1 chút để  phân biệt tránh bị nhầm lẫn với API của Cache, và nên mark nó là private luôn, vì nó ko phải API của cache.


    Với 2 methods này bây giờ mình có thể encode cục Cache rồi:

    • Khi encode thì sẽ sử dụng list keys ở keyTracker + map với method entry(forKey key: Key) để ra 1 list Entry, và encode list này bằng singleValueContainer
    • Khi decode thì lấy ngược lại list Entry đã encode và add lại vào cache bằng method insert(_ entry: Entry).

    Có thể đọc thêm về encode/decode bằng singleValueContainer ở đây: https://medium.com/swiftly-swift/swift-4-decodable-beyond-the-basics-990cc48b7375

    Công việc còn lại chỉ là write cái cục vừa encode đấy xuống file, để ý là write vào folder cache (.cachesDirectory) luôn cho nó chuẩn:

    5. Demo with App

    Để demo, hãy viết 1 app đơn giản lấy dữ liệu từ 1 open API: https://developers.themoviedb.org/3

    đây là API về phim, mình mê phim cho nên là quyết định dùng API này:D

    mỗi phim thì sẽ chưa thông tin gồm: Id/Name/Overview/posterImage

    Như vậy mình sẽ cần 2 cache: 1 cho thông tin về phim, 2 cho posterImage load được

    đang để access global luôn

    Khi lấy được dữ liệu từ API thì mình sẽ ghi nó vào cache:

    đây là khi API đã trả về response

    Và khi app xuống background thì mình sẽ save nó xuống disk, tuỳ vào bài toán, mình có thể đặt 1 cái timer cho nó 5 phút save 1 lần cũng được, ở đây mình làm thế này cho dễ test.

    M.n có để ý là với movieCache mình có thể gọi method saveToDisk được, nhưng với imageCache thì lại ko có method đấy? Lý do là vì protocol conditional conformance, nếu mn nhìn lại sẽ thấy method saveToDisk() chỉ được conform với điều kiện Key và Value đều Codable. imageCache value của nó là UIImage, UIImage ko Codable –> imageCache sẽ ko conform vào method này, và sẽ ko visible luôn, có thể đổi UIImage thành Data thì sẽ lại conform.

    OK, sau khi mà save cache xuống disk rồi thì nó sẽ nằm ở folder cache: 

    bây giờ sẽ lấy ngược lại cache ra khi mở app, ở đây mình lấy ở application didFinishLaunching và ở main thread luôn:

    và mình viết thêm 1 computed property nữa để lấy ra list Movie đã cache, 1 cái conditional conformances nữa, property  listCacheMovie này sẽ chỉ visible với movieCache, hợp lý đúng ko?

    bây giờ giả sử trong case API fail thì mình sẽ dùng cache, ví dụ vậy đi:

    Lúc đầu mình có nói là nếu mình đặt countLimit cho cache thì vượt quá số đấy nó sẽ tự delete record đi, để test mn có thể pull source về rồi đặt print ra console để xem new recrod được insert vào và old record bị remove đi liên tục nhé.

    Link source: https://github.com/nguyenlam96/CacheIt

    Bài viết cũng đã khá dài rồi, nhưng mà mình tin nó cũng gói gọn được 1 vấn đề, hy vọng mn cảm thấy hữu ích. 
    Thanks for reading.

  • Dependency Injection trong iOS

    Dependency Injection trong iOS

    Trong lập trình OOP hay POP, thì luôn dc recommend code 1 cách tách biệt hệ thống thành những module sao cho chúng liên kết với nhau 1 cách lỏng lẻo, để những module đó có thể hoạt động 1 cách độc lập nhiều nhất có thể.
    Nếu các module liên kết quá chặt chẽ với nhau thì khi hệ thống cần nâng cấp, phát triển thì sẽ gặp nhiều vấn đề, ngoài ra việc maintain sau này hay việc viết unit test cũng gặp nhiều khó khăn.
    -> Dependency Injection (DI) là 1 design pattern để ngăn chặn sự phụ thuộc chặt chẽ này.

    Content

    • Ý tưởng của DI
    • Implement DI trong swift

    Ý tưởng của DI:

    Chữ D trong SOLID, là Dependency Inversion Principle. Nội dung của nguyên lý này được tóm gọn lại như sau:

    • Các module cấp cao không nên phụ thuộc vào các module cấp thấp.
    • Interface không nên phụ thuộc vào chi tiết, mà ngược lại.
    • Các class giao tiếp với nhau thông qua interface, chứ không phải implementation.

    Note:

    • Dependency Injection là 1 design pattern để code có thể tuân thủ nguyên lý Dependency Inversion.
    • DI là 1 kỹ thuật cho phép xóa bỏ sự phụ thuộc chặt chẽ giữa các module, làm cho ứng dụng dễ dàng hơn trong việc maintain, extend, debug và test bởi khi đó các module hoạt động độc lập.

    Implement DI trong swift:

    Trước hết hãy đến 1 ví dụ không sử dụng DI:

    Vì k sử dụng DI, ví dụ ở trên sẽ có những hạn chế như sau:
    Class Car bị phụ thuộc vào NormalEngine:

    • Trong trường hợp muốn nâng cấp bằng cách thay vào FastEngine, hay nhiều loại Engine khác thì sẽ gặp nhiều vấn đề,
    • Khi có thay đổi trong class NormalEngine, thì có thể sẽ ảnh hưởng trực tiếp đến class Car.
      Ex: Thêm nhiều thuộc tính cho NormalEngine thì constructor sẽ thay đổi.
    • Khó khăn trong việc viết unit test.

    -> Chúng ta sẽ áp dụng DI Design pattern để giải quyết điều này.

    Constructor Injection

    // 1
    protocol Engine {
        func start()
    }
    
    // 2
    class NormalEngine: Engine {
        func start() {
            print("Normal engine start running...")
        }
    }
    
    class FastEngine: Engine {
        func start() {
            print("Fast engine start running...")
        }
    }
    
    class Car {
        // 3
        var engine: Engine
        init(engine: Engine) {
            self.engine = engine
        }
    
        func run() {
            engine.start()
        }
    }
    
    // 4
    let car = Car(engine: NormalEngine())
    car.run()
    car.engine = FastEngine()
    car.run()
    1. Tạo 1 protocol(interface) để giao tiếp với class thay vì giao tiếp bằng implementation (nguyên lý Dependency Inversion).
    2. Các class NormalEngine, FastEngine implement protocol đó
    3. Khai báo 1 biến kiểu Engine trong class Car
    4. Khi khởi tạo class Car, chúng ta sẽ truyền 1 engine cụ thể vào thông qua constructor.
    Ưu điểmNhược điểm
    – Tính đóng gói cao
    – Chắc chắn rằng đối tượng đã được khởi tạo
    – Nếu giao tiếp với nhiều protocol thì sẽ làm cho constructor của class đó nhiều lên.
    – Đối với các ViewController, đặc biệt là ViewController được define trong storyboard thì sẽ không có hàm khởi tạo.

    Property Injection

    Property Injection được dùng để tránh nhược điểm của Constructor Injection,bằng cách không khởi tạo các dependencies trong constructor mà khởi tạo bằng cách set value cho property.

    class Car {
        var engine: Engine!
    
        func run() {
            engine.start()
        }
    }
    
    let car = Car()
    car.engine = NormalEngine()

    Ưu điểm:

    • Cho phép set dependencies vào thời điểm bạn muốn.

    Nhược điểm:

    • Tính đóng gói thấp.
    • Vì các dependency được khai báo theo kiểu !, nên nếu quên không set dependency trước khi sử dụng thì dễ gây ra crash; còn nếu khai báo dependency theo kiểu optional thì mỗi lần sử dụng phải unwrap.

    Interface Injection

    • Ở đây bạn tạo ra 1 hàm setter để set dependency.
    protocol HasEngine {
        func setEngine(dependency: Engine)
    }
    
    class Car: HasEngine {
        var engine: Engine!
    
        func setEngine(dependency: Engine) {
            self.engine = dependency
        }
    
        func run() {
            engine.start()
        }
    }
    
    let car = Car()
    car.setEngine(dependency: FastEngine())

    Method Injection

    • Nếu bạn chỉ cần sử dụng dependency để dùng 1 lần thì bạn không cần lưuu lại như là 1 biến. Bạn chỉ đơn giản là pass dependency như là 1 method param.
    class Car {
        func run(engine: Engine) {
            engine.start()
        }
    }
    let car = Car()
    car.run(engine: NormalEngine())

    Ps: Thật ra ví dụ này không phù hợp thực tế lắm bởi xe nào cũng cần động cơ :)) Nhưng để hiểu được thì thế là đủ hiểu r 😉

    Ambition Context:

    • Đây là 1 cách tránh DI nhưng chỉ nên được sử dụng cho các global dependency mà được chia sẻ với nhiều đối tượng khác.
    • Tuy nhiên bạn nên hạn chế sử dụng cách này, bởi nếu không quản lí đọc ghi 1 biến dùng chung 1 cách cẩn thận thì sẽ dễ dẫn tới Race Condition.
    class Car {
        static var engine: Engine = NormalEngine()
    
        func run() {
            Car.engine.start()
        }
    }
    
    let car = Car()
    car.run()

    Ưu điểm:

    • Dependency được sử dụng global.
    • Vẫn có thể thay đổi dependency trong quá trình sử dụng.

    Nhược điểm:

    • Tính đóng gói thấp.
    • Yêu cầu Thread safe.

    Kết luận:

    • Dependency Injection là 1 kỹ thuật mạnh mẽ để giúp viết code 1 cách clean và dễ maintain.
    • Ngoài ra có thể sử dụng các design pattern khác để tránh dependency như Factory, Service Locator hay Dependency Injection Container.
  • Basic CAShapeLayer iOS (P2)

    Basic CAShapeLayer iOS (P2)

    Phần 1 của loạt bài viết về CAShapeLayer đã nói về cách vẽ đường thẳng, các hình khối.
    Ở phần 2 này, mình sẽ nói về những attribute quan trọng, thường được sử dụng của CAShapeLaye.

    Content

    • Stroke
    • Fill Color
    • Line

    Stroke

    Stroke Color

    • strokeColor là thuộc tính để set màu cho đường line của 1 đường thẳng, hình khối.
    • Ex: Để vẽ 1 hình vuông có các đường thẳng màu xanh thì chỉ cần thêm dòng code dưới đây khi vẽ hình:
    shapeLayer.strokeColor = UIColor.blue.cgColor

    Code vẽ hình vuông:

    func createSquare() {
        let shapeLayer = CAShapeLayer()
            
        let path = UIBezierPath()
        path.move(to: CGPoint(x: 0.0, y: 0.0))
        path.addLine(to: CGPoint(x: 100.0, y: 0.0))
        path.addLine(to: CGPoint(x: 100.0, y: 100.0))
        path.addLine(to: CGPoint(x: 0.0, y: 100.0))
        path.close() // creating a line segment between the first point and current point
            
        shapeLayer.path = path.cgPath
        shapeLayer.fillColor = nil
        shapeLayer.strokeColor = UIColor.blue.cgColor
        layer.addSublayer(shapeLayer)
    }

    Stroke Start & Stroke End

    • Ok, vậy là bạn đã biết cách tạo 1 path và gán path đó cho shapeLayer.

    • Nếu coi việc vẽ 1 path từ đầu đến cuối là 100% công việc, vậy giả sử nếu bạn chỉ muốn vẽ được 40, 50 hay 60% của công việc đó?
      -> strokeStart và strokeEnd sinh ra là để giúp bạn làm điều đó.

    • strokeStart: Chạy trong khoảng 0 đến 1, giá trị default là 0, tức là sẽ vẽ từ đầu.

    • strokeEnd: Chạy trong khoảng 0 đến 1, giá trị default là 1, tức là sẽ vẽ cho đến cuối cùng.

    • Khi bắt đầu công việc vẽ thì sẽ bắt đầu từ strokeStart đến strokeEnd.

    Vẫn đoạn code vẽ hình vuông ở trên, giả sử bạn chỉ muốn vẽ từ đầu cho đến 70% công việc thì thêm dòng code sau:

    shapeLayer.strokeEnd = 0.7

    Còn nếu bạn muốn vẽ từ 30% công việc đến 100% công việc thì chỉ cần set strokeStart:

    shapeLayer.strokeStart = 0.3

    Bạn có thể kết hợp cả strokeStart và strokeEnd tùy ý.
    Ngoài ra, strokeStart và strokeEnd còn có thể để dùng để tạo animation. Phần animation này sẽ nói ở bài viết sau.

    Fill Color

    • fillColor là thuộc tính dùng để thay đổi màu cho phần bên trong của 1 hình khối.
    • Ở phần 1, mình đã có vẽ 1 hình tròn có đường line màu đỏ và phần bên trong không màu. Giờ thì hãy set màu cho phần bên trong hình tròn bằng cách thêm dòng code:
    shapeLayer.fillColor = UIColor.yellow.cgColor

    Code vẽ hình tròn:

    private func createCircle() {
        let shapeLayer = CAShapeLayer()
            
        shapeLayer.lineWidth = 2.0
        shapeLayer.fillColor = UIColor.yellow.cgColor
        shapeLayer.strokeColor = UIColor.red.cgColor
        let openCirclePath = UIBezierPath(arcCenter: CGPoint(x: 60.0, y: 60.0),
                                          radius: 60.0,
                                          startAngle: 0.0,
                                          endAngle: CGFloat.pi * 2,
                                          clockwise: true)
            
        shapeLayer.path = openCirclePath.cgPath
        layer.addSublayer(shapeLayer)
    }
    

    Kể cả đối với các hình không kín thì việc set fillColor vẫn cứ là OK nhé :

    Line

    Line Width

    Nghe cái tên thôi là đã biết attribute này để làm gì rồi 🙂 Đó là set width cho path dùng để vẽ đường thẳng, hình khối. Thêm dòng code sau vào đoạn code vẽ hình tròn:

    shapeLayer.lineWidth = 20.0

    Kết quả thu được:

    Line Cap

    thuộc tính lineCap sẽ quyết định xem điểm cuối của 1 open path được vẽ như thế nào: butt, round hay square.
    Hình dưới đây lần lượt là hình của shapeLayer sau khi set lineCap thành butt, round và square:

    Để set thuộc tính lineCap cho shapeLayer thì thêm dòng code:

    shapeLayer.lineCap = .round hoặc .butt hoặc .square

    Line Dash Pattern

    • Thuộc tính lineDashPattern giúp bạn vẽ những đường thẳng đứt đoạn chứ không phải là 1 đường thẳng liền mạch.
    • lineDashPattern là 1 mảng các số định nghĩa độ dài nét liền và nét đứt bạn muốn vẽ.
      Giả sử bạn muốn vẽ 1 đường thẳng đứt đọan như sau: Cứ vẽ được 1 nét dài 5 thì sẽ đứt đoạn 1 khoảng 15 thì set lineDashPattern như sau:
    shapeLayer.lineDashPattern = [5, 15]

    Hoặc nếu bạn muốn vẽ 1 nét dài 5 rồi đến 1 nét đứt 10, rồi vẽ 1 nét dài 15 và 1 nét đứt dài 20:

    shapeLayer.lineDashPattern = [5, 10, 15, 20]

    Cấu trúc lineDashPattern bạn cung cấp sẽ được lặp lại cho đến khi vẽ xong hình.

    Line Dash Phase

    "Line dash phase specifies how far into the dash pattern the line starts." – Apple

    • Có thể hiểu đơn giản là lineDashPhase là 1 thuộc tính sẽ cho bạn dịch điểm bắt đầu vẽ đi 1 khoảng bạn muốn. Độ dài của path sẽ không thay đổi.
    • lineDashPattern có default value = 0.
    let circleShapeLayer1 = ...
    circleShapeLayer1.lineWidth = 2.0
    circleShapeLayer1.lineDashPattern = [47.12]
    
    let circleShapeLayer2 = ...
    circleShapeLayer2.lineWidth = 2.0
    circleShapeLayer2.lineDashPattern = [47.12]
    circleShapeLayer2.lineDasePhase = 23.56
    
    let circleShapeLayer3 = ...
    circleShapeLayer3.lineWidth = 2.0
    circleShapeLayer3.lineDashPattern = [47.12]
    circleShapeLayer3.lineDasePhase = -23.56

    Ở bài viết tiếp theo, mình sẽ nói về animation của CAShapeLayer.

    refer: https://www.calayer.com/core-animation/2016/05/22/cashapelayer-in-depth.html

  • Basic CAShapeLayer iOS (P1)

    Basic CAShapeLayer iOS (P1)

    Trong lập trình ứng dụng, sẽ có những lúc bạn phải làm việc với các đường thẳng, các hình khối.
    Đôi khi chúng quá đơn giản để phải cần cắt ra các image, đôi khi các ứng dụng đòi hỏi các animation, dẫn đến phức tạp thì không thể dùng image được.
    CAShapeLayer được dùng để làm việc với các đường thẳng, hình khối 1 cách đơn giản.

    Contents:

    • How to draw a line
    • Draw shape with shape layer

    How to draw a line?

    Học cách vẽ 1 đường thẳng là điều đầu tiên phải học trước khi học vẽ 1 hình khối.
    Để vẽ 1 đường thẳng thì cần tối thiểu 2 điểm: điểm đầu và điểm cuối.
    UIView có func draw() dùng để vẽ các line:

    • move(to: ): move đến 1 điểm cụ thể để bắt đầu vẽ.
    • addLine(to: ): vẽ 1 đường thẳng đến 1 điểm cụ thể.
      Kết quả thu được:

    Để vẽ 1 đường cong, thì apple cung cấp 2 phương thức addCurve() addQuadCurve():

    1. Với func addCurve() thì sẽ vẽ được 1 đường cong với 2 điểm control point là 2 điểm cong.
    2. Với func addQuadCurve() thì sẽ chỉ có 1 điểm control point để tạo điểm cong.
      Kết quả thu được:

    Draw shape with shape layer:

    Việc sử dụng draw() để vẽ có 1 nhược điểm lớn là nó sẽ bị gọi ngay khi khởi tạo view, và không thể lưu lại. Vậy trong trường hợp bạn muốn vẽ vào 1 thời điểm khác chứ không phải từ đầu? Ta sẽ sử dụng 1 cách phổ biến và đơn giản hơn, là vẽ bằng cách sử dụng CAShapeLayer.

    ShapeLayer là gì?

    • Nhìn vào hình trên có thể thấy 1 view sẽ chứa 1 root layer kiẻu CALayer và root layer đó sẽ chứa nhiều sublayer.
    • CAShapeLayer là 1 class kế thừa từ CALayer.
    • Ta dùng CAShapeLayer để tạo ra các custom layer, sau đó sẽ add đề lên root layer của 1 view như là 1 sublayer. Do đó, ta có thể tạo các custom layer tại bất kì thời điểm nào ta muốn và có thể lưu lại.
    Thứ tự hiển thị trên view khi add các sublayer

    Vẽ các hình khối:

    Giờ thì sẽ bắt đầu vẽ các hình khối trên 1 sublayer và add đè lên layer của view.
    1 vài hình đơn giản như hình vuông, hình tam giác,… đơn thuần là các nét thẳng thì có thể vẽ đơn giản bằng cách ghép các đường thẳng:

    Vẽ hình vuông:

    func createSquare() {
            let shapeLayer = CAShapeLayer()
            
            let path = UIBezierPath()
            path.move(to: CGPoint(x: 0.0, y: 0.0))
            path.addLine(to: CGPoint(x: 100.0, y: 0.0))
            path.addLine(to: CGPoint(x: 100.0, y: 100.0))
            path.addLine(to: CGPoint(x: 0.0, y: 100.0))
    //        path.close() // creating a line segment between the first point and current point
            
            shapeLayer.path = path.cgPath
            shapeLayer.fillColor = UIColor.yellow.cgColor
            shapeLayer.strokeColor = UIColor.red.cgColor
            layer.addSublayer(shapeLayer)
        }
    • Tạo ra 1 shapeLayer.
    • Tạo ra 1 path, 1 path là sự ghép lại của nhiều đường thẳng.
    • Sau khi vẽ xong path, gán path cho shapeLayer.
    • Add đè shapeLayer vừa tạo lên root layer của view.

    Đưa đoạn code trên vào hàm init của UIView và chạy thử:
    Kết quả thu được:

    Hàm close() ở trên được dùng để tạo ra 1 đường thẳng nối điểm hiện tại và điểm đầu tiên khi bắt đầu vẽ.
    -> Uncomment hàm close() ở trên và run sẽ thu được kết quả:

    Ngoài ra cũng có thể dùng 1 hàm addLine() thay cho hàm close() để nối 2 điểm.

    Hình bầu dục:

    Để vẽ hình bầu dục, sử dụng hàm UIBezierPath(ovalIn: CGRect):

    private func createOval() {
        let shapeLayer = CAShapeLayer()
            
        shapeLayer.lineWidth = 2.0
        shapeLayer.fillColor = nil
        shapeLayer.strokeColor = UIColor.red.cgColor
        let openCirclePath = UIBezierPath(ovalIn: self.bounds)
            
        shapeLayer.path = openCirclePath.cgPath        
        layer.addSublayer(shapeLayer)
    }

    Bạn cung cấp 1 frame để vẽ hình bầu dục trong đó, ở đây mình sẽ dùng view.bounds. Kết quả thu được:

    Vẽ hình tròn:

    Để vẽ 1 hình tròn thì trước hết cần phải biết các góc trong 1 hình tròn:

    Sau đó chỉ cần dùng hàm được cung cấp sẵn:

    private func createCircle() {
        let shapeLayer = CAShapeLayer()
            
        shapeLayer.lineWidth = 2.0
        shapeLayer.fillColor = nil
        shapeLayer.strokeColor = UIColor.red.cgColor
        let openCirclePath = UIBezierPath(arcCenter: CGPoint(x: 60.0, y: 60.0),
                                          radius: 60.0,
                                          startAngle: 0.0,
                                          endAngle: CGFloat.pi * 2,
                                          clockwise: true)
            
        shapeLayer.path = openCirclePath.cgPath
        layer.addSublayer(shapeLayer)
    }
    • arcCenter: tâm hình tròn
    • radius: Bán kính
    • startAngle, endAngle: Điểm bắt đầu và điểm kết thúc.
    • clockWise: có vẽ theo chiều kim đồng hồ không.

    Tùy vào bạn muốn vẽ 1 góc bao nhiêu độ mà thay 1 radius thích hợp.
    Kết quả thu được:

    Bài viết này đã tóm tắt về cách vẽ hình cơ bản.
    Ở bài viết sau về shapeLayer, sẽ nói về ShapeLayer Attribute và ShapeLayer Animation.

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