Author: TienVV8

  • Apple CarPlay P2

    Apple CarPlay P2

    Hi, Ở phần 1 mình đã giới thiệu đến các bạn tổng quan về CP (CarPlay). Phần này mình sẽ chia sẻ cách để xây dựng một ứng dụng hỗ trợ CP, cụ thể ở đây là ứng dụng hỗ trợ một trong 8 CP types mà apple cung cấp, còn type Automaker thì mình chia sẻ mở bài tiếp theo.

    Bắt đầu nào!!!

    Tạo Project

    Ứng dụng CP cũng là một ứng dụng bình thường, các bạn vẫn sử dụng xCode và tạo project như bình thường. Phần code của phone và CP sẽ là hai phần riêng biệt. Vì trên iPhone quá quen thuộc với các bạn rồi nên ở hướng dẫn này mình chỉ thực hiện hiển thị 1 dòng chữ ở giữa màn hình trên iPhone và tập chung vào việc hiển thị trên CP với các template

    Tạo entitlement file

    Như mình đề cập ở phần 1, với 8 type bình thường của Apple cung cấp thì đi kèm với nó là các entitlement key, thì key này sử dụng ở đâu?. Câu trả lời là key này sử dụng để config trong entitlement file.

    Select file -> New file -> choose Property List

    Điền tên file và chọn create

    Ở đây mình để tên file là CPTemplateSeminar.entitlements

    Sau khi tạo entitlement file thì chúng ta cần nhúng đường path dẫn đến entitlement file vào Code Signing Entitlements bằng cách chọn Project Setting -> Build Setting -> Code Signing Entitlements và set giá trị là path dẫn đến entitlement file

    Tip: Ngoài cách tạo file và nhúng đường path như ở trên, các bạn có thể enable bất kì một Capabilities nào đó trong xCode để xCode tự động tạo ra entitlement file và tự động nhúng đường path vào Code Signing Entitlements

    Sau khi setting Code Signing Entitlements xong, Chúng ta đến bước add entitlement key vào file vừa tạo.
    Ở đây mình sử dụng key com.apple.developer.carplay-audio. (Nếu các bạn chưa biết key này là gì và lấy ở đâu thì các bạn xem lại phần 1 của mình nhé!)

    Implement Template

    Tạo một class kế thừa CPTemplateApplicationSceneDelegate, delegate này có các methods như didConnect, didDisconnectInterfaceController …

    import CarPlay
    
    class CPSceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate {
    
        var interfaceController: CPInterfaceController?
    
        // CarPlay connected
        func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene,
                                      didConnect interfaceController: CPInterfaceController) {
            self.interfaceController = interfaceController
        }
    
        // CarPlay disconnected
        func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didDisconnectInterfaceController interfaceController: CPInterfaceController) {
            self.interfaceController = nil
        }
    }
    

    Setting info.plist

    Tất cả các CP app đều phải khai báo CP scene để dử dụng CP Framework. Để khai báo CP scene thì chúng ta khai báo ở trong info.plist

    Khai báo thêm key CPTemplateApplicationSceneSessionRoleApplication theo như hướng dẫn dưới đây:
    Mình set UISceneConfigurationName là CarPlay, UISceneDelegateClassName là class mà mình đã tạo ở bên trên

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
    	<key>UIApplicationSceneManifest</key>
    	<dict>
    		<key>UIApplicationSupportsMultipleScenes</key>
    		<false/>
    		<key>UISceneConfigurations</key>
    		<dict>
    			<key>CPTemplateApplicationSceneSessionRoleApplication</key>
    			<array>
    				<dict>
    					<key>UISceneConfigurationName</key>
    					<string>CarPlay</string>
    					<key>UISceneDelegateClassName</key>
    					<string>$(PRODUCT_MODULE_NAME).CPSceneDelegate</string>
    				</dict>
    			</array>
    			<key>UIWindowSceneSessionRoleApplication</key>
    			<array>
    				<dict>
    					<key>UISceneConfigurationName</key>
    					<string>iPhone</string>
    					<key>UISceneDelegateClassName</key>
    					<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
    					<key>UISceneStoryboardFile</key>
    					<string>Main</string>
    				</dict>
    			</array>
    		</dict>
    	</dict>
    </dict>
    </plist>
    

    Setup Root Template

    Như một ứng dụng thông thường thì CP app cũng cần setup root view. Ở hướng dẫn này mình xây dựng CP app với root template là 1 tab bar bao gồm tab Home và tab Setting. Tab Home và Setting mình sẽ hiển thị list đơn giản

        // CarPlay connected
        func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene,
                                      didConnect interfaceController: CPInterfaceController) {
            self.interfaceController = interfaceController
            interfaceController.setRootTemplate(createTabTemplate(), animated: true, completion: nil)
        }
    import Foundation
    import CarPlay
    
    // MARK: - Tab Template
    extension CPSceneDelegate {
        func createTabTemplate() -> CPTabBarTemplate {
            var tabs: [CPTemplate] = []
    
            let homeItem = CPListItem(text: "Home", detailText: "")
            let homeSection = CPListSection(items: [homeItem])
            let homeTemplate = CPListTemplate(title: "Home", sections: [homeSection])
            homeTemplate.tabImage = UIImage(systemName: "house.fill")
    
            let settingItem = CPListItem(text: "Setting", detailText: "")
            let settingSection = CPListSection(items: [settingItem])
            let settingTemplate = CPListTemplate(title: "Home", sections: [settingSection])
            settingTemplate.tabImage = UIImage(systemName: "gearshape.fill")
    
            tabs.append(homeTemplate)
            tabs.append(settingTemplate)
    
            let tabBar = CPTabBarTemplate(templates: tabs)
            return tabBar
        }
    }
    

    Mình tạo một tab bar bằng cách sử dụng CPTabBarTemplate, và mình set tab bar này thành root template khi iPhone và màn hình trên ô tô được kết nối.

    Đến step này là đã hoàn thành việc coding một ứng dụng hỗ trợ CP rồi, Chúng ta có thể trải nghiệm ứng dụng bằng cách build lên simulator và sau đó mở cửa sổ xCode Simulator để xem thành quả. Tuy nhiên mình sẽ thêm phần action bấm vào item trên màn home và thực hiện di chuyển đến màn hình phát nhạc.

    Tạo Now Playing template

    import Foundation
    import CarPlay
    
    // MARK: - Playing now Template
    extension CPSceneDelegate {
        func createNowPlaying() -> CPNowPlayingTemplate {
            let playing = CPNowPlayingTemplate.shared
            playing.add(self)
            playing.isUpNextButtonEnabled = true
            playing.isAlbumArtistButtonEnabled = true
    
            return playing
        }
    }
    
    extension CPSceneDelegate: CPNowPlayingTemplateObserver {
        
        func nowPlayingTemplateUpNextButtonTapped(_ nowPlayingTemplate: CPNowPlayingTemplate) {
    
        }
    
        func nowPlayingTemplateAlbumArtistButtonTapped(_ nowPlayingTemplate: CPNowPlayingTemplate) {
    
        }
    }

    Thực hiện di chuyển khi bấm vào item trên màn hình Home. Mình sẽ thực hiện pushTemplate đến màn hình Now Playing được tạo bên trên, Các bạn có thể thực hiện push hoặc present tuỳ thích, tuy nhiên ở phần 1 mình cũng có warning là không phải template nào cũng có thể push và present được. Các bạn cứ thử thực hiện để trải nghiệm nhé!

    import Foundation
    import CarPlay
    
    // MARK: - Tab Template
    extension CPSceneDelegate {
        func createTabTemplate() -> CPTabBarTemplate {
            var tabs: [CPTemplate] = []
    
            let homeItem = CPListItem(text: "Home", detailText: "")
            homeItem.handler = { item, completion in
                self.interfaceController?.pushTemplate(self.createNowPlaying(), animated: true, completion: nil)
                completion()
            }
            let homeSection = CPListSection(items: [homeItem])
            let homeTemplate = CPListTemplate(title: "Home", sections: [homeSection])
            homeTemplate.tabImage = UIImage(systemName: "house.fill")
    
            let settingItem = CPListItem(text: "Setting", detailText: "")
            let settingSection = CPListSection(items: [settingItem])
            let settingTemplate = CPListTemplate(title: "Home", sections: [settingSection])
            settingTemplate.tabImage = UIImage(systemName: "gearshape.fill")
    
            tabs.append(homeTemplate)
            tabs.append(settingTemplate)
    
            let tabBar = CPTabBarTemplate(templates: tabs)
            return tabBar
        }
    }
    

    Ở đây mình chỉ sử dụng các template mà Audio type hỗ trợ, các bạn cũng có thể sử dụng các template mà Audio không hỗ trợ để trải nghiệm nhé, ví dụ như sử dụng Action Sheet template. (Lưu ý là app sẽ crash nhé!!!)

    Ngoài ra các bạn cũng có thể xây dựng ứng dụng của mình với các type còn lại để trải nghiệm nhé!

    Kết quả

    Sau khi build trên xCode Simulator mình được kết quả như sau:

    Trên đây là phần mình chia sẻ về cách tạo một ứng dụng hỗ trợ CP sử dụng type là template được cung cấp bởi Apple. Bài tiếp theo mình sẽ chia sẻ về cách tạo một ứng dụng hỗ tợ CP với type Automaker

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

  • Apple CarPlay P1

    Apple CarPlay P1

    Hi. Apple CarPlay không còn xa lạ với nhiều người, đặc biệt là các anh em sử dụng xe hàng ngày. Vậy làm sao để một ứng dụng hỗ trợ CP? (CarPlay). Trong bài này mình sẽ giới thiệu đến anh em về CP.

    CP là gì?

    CP là một cách an toàn để sử dụng iPhone trên xe ô tô, CP sẽ thực hiện các tác vụ mà người dùng muốn thực hiện với iPhone trong khi lái xe.

    CP Types

    Ở thời điểm hiện tại Apple cung cấp 9 CP types

    • Audio
    • Communication (Messaging & calling)
    • Driving task
    • EV charging
    • Fueling
    • Navigation
    • Packing
    • Quick food ordering
    • Automaker

    Đến đây chắc hẳn các bạn cũng thắc mắc tại sao type Automaker mình lại highlight lên như vậy. Thì đây là một type rất đặc biệt. Và ở phần 1 này mình chỉ chia sẻ về 8 types bên trên, riêng type Automaker mình sẽ chia sẻ ở phần tiếp theo.

    Mỗi CP type sẽ có một entitlement key, key này chính là để enable/disable tính năng CP trong các ứng dụng

    EntitlementKeyMinimum version
    CarPlay Audio App (CarPlay framework)
    App supports the CarPlay framework. Include both CarPlay audio app entitlements if your app supports the CarPlay framework and the Media Player framework.
    com.apple.developer.carplay-audio
    iOS 14
    CarPlay Audio App (Media Player framework)
    Deprecated. App supports the Media Player framework. Include both CarPlay audio app entitlements if your app supports the CarPlay framework and the Media Player framework.
    com.apple.developer.playable-content
    CarPlay Communication App
    App supports the CarPlay framework, and SiriKit intents for messaging or VoIP calling apps. May be combined with the optional CarPlay Messaging App and CarPlay VoIP Calling App entitlements to support iOS 13 and earlier.
    com.apple.developer.carplay-communicationiOS 14
    CarPlay Messaging App
    Deprecated. App relies solely on SiriKit and supports SiriKit intents to send, request, and modify messages. May be combined with the CarPlay Communication App entitlement, and the optional CarPlay VoIP Calling App entitlement to support iOS 13 and earlier.
    com.apple.developer.carplay-messaging
    CarPlay VoIP Calling App
    Deprecated. App relies solely on SiriKit and CallKit, and supports SiriKit intents for starting calls and requesting a list of calls. May be combined with the CarPlay Communication App entitlement, and the optional CarPlay Messaging App entitlement to support iOS 13 and earlier.
    com.apple.developer.carplay-calling
    CarPlay Driving Task App
    App supports the CarPlay framework.
    com.apple.developer.carplay-driving-taskiOS 16
    CarPlay EV Charging App
    App supports the CarPlay framework. May be combined with the CarPlay Fueling App entitlement.
    com.apple.developer.carplay-chargingiOS 14
    CarPlay Fueling App
    App supports the CarPlay framework. May be combined with the CarPlay EV Charging App entitlement.
    com.apple.developer.carplay-fuelingiOS 16
    CarPlay Navigation App
    App supports the CarPlay framework.
    com.apple.developer.carplay-mapsiOS 12
    CarPlay Parking App
    App supports the CarPlay framework.
    com.apple.developer.carplay-parkingiOS 14
    CarPlay Quick Food Ordering App
    App supports the CarPlay framework.
    com.apple.developer.carplay-quick-orderingiOS 14
    đây là các key của 8 types được cung cấp bưởi Apple

    Note: Khi xây dựng một ứng dụng có hỗ trợ CP, thì chúng ta chỉ được chọn một trong 8 type này. Và config với key tương ứng trong file entitlement, như vậy là compiler đã hiểu là ứng dụng này có hỗ trợ CP.

    CP Templates

    Apple cung cấp 12 templates để hiển thị trên CP. Chúng ta không thể thay đổi layout của của các template này mà chỉ có thể input data vào để hiển thị.

    1. Action Sheet
    2. Alert
    3. Grid
    4. List
    5. Tab bar
    6. Information
    7. Point of interest
    8. Now Playing
    9. Contact
    10. Map
    11. Search
    12. Voice control

    Hình ảnh của các templates có đầy đủ ở page 18, 19, 20, 21, 22, 23, 24

    Mối quan hệ giữa CP Types và CP Templates

    Ở trên mình đã giới thiệu đến các bạn types và templates trong CP. Vậy nó có mối quan hệ gì với nhau không?

    Câu trả lời là có, mỗi CP type sẽ hỗ trợ một vài templates.

    Ví dụ: với những ứng dụng CP Audio thì chỉ có thể hiển thị được với các templates như Alert, Grid, List, Tab bar, Now Playing

    Trong trường hợp chọn một template không hỗ trợ để hiển thị thì app sẽ crash.

    Ví dụ: ứng dụng CP Audio không hỗ trợ type Action Sheet, nếu chúng ta sử dụng type Action Sheet để hiển thị thì ứng dụng CP sẽ crash.

    Di chuyển màn hình

    • Như một ứng dụng chạy trên iPhone, thì CP cũng có thể di chuyển màn hình bằng hai phương thức chính là push và present. Nhưng bản chất ở đây là di chuyển giữa các templates
      Ví dụ: Chúng ta xây dựng ứng dụng CP Audio, màn hình root view là List template, khi bấm vào một item trong list thì di chuyển đến template Now Playing thì đơn giản là chúng ta sử dụng phương thức push hoặc present để di chuyển.
    • Không phải template nào cũng có thể push và present được.
      Ví dụ: khi các bạn push đên Grid template thì không vấn đề gì, còn khi các bạn present đến Grid thì CP sẽ crash và báo present không support Grid.
    • limit khi di chuyển màn hình, hầu hết các app sẽ có độ sâu phần cấp là 5, ứng dụng Fueling sẽ có độ sâu là 3, ứng dụng Driving task và Quick food ordering có độ sâu là 2. Tính từ màn hình root.
      Ví dụ: khi xây dựng CP Audio các bạn không thể di chuyển quá 5 màn hình
      Template1 -> Template2 -> Template3 -> Template4 -> Template5 -> Template6
      Di chuyển từ Template1 đến Template5 thì không sao, và app sẽ crash khi di chuyển từ Template5 đến Template6

    Hướng dẫn khi xây dựng ứng dụng CP

    Khi xây dựng một ứng dụng CP thì chúng ta phải tuân thủ theo 8 hướng chung sau đây: (Mình sẽ lấy nguyên các hướng dẫn của Apple cung cấp để cho các bạn tham khảo nhé)

    1. Your CarPlay app must be designed primarily to provide the specified feature to a user (e.g. CarPlay audio apps must be designed primarily to provide audio playback services, CarPlay parking apps must be designed primarily to provide parking services, etc.).
    2. Never instruct users to pick up their iPhone to perform a task. If there is an error condition, such as a required log in, you can let users know about the condition so they can take action when safe. However, user messages must not include wording that asks users to manipulate their iPhone.
    3. All CarPlay user flows must be possible without interacting with iPhone.
    4. All CarPlay user flows must be meaningful to use while driving. Don’t include features in CarPlay that aren’t related to the primary task (e.g. unrelated settings, maintenance features, etc.).
    5. No gaming or social networking.
    6. Never show the content of messages, texts, or emails on the CarPlay screen.
    7. Use templates for their intended purpose, and only populate templates with the specified information types (e.g. a list template must be used to present a list for selection, album artwork in the now playing screen must be used to show an album cover, etc.).
    8. All voice interaction must be handled using SiriKit (with the exception of CarPlay navigation apps, see below).

    Ngoài 8 hướng dẫn chung này ra thì một số CP types có các hướng dẫn đặc biệt nữa. Chúng ta có thể tham khảo ở page 6, 7, 8, 9, 10

    Môi trường phát triển

    Cũng như các ứng dụng trên iPhone khi các bạn muốn chạy trên device thật thì cần certificate và provisioning. Thì ứng dụng CP cũng vậy. Tuy nhiên khi chạy ứng dụng ra simulator thì không cần thiết.
    Ví dụ: Khi xây dựng một ứng dụng hỗ trợ notification thì cần enable notification ở capabilities, thì với ứng dụng CP cũng cần enable entitlement ở trong Additional Capabilities.

    Mục Additional Capabilities của tất cả các Apple Account đều là rỗng, developer phải request đến Apple và đợi họ approve thì mới có thể enable Capabilities để chạy trên device thật.

    Account của mình đã được Apple approve với type Audio, mình nhớ là mất khoảng 14 ngày để Apple approve.

    Đây là Link request nhé!

    CP Simulator

    Apple cung cấp cho chúng ta hai simulators để thực hiện test trong quá trình phát triển ứng dụng CP

    • xCode Simulator: đây là simulator tích hợp sẵn trong xCode. Chúng ta có thể mở simulator này lên bằng cách chọn I/O -> External Display -> CarPlay…
    • CP Simulator: đây là một tool riêng biệt, nó mô phỏng lại môi trường của xe ô tô và phải cài ứng dụng hỗ trợ CP lên iPhone, sau đó kết nối với máy tính và mở CP Simulator lên (sau khi tải tool về thì mở folder Hardware -> CarPlay Simulator), tất cả các apps hỗ trợ CP trên iPhone sẽ hiển thị trên CP simulator.
      Link tải tool đây nhé, lưu ý các bạn chọn đúng tool mà xCode của mình đang sử dụng nhé

    Trên đây là phần giới thiệu của mình về CP, phần tiếp theo mình sẽ chia sẻ việc implement một ứng dụng hỗ trợ CP.

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

  • iOS/Swift In-App Purchase (P2)

    iOS/Swift In-App Purchase (P2)

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

    Implement

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

    Bắt đầu implememt code

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

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

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


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

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

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

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

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

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

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

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

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

    Test In-App Purchase sau khi implement

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

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

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

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

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

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

  • iOS/Swift In-App Purchase (P1)

    iOS/Swift In-App Purchase (P1)

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

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

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

    Các kiểu Thanh toán

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

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

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

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

    Setup môi trường

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

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

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

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

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

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

    Setup Payments

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

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

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


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


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

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

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

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

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

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

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

  • Deprecate old API in Swift

    Deprecate old API in Swift

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

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

    Định nghĩa:

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

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

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

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

    Basic usage

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

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

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

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

    Deprecated version

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


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

    Deprecated message

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

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

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

    Rename

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

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

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

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

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

    Obsoleted

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

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

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

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

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

  • Combine big framework từ iOS 13

    Combine big framework từ iOS 13

    Định nghĩa

    Combine là một framework của Apple được tích hợp từ iOS 13. Combine cung cấp các API cho phép khai báo để xử lý các giá trị theo thời gian. Các giá trị này có thể đại diện cho nhiều loại sự kiện không đồng bộ. Combine cung cấp các Publishers hiển thị các giá trị có thể thay đổi theo thời gian và subscribers nhận các giá trị đó từ Publishers.

    Reactive có nghĩa là lập trình với các luồng giá trị không đồng bộ. Các bạn có thể tìm hiểu thêm về Reactive ở đây

    Functional programming là tất cả các chức năng trong lập trình. Trong Swift, các hàm có thể được truyền dưới dạng đối số cho các hàm khác, được trả về từ các hàm, được lưu trữ trong các biến và cấu trúc dữ liệu và được xây dựng trong thời gian chạy dưới dạng bao đóng.

    Trong style của declarative, bạn mô tả những gì chương trình thực hiện mà không mô tả luồng điều khiển. Theo style imperative, bạn viết cách thức hoạt động của chương trình bằng cách triển khai và xử lý một loạt tác vụ. Các chương trình imperative chủ yếu dựa vào trạng thái, thường được sửa đổi bằng các bài tập.

    Lập trình với Combine là sự kết hợp của declarative, reactive và functional. Nó liên quan đến các chức năng xâu chuỗi và chuyển các giá trị từ cái này sang cái khác. Điều này tạo ra các luồng giá trị, chảy từ đầu vào đến đầu ra.

    Lý thuyết là vậy, nhưng nếu chúng ta bỏ qua hết các định nghĩa thì Combine được thể hiện đơn giản như hình ảnh bên dưới đây:

    Và thệm trí có thể ngắn gọn hơn:
    Combine = Publishers + Subscribers + Operators

    Đến đây có vẻ chúng ta đã hiểu được Combine là gì rồi nhỉ, Tiếp tục đi đến các thành phần khác trong combine nhé.

    Publisher là gì?

    Publisher là gửi chuỗi giá trị theo thời gian đến một hoặc nhiều subscribers

    Combine publishers được tuân thủ theo protocol sau:


    Một publisher có thể gửi giá trị hoặc terminate với thành công hoặc lỗi. Output xác định loại giá trị mà publisher có thể gửi. Failure xác định loại lỗi mà nó có thể thất bại.

    Mothod receive(subscriber:) kết nối subscriber với publisher. Nó xác định hợp đồng: đầu ra của publisher phải khớp với đầu vào của subscriber và các loại lỗi cũng vậy.

    Subscriber là gì?

    Subscriber là để nhận các giá trị từ Publisher



    Một subscriber có thể nhận nhận giá trị với type Input hoặc termination với thành công hoặc lỗi

    các mothods receice mô tả các bước khác nhau trong vòng đời của người đăng ký. Chúng ta sẽ tìm hiểu vòng đời của subscriber này ở phần tiếp theo nhé!

    Kết nối Publisher với Subscriber

    Combine có hai methods được tích hợp sẵn: Subscribers.Sink và Subscribers.Assign. Bạn có thể kết nối chúng bằng cách gọi một trong hai method này bên dưới publisher

    • Sink(receiveCompletion:receiveValue:) để xử lý phần tử mới hoặc sự kiện hoàn thành trong một lần đóng.
    • assign(to:on:) để ghi phần tử mới vào một thuộc tính.


    1. Tạo ra một publisher Just gửi một signle giá trị và sau đó hoàn thành. Combine sẵn một số built-in trong đó có Just
    2. Kết nối publisher với subscriber bằng method sink

    Kết quả in ra lần lượt là:

    1
    finished

    Sau khi send 1 thì publisher tự động kết thúc, ở đây chúng ta không xử lý bất kì error nào, bởi vì Just không bao giờ thật bại.

    Subjects là gì?

    Subject là một lại publisher đặc biệt, nó có thể insert giá trị, truyền được giá trị từ bên ngoài vào. Giao diện của Subject cung cấp ba cách khác nhau để gửi các elements



    Combine có hai chủ thể tích hợp sẵn: PassthroughSubject và CurrentValueSubject.

    Bắt đầu với PassthroughSubject


    1. Tạo ra một passthrough subject. Chúng ta set Failure type là Never để cho nó biết rằng nó luôn kết thúc thành công.
    2. Đăng ký subject(hay nhớ rằng đó vẫn là một publisher).
    3. Gửi hai giá trị tới luồng và sau đó hoàn thành nó.

    Kết quả lần lượt như sau:

    Hello,
    World!
    finished

    Tiếp theo mình đi đến CurrentValueSubject


    1. Tạo ra một subject với giá trị khởi tạo là 1
    2. In ra giá trị hiện tại
    3. Cập nhật giá trị hiện tại thành 2 và in nó ra
    4. Đăng ký publisher

    Kết quả lần lượt như sau:

    1
    2
    2

    Combine Publisher and Subscriber Life Cycle

    Như đề cập ở bên trên, bây giờ chúng ta cùng tìm hiểu về life cycle của Publisher và Subscriber nhé!!!

    Sự kết nối giữ Publisher và Subscriber được gọi là subscription. Thông qua các bước của kết nối như vậy xác định vòng đời của Publisher và Subscriber


    Để ý đến operator print(_:to:) ở trên. Nó in ra các thông báo cho tất cả các sự kiện của Publisher vào console, từ các log được in ra thì chúng ta cũng có thể nắm được một phần nào đó về vòng đời của Publisher và Subscriber. Đây là log được ghi nhận từ console

    Từ log bên trên chúng ta đã có manh mối về vòng đời của publisher-subscriber. Với log ghi được, chúng ta sẽ đi phân tích từ đầu đến cuối nhé

    1. Subscriber kết nối với Publisher bằng cách gọi subscribe(S).

    2. Tạo một đăng ký tới Publisher bằng cách gọi receive(subscriber: S) trên chính nó.

    3. Publisher nhận yêu cầu đăng ký. Nó gọi receive(subscription:) trong subscriber

    4. Subscriber đăng ký một phần tử mà nó muốn nhận, Nó gọi đến request(:) và chuyển Request dưới dạng tham số. Nhu cầu xác định số lượng mục mà publisher có thể gửi cho subsriber thông qua subscription. Trong trường hợp này thì nhu cầu là không giới hạn (unlimited)

    5. Publisher gửi giá trị thông bằng cách gọi receive(_:) trên subscriber. Method này trả về một Demand, cho biết có bao nhiêu items mà subscriber mong muốn nhận được. Subscriber chỉ có thể tăng demand hoặc để nguyên, không thể giảm demand đi

    6. Kết thúc đăng ký với một trong những kết quả sau đây:
    – Cancelled Điều này có thể tự động xảy ra khi publisher được giải phóng, được hiển thị trong ví dụ bên trên. Một cách khác là hủy thủ công: token.cancel().
    – Finish thành công
    – Fail với một error.

    Chaining Publishers với Operators

    Operators là các methods đặc biệt được gọi bên dưới Publisher và trả ra một Publisher khác. Điều này cho phép áp dụng chúng một cách lần lượt, tạo ra một chuỗi. Mỗi operator đều biến đổi publisher cũ và được trả ra từ chính publisher cũ đó.

    Một operator đều tạo mới một publisher. Sau đó, các operators có thể áp dụng lần lượt. Mỗi operator nhận được được tạo ra từ publisher trước đó trong chỗi. Ở đây mình đề cập đến thứ tự tương đối của opetator là ngược dòng, nghĩa là operator liền trước và tiếp theo.

    Bây giờ chúng ta cùng nhau đến một ví dụ thông qua đó có thể xem cách xâu chuỗi operators khi xử lý request HTTP URL với Combine



    1. Tạo một request để tải kho GitHub. Mình đang sử dụng API GitHub REST.
    2. Combine được tích hợp tốt vào Swift system frameworks và SDK iOS. Điều này cho phép chúng ta sử dụng publisher để xử lý các tác vụ dữ liệu URLSession.
    3. Truyền dữ liệu phản hồi. Chúng ta sử dụng toán tử map(_:), biến đổi giá trị ngược dòng từ (data: Data, response: URLResponse) thành Data.
    4. Decode nội dung phản hồi bằng JSONDecoder.
    5. Kết nối sink subscriber. Nó in số lượng kho lưu trữ đã nhận và hoàn thành.

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

    V8tr has 30 repositories
    finished

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

  • Schedulers in Swift Combine Framework

    Schedulers in Swift Combine Framework

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

    Nội dung:

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

    Scheduler là gì?

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

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

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

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

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

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

    Scheduler Mặc định

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

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

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

    Switching Schedulers

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

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

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

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

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

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

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

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

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

    Thực hiện Asynchronous với Combine

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

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

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

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

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

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

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

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

  • Kiểm tra kết nối của thiết bị với NWPathMonitor

    Kiểm tra kết nối của thiết bị với NWPathMonitor

    Hi các bạn, Trong quá trình develop của mình chắc hẳn các bạn đã sử dụng đến chức năng kiểm tra tình trạng kết nối của device. Đơn giản như việc kiểm tra internet khi call API hay download một cái gì đó.

    Trước đây thông thường chúng ta sử dụng Network Reachability.

    Network Reachability ở đây hiểu nôm na là trạng thái kết nối mạng của device. Chúng ta có thể sử dụng để xác định rằng device đang online hay offline, thậm chí là đang dùng mạng wifi hay gói dữ liệu data từ nhà mạng. Tuy nhiên đây không phải một thư viện chính thống từ Apple và chúng ta phải nhúng vào source code của mình trước khi sử dụng.

    Với iOS 12, Apple giới thiệu tới các developer một framework mới để kiểm tra tình trạng kết nối của device. NWPathMonitor có lẽ đây là một sự thay thế hoàn hảo cho Reachability.

    Trong sự kiện WWDC tháng 6 năm 2018, một framework mới có tên Network framework được giới thiệu dành cho iOS 12 trở đi. Một đối tượng có tên NWPathMonitor, framework này đem đến cho chúng ta một cách chính thống để xác định trạng thái kết nối mạng thay vì phải dùng thư viện của bên thư ba như từ trước tới nay.

    Dưới đây là cách sử dụng của NWPathMonitor

    Để sử dụng NWPathMonitor chúng ta chỉ cần import Network và sau đó tạo một đối tượng của NWPathMonitor như sau. Tất nhiên ứng dụng của các bạn phải hỗ trợ từ iOS 12 trở lên nhé.

    Với khởi tạo như ở trên thì chúng ta đang kiểm tra connect của tất cả tất cả các Interface type. Nếu chúng ta chỉ quan tâm đến Nếu chỉ quan tâm đến những thay đổi của một network adapter cụ thể ví dụ như Wifi chẳng hạn, chúng ta có thể khởi tạo bằng phương thức init(requiredInterfaceType:) với tham số truyền vào là một kiểu dữ liệu NWInterface.InterfaceType như sau:

    Đây là các interface type mà NWPathMonitor hỗ trợ theo dõi:

    • cellular
    • loopback
    • wifi
    • wiredEthernet
    • other

    Để nhận được các thay đổi về network state, chúng ta chỉ cần gán callback cho một thuộc tính có tên pathUpdateHandler, callback này sẽ được gọi đến bất cứ khi nào có sự thay đổi đối với network, ví dụ như khi điện thoại chuyển từ sử dụng cellular sang wifi. Trong callback có một giá trị trả về có kiểu NWPath giúp chúng ta có thể dễ dàng xác định được trạng thái đã connect hay chưa:

    Đối tượng NWPath có các thuộc tính mà qua đó chúng ta thể dùng để xác định khá nhiều trạng thái của network. Một trong số đó là một thuộc tính khá thú vị mang tên isExpensive dùng để xác định xem network hiện tại có được coi là đắt tiền hay không. Chúng ta cũng có thể kiểm tra xem có hỗ trợ DNS, IPv4 hoặc IPv6 hay không. Ngoài ra nếu muốn xem network trước khi bị thay đổi sang network hiện tại là gì chúng ta có thể sử dụng thuộc tính usedInterfaceType.

    Để bắt đầu lắng nghe trạng thái connect của device, chúng ta gọi phương thức start(). Và gọi stop() khi muốn kết thúc việc lắng nghe.

    Khi đã hoàn tất quá trình lắng nghe sự thay đổi, chúng ta chỉ cần đơn giản gọi phương thức cancel(). Lưu ý rằng một khi đã gọi phương thức cancel() chúng ta không thể gọi phương thức start() được nữa. Thay vào đó chúng ta cần khởi tạo một biến NWPathMonitor mới.

    Trên đây mình đã giới thiệu đến các bạn về NWPathMonitor một framework hỗ trợ từ iOS 12 của Apple, cũng như cách sử dụng của nó.

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

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

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

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

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

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

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

    Định nghĩa

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

    Custom URLProtocol

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

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

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

    Unit Test cho Network

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

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

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

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

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

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

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

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

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

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

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

  • Tạo server mock bằng Postman

    Tạo server mock bằng Postman

    Hi mọi người. Trong quá trình develop chắc hẳn các bạn đã từng gặp trường trường hợp server đã define json response nhưng server deploy. Việc chờ server và làm UI trước thì cũng được, nhưng trong khi làm UI mà có một server để thực hiện request và có thể thay đổi response theo mong muốn của mình thì thật là tuyệt vời.

    Trong bài này mình sẽ hướng dẫn cách tạo ra một server mock thông qua postman.

    Tóm tắt nội dung của bài:

    • Gửi một request
    • Lưu request vào collection
    • lưu response
    • Tạo mock server từ collection
    • request đến server mock

    Bắt đầu nào!!!!

    Step 1: Gửi một request

    Send một request bất kì. Mục đích của việc send một request này là để lấy response và lưu lại

    Ở đây mình request với url https://postman-echo.com/get?test=123


    Đây là response từ https://postman-echo.com/get?test=123

    Step 2: Lưu request vào một collection

    Đầu tiền mình thực hiện tạo một collection mới và đặt tên là “Mock Collection”




    Tiếp theo lưu request vào Mock Collection

    Step 3: Lưu response

    Lưu response của request bên trên vào trong Mock Collection




    Sau khi lưu response, các bạn có thể đổi tên response đó, ở đây mình đổi tên thành “test-get”

    Step 4: Tạo Mock Server từ collection

    Tạo Mock Server cho Mock Collection vừa tạo ở trên.


    Đặt tên cho server mock và ấn tạo


    Về cơ bản thì thực hiện đến đây là server mock đã hoàn thành và có thể thực hiện request rồi. Các bạn nhớ copy url của server mock nhé!!!

    Step 5: Request đến server mock

    Dùng url đã copy ở step 4 và thực hiện một request bằng postman
    url mình đã copy ở step 4 như sau: https://f4205914-b48b-4ff4-9dfe-5cbf131a24d4.mock.pstmn.io


    Opp!!! lỗi rồi…. Lúc này server mock sẽ trả ra lỗi 404 như ảnh bên trên. Lý do lỗi ở đây là bạn chưa thêm path cho mock server url.

    Các bạn thêm đuôi /get cho url và đổi method thành GET. Lúc này server mock sẽ trả ra response như đã lưu ở step 3

    sau khi sửa url thì nó sẽ như thế này: https://f4205914-b48b-4ff4-9dfe-5cbf131a24d4.mock.pstmn.io/get



    Các bạn có thể đổi response của server mock theo mong muốn của mình
    Ở đây mình đổi json đã lưu ở step 3 thành {“title”: “Hello World”}. sau khi đổi json response thì ấn Save ở góc trên bên phải.


    Sau khi thay đổi và lưu response xong, thực hiện lại request đến server mock. Và kết quả như sẽ như response đã sửa ở trên

    Trên đây mình đã hướng dẫn các bạn tạo server mock và có thể chỉnh sửa response theo mong muốn của mình.

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

    Tham khảo: https://learning.postman.com/docs/designing-and-developing-your-api/mocking-data/mocking-with-examples/