Month: June 2020

  • Hướng dẫn tạo và sử dụng Manually Signing App

    Hướng dẫn tạo và sử dụng Manually Signing App

    Link tham khảo: https://help.apple.com/developer-account/

    Code Signing là một thứ bắt buộc để ta có thể cài đặt ứng dụng vào devices thật, hoặc để upload lên AppStore Connect. Có hai cách để cài đặt code signing, “Automatically manage signing” hoặc “Manually manage signing”. Bài viết này sẽ hướng dẫn cài đặt manual code signing.

    Để cài đặt app, bạn cần có :

    • Signing certificate (Personal Information Exchange, .p12)
    • Provisioning profile (.mobileprovision)

    Signing certificate là chứng chỉ giúp xác định danh tính để cài app

    Provision profile (development hoặc distribution) chứa những thông tin về appID, các devices mà app có thể cài đặt, thông tin certificate để signing app. Lưu ý rằng nếu ứng dụng có chứa các extensions, bạn cần thêm các provision profile tương ứng 

    Mỗi dự án sẽ có những certificate và provision profile riêng. Như ảnh dưới, ta có 2 certificate, cho môi trường dev và distribute, ta cũng có các provision dev và distribute tương ứng, kèm theo đó là những provision profile của các extension của app.

    Sau đây là hướng dẫn tạo manually signing app:

    B1. Tạo certificate cho app

    1. Ở Certificates, Identifiers & Profiles, chọn Certificates

    2. Chọn nút (+)

    3. Chọn loại certificates mà mình muốn và chọn nút “Tiếp tục”

    4. Tạo certificate signing request

    4.1. Mở app Keychain Access ở máy

    4.2. Chọn Keychain Access > Certificate Assistant > Request a Certificate from a Certificate Authority. 

    4.3. Điền thông tin như email, name, bổ trống CA Email Address

    4.4. Chọn “Save to disk” và chọn tiếp tục

    5. Chọn file đuôi .certSigningRequest đã tạo ở b4

    6. Chọn “Tiếp tục” và “Tải về” máy. (File certificate sẽ có đuôi .cer)

    B2. Đăng ký AppID

    AppID sẽ định danh app của bạn trong provisioning profile. Có 2 loại AppID: explicit AppID (sử dụng riêng từng app) và wildcard AppID (sử dụng chung 1 số app). Wildcard AppID sẽ chỉ enable được một số Capabilities, nếu muốn sử dụng những Capabilities khác, bạn phải tạo explicit AppID

    Các bước tạo AppID:

    1. Trong Certificates, Identifiers & Profiles, chọn “Identifiers”, rồi chọn (+)
    2. Chọn AppIDs 
    3. Điền name, descriptions, chọn các loại Capabilities mà app sẽ dùng
    • Nếu chọn Explicit App ID, bạn phải điền giống bundleID của app trong Xcode
    • Nếu chọn Wildcard App ID, bạn phải điền bundle ID với hậu tố (VD: com.domainname.*)

    B3. Đăng ký devices

    Đăng ký một device:

    1. Trong Certificates, Identifiers & Profiles, chọn Devices, rồi chọn (+)
    2. Chọn platform, điền device name, device ID (UDID)
    3. Chọn tiếp tục, chọn “Register” để hoàn tất đăng ký

    Đăng ký nhiều device:

    Bạn có thể dùng app “Configurator 2” trên MacAppStore hoặc tạo file .txt chứa thông tin (mỗi dòng chứa deviceID, device name, platform name cách nhau bởi tab-delimited)

    B4. Tạo provisioning profile

    1. Trong Certificates, Identifiers & Profiles, chọn Profiles, rồi chọn nút (+) 
    2. Chọn loại provisioning profile mà bạn muốn tạo, rồi chọn “Tiếp tục”

    3. Chọn App ID mà mình đã tạo ở Bước 2, chọn “Tiếp tục”

    4. Chọn Certificate mà mình đã tạo ở Bước 1, chọn “Tiếp tục”

    5. Chọn các device đã được tạo ở Bước 3, chọn “Tiếp tục”

    6. Điền profile name, rồi chọn “Generate”

    7. Chọn “Download” để tải về

    8. Sau khi đã tải về, click double vào các certificate và nhập mật khẩu để add vào keychain

    • Tắt Automatically manage signing trong Xcode 
    • Import các provision profile tương ứng 

    Nếu status không còn báo đỏ nữa là bạn đã import thành công. Giờ run và build thôi.

  • MVVM with Swift P2

    MVVM with Swift P2

    Demo MVVM với RxSwift:

    Model Layer:

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

    Class Service để thao tác với database:

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

    ViewModel Layer

    import Foundation
    import RxSwift
    
    class HomeViewModel {
        var category: Category
        var service: ICategoryService
        
        var displayName: String {
            return transformName(category.name)
        }
        
        // 1
        var valueSubject: PublishSubject<String>
        
        init(category: Category, service: ICategoryService) {
            self.category = category
            self.service = service
            valueSubject = PublishSubject()
        }
        
        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()
        }
        
        // 2
        func getCategory(id: Int) {
            if let newCategory = service.getCategoryById(id: id) {
                self.category = newCategory
                valueSubject.onNext(transformName(category.name))
            }
        }
    }
    1. Khởi tạo 1 subject kiểu Publish Subject để phát ra các event khi giá trị của category’s name được thay đổi.
    2. ViewModel sẽ thông qua Model Layer để truy xuất đến database. Sau khi lấy được data cần thiết, thì valueSubject sẽ phát ra 1 event có giá trị là name đã được transform của category mới.

    View Layer:

    import UIKit
    import RxSwift
    
    class HomeView: UIViewController {
        @IBOutlet private weak var categoryNameLabel: UILabel!
        
        private var viewModel: HomeViewModel?
        private let disposeBag = DisposeBag()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            let category = Category(id: 1, name: "Hollywood")
            viewModel = HomeViewModel(category: category, service: CategoryService())
            // 1
            self.categoryNameLabel.text = viewModel?.displayName
            
            // 2
            viewModel?.valueSubject
                .skip(1)
                .subscribeOn(MainScheduler.instance)
                .subscribe(onNext: { (displayName) in
                    self.categoryNameLabel.text = displayName
                })
                .disposed(by: disposeBag)
        }
        
        @IBAction func didTapButtonChangeCategory(_ sender: Any) {
            // 3
            viewModel?.getCategory(id: 3)
        }
    }
    1. Set text cho label lần đầu khi khởi tạo category.
    2. Lắng nghe khi viewModel phát ra 1 value mới.
    3. Tap button để yêu cầu viewModel lấy ra 1 category có id = 3.

    Tác dụng của MVVM:

    • Tương tự như MVP, lợi ích đầu tiên của MVVM là tách biệt phần logic ra khỏi View. Từ đó dẫn đến dễ viết unit test, dễ maintain, …
    • Dễ dàng reuse các ViewModel.
    • Bằng việc tương tác với View thông qua cơ chế data binding, vì vậy không cần tạo thêm nhiều protocol, class…

    MVVM vs MVP:

    • Trong MVVM, vì ViewModel tương tác với View bằng data binding, vì vậy ViewModel không có 1 reference nào đến View. Từ đó ViewModel sẽ dễ dàng được reuse, dễ dàng viết test hơn so với MVP.
    Presenter và View là liên kết chặt với quan hệ 1:1 trong MVP
    Quan hệ giữa ViewModel và View trong MVVM
    • Trong MVVM, 1 View có thể có nhiều ViewModel.
    • MVVM sẽ không cần phải tạo nhiều protocol như MVP.
    • Việc sử dụng cơ chế data binding cũng dẫn đến 1 hệ quả là MVVM sẽ khó để debug hơn nhiều so với việc sử dụng MVP.
    • Nếu sử dụng RxSwift kết hợp với MVVM, thì sẽ phải đòi hỏi team của bạn đều phải biết RxSwift ở mức ổn.

    Kết luận:

    • Mục tiêu chính của MVVM là 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 nguyên tắc MVVM thì ViewModel không import UIKit để tách biệt logic và View.
  • 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.