Category: iOS

  • Làm cách nào để thực hiện cuộc gọi, gọi FaceTime và gửi SMS trong ứng dụng sử dụng Swift?

    Làm cách nào để thực hiện cuộc gọi, gọi FaceTime và gửi SMS trong ứng dụng sử dụng Swift?

    Hiện nay hầu hết các ứng dụng di động đều có tính năng liên lạc nhằm mục đích giúp người sử dụng dễ dàng liên hệ được với bộ phận chăm sóc khách hàng. Để làm được việc này thì Apple có cung cấp một URL Scheme để thực hiện việc này.

    URL Scheme Mail

    Để có thể sử dụng được tính năng gửi mail thông qua app của Apple chúng ta cần cấu hình dự án cho phép sử dụng URL schemes “mailto” như sau:

    Mở file info.plist và thêm Queried URL Shemes -> add item và đặt trường value với giá trị là mailto

    Để thực hiện được việc gửi mail chúng ta thực hiện đoạn code như sau:

      func mail(to: String, cc: String = "", subject: String = "", body: String = "") {
            
            var mailURLString: String = "mailto:"
            
            // add to
            mailURLString += to
            
            // add cc
            mailURLString += "?cc=\(cc)"
            
            // add subject
            if let subjectEncode = subject.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) {
                mailURLString += "&subject=\(subjectEncode)"
            }
            
            // add content
            if let bodyEncode = body.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) {
                mailURLString += "&body=\(bodyEncode)"
            }
            
            // check url
            if let mailURL = URL(string: mailURLString), UIApplication.shared.canOpenURL(mailURL) {
                // open app mail with url
                UIApplication.shared.open(mailURL)
            }
        }

    Bây giờ bất kể chỗ nào chúng ta dùng để gửi mail đều có thể call func này để thực hiện việc gửi mail ví dụ như sau:

    mail(to: "[email protected]", cc: "[email protected]", subject: "Techover.io", body: "New post")

    URL Scheme Phone

    Tương tự như Mail việc gọi điện app cũng có 1 scheme cho phép thực hiện việc này.

        func tel(to: String) {
            if let telURL = URL(string: "tel:\(to)"), UIApplication.shared.canOpenURL(telURL) {
                UIApplication.shared.open(telURL)
            }
        }

    URL Scheme FaceTime

        func faceTime(to: String) {
            if let telURL = URL(string: "facetime-audio://\(to)"), UIApplication.shared.canOpenURL(telURL) {
                UIApplication.shared.open(telURL)
            }
        }

    URL Scheme SMS

        func sms(to: String) {
            if let telURL = URL(string: "sms:\(to)"), UIApplication.shared.canOpenURL(telURL) {
                UIApplication.shared.open(telURL)
            }
        }

    Ngoài ra chúng ta còn có thêm các Apple URL Scheme khác như MAP Links , iTunes Links, YouTube Links … Anh em có thể tham khảo thêm tài liệu của Apple ở link sau: https://developer.apple.com/library/archive/featuredarticles/iPhoneURLScheme_Reference/Introduction/Introduction.html#//apple_ref/doc/uid/TP40007899-CH1-SW1

  • IBInspectable and IBDesignable in Swift

    IBInspectable and IBDesignable in Swift

    Xin chào mọi người, bài viết này mình xin giới thiệu với các bạn về IBInspectable và IBDesignable trong swift.

    IBInspectable

    Khi các bạn thực hiện code UI bằng Interface builder của Xcode, nó sẽ hiển thị cho các bạn một số các thuộc tính cơ bản để các bạn có thể chỉnh sửa. Hình dưới đây là Atributes Inspector của UIView.

    Inspectable-and-IBDesignable

    Có bao giờ bạn muốn thêm các thuộc tính của một UI trong tab Attributes Inspector chưa? Nếu bạn có ý định này thì xin chúc mừng. IBInspectable sẽ giúp bạn làm được việc này.

    IBInspectable giúp cho bạn có thể thêm được rất nhiều các thuộc tính vào tab Attributes Inspector từ đó giúp các bạn dễ dàng chỉnh sửa nó trên Interface builder của Xcode một cách dễ dàng.

    Vậy để sử dụng IBInspectable thêm các thuộc tính vào Interface builder của xCode chúng ta làm như sau:

    Ở đây mình sẽ làm một ví dụ để thêm thuộc tính cho UIView

    Như bạn đã biết thì UIView trên Interface builder không có các thuộc tính như cornerRadius(bo góc), borderColor(màu viền), borderWidth(độ rộng viền)… Vậy trong ví dụ này mình sẽ thêm các thuộc tính này vào Attributes inspector của UIView.

    Đầu tiên mình tạo một class CommonView kế thừa lại UIView như sau:

    class CommonView: UIView {
        // thêm thuộc tính để bo góc cho View
        @IBInspectable
        var cornerRadius: CGFloat = 4 {
            didSet {
                clipsToBounds = true
                layer.cornerRadius = cornerRadius
            }
        }
        
        // thêm thuộc tính để đặt độ dày của viền cho View
        @IBInspectable
        var borderWidth: CGFloat = 1 {
            didSet {
                layer.borderWidth = borderWidth
            }
        }
        // thêm thuộc tính để sửa màu viền cho View
        @IBInspectable
        var borderColor: UIColor = .red {
            
            didSet {
                layer.borderColor = borderColor.cgColor
            }
        }
    }

    Để sử dụng CommonView thì chúng ta mở file Storyboard hoặc file xib lên và kéo một UIView vào, sau đó đổi class từ UIView(mặc định) sang CommonView, vậy là xong.

    Kết quả chúng ta sẽ được như sau:

    Inspectable-and-IBDesignable
    Các thuộc tính Corner Radius, Border Witdh, Border color đã được thêm vào Attributes Inspector băng thuộc tính @IBInspectable

    Vậy là chúng ta đã thêm được các thuộc tính vào Attributes Inspector của Xcode, tuy nhiên chúng ta cần phải build app lên thì mới thấy sự thay đổi. Sao nó không thay đổi ngay khi chúng ta sửa giá trị như các thuộc tính khác? Vì một mình IBInspectable thì không làm được vì vậy các nhà phát triển của Apple mới đẻ ra IBDesignable để làm việc này.

    IBDesignable

    IBDesignable cho phép chúng ta xem trực tiếp các thay đổi của view trong storyboard hoặc trong file xib mà không cần phải run ứng dụng.

    Để sử dụng IBDesignable thì chúng ta chỉ cần thêm @IBDesignable vào đằng trước class mà chúng ta muốn và override lại func prepareForInterfaceBuilder() để nó update giá trị và hiển thị lên trên Interface builder, trong ví dụ này mình để nó ở trước class CommonView của mình như sau:

    @IBDesignable
    class CommonView: UIView {
        // set giá trị để hiển thị cho Interface builder
        override func prepareForInterfaceBuilder() {
            setupView()
        }
        // setup view
        private func setupView() {
            self.layer.cornerRadius = cornerRadius
            self.layer.borderWidth = borderWidth
            self.layer.borderColor = borderColor.cgColor
        }
    
        @IBInspectable
        var cornerRadius: CGFloat = 4 {
            didSet {
                clipsToBounds = true
                layer.cornerRadius = cornerRadius
            }
        }
        
        @IBInspectable
        var borderWidth: CGFloat = 1 {
            didSet {
                layer.borderWidth = borderWidth
            }
        }
        
        @IBInspectable
        var borderColor: UIColor = .red {
            
            didSet {
                layer.borderColor = borderColor.cgColor
            }
        }
    }

    CHÚ Ý: Bạn cần phải override lại func prepareForInterfaceBuilder() và set lại các thuộc tính để nó có thể update giá trị cho interface builder.

    Bây giờ chúng ta chỉ cần kéo UIView vào là nó sẽ tự apply các thuộc tính và khi sửa tại Attributes inspector thì nó sẽ được update ngay mà không cần phải build ứng dụng để kiểm tra lại UI.

    Kết quả chúng ta được như hình dưới đây:

    IBInspectable and IBDesignable uiview

    Trong trường hợp các bạn muốn làm common và không cho sửa thuộc tính nào trên interface builder thì bạn chỉ cần bỏ IBInspectable của thuộc tính đó đi là được.

    Tổng kết

    Vậy là mình đã giới thiệu cho các bạn một phương pháp để thực hiện làm common rất hiệu quả và tiết kiệm thời gian khi làm ứng dụng di động trên iOS. Từ ví dụ common view này chúng ta có thể phát triên cho các common khác như UILabel, UIButton … Mình hi vọng bài viết sẽ giúp ích cho các bạn trong quá trình học hỏi và phát triển ứng dụng iOS.

  • Dynamic Member Lookup in Swift

    Dynamic Member Lookup in Swift

    Chắc hẳn chúng ta đã từng phải viết những đoạn code dài dòng kiểu như thế này chỉ để lấy được 1 giá trị cuối cùng ở cuối 1 chuỗi chaining:

    apiManager.configuration.networkConfiguration.baseHeaders
    apiManager.configuration.networkConfiguration.baseURL

    Việc này có 1 vài hạn chế:

    • Quá dài chỉ để lấy ra 1 vài giá trị, chưa kể nó bị lặp lại ở nhiều nơi
    • Không set private cho configuration được vì như vậy consumer của API sẽ k lấy được giá trị của baseHeaders/baseURL.
    • Leak detail implementation cho consumer của API.

    Để giải quyết vấn đề này, ta có thể sử dụng Dynamic Member Lookup

    Table of contents

    • Giới thiệu
    • Dynamic Member Lookup với ExpressibleByStringLiteral
    • Dynamic Member Lookup với KeyPath
    • Kết luận

    Giới thiệu

    Apply this attribute to a class, structure, enumeration, or protocol to enable members to be looked up by name at runtime.

    By Apple

    Sử dụng Dynamic Member Lookup bằng cách:

    • Sử dụng @dynamicMemberLookup attribute cho class/struct/enum/protocol bạn muốn support
    • Khai báo subscript subscript(dynamicMember:). Kiểu dữ liệu của dynamicMember có thể là KeyPath, hoặc 1 kiểu dữ liệu conform ExpressibleByStringLiteral

    Dynamic Member Lookup với ExpressibleByStringLiteral

    1. Dòng @dynamicMemberLookup đánh dấu struct User để cho biết rằng nó hỗ trợ tính năng dynamicMemberLookup.
    2. Phương thức subscript được sử dụng để xử lý truy cập vào thành viên của đối tượng. Tham số dynamicMember đại diện cho tên thành viên được truy cập, và phương thức sẽ trả về một chuỗi đơn giản là "Tuan" để minh họa cho việc truy cập thành viên.
    3. user.name được truy cập, sử dụng cú pháp giống như truy cập vào một thuộc tính của đối tượng. Khi đó, phương thức subscript của struct User được gọi để xử lý việc truy cập thành viên và trả về chuỗi "Tuan".

    Chúng ta cũng có thể implement nhiều subscript(dynamicMember) khác nhau cho cùng 1 đối tượng support dynamicMemberLookup

    Tuy nhiên, đừng quá lạm dụng sử dụng subscript(dynamicMember:) với ExpressibleByStringLiteral. Nó có 1 vài nhược điểm:

    • Swift tất nhiên sẽ không gợi í cho consumer của class là phải gọi name/age để lấy thông tin. Chúng ta sẽ phải bảo với consumer là phải dùng dynamicMemberLookup để lấy thông tin => Bad API design.
    • Làm mất tính safety của Swift.

    Having to be aware of internal implementation details is usually a bad sign when it comes to API design

    Dynamic Member Lookup với KeyPath

    Sử dụng Dynamic Member Lookup với KeyPath là 1 sự lựa chọn tuyệt vời khi nó vẫn giữ được tính safety của Swift, đồng thời vẫn có recommend cho user.

    @dynamicMemberLookup
    struct APIManager {
        private var configuration: Configuration
    
        init(configuration: Configuration) {
            self.configuration = configuration
        }
    
        subscript<T>(dynamicMember keyPath: KeyPath<NetworkConfiguration, T>) -> T {
            return configuration.networkConfiguration[keyPath: keyPath]
        }
    
        subscript<T>(dynamicMember keyPath: WritableKeyPath<NetworkConfiguration, T>) -> T {
            get {
                return configuration.networkConfiguration[keyPath: keyPath]
            }
    
            set {
                configuration.networkConfiguration[keyPath: keyPath] = newValue
            }
        }
    }
    
    var apiManager = APIManager(
        configuration: Configuration(networkConfiguration: .init(
            baseURL: URL(string: "https://www.google.com.vn/")!,
            baseHeaders: ["key":"someValue"])
        )
    )
    
    apiManager.baseHeaders
    apiManager.baseHeaders["anotherKey"] = "anotherValue"
    apiManager.baseHeaders
    Bạn có thể thấy, Swift vẫn recommend sử dụng baseHeadersbaseURL cho consumer mà k leak detail implementation của API -> Good API design

    Kết luận

    • DynamicMemberLookup được sử dụng rộng rãi trong các thư viện phần mềm như Alamofire, Combine, SwiftUI và nhiều thư viện khác để giảm thiểu việc viết mã boilerplate và tăng tính linh hoạt của ứng dụng.
    • Với DynamicMemberLookup, bạn có thể viết mã Swift hiệu quả hơn và tránh việc phải sao chép và dán nhiều mã lặp đi lặp lại.
    • DynamicMemberLookup với ExpressibleByStringLiteral giải quyết bài toán khi app cần tương tác với WebView khá tốt, vì ta phải handle javascript code. Đối với các case còn lại, đừng quá lạm dụng DynamicMemberLookup :3

    Thanks for reading, guys ?

  • Value type và Reference type – ARC in Swift

    Value type và Reference type – ARC in Swift

    Value type và Reference type – ARC in Swift

    1. Value type và Reference type:

    Là một lập trình viên iOS chúng ta biết rằng, Apple khuyên chúng ta sử dụng struct và enum nhiều hơn. Lí do là chúng là kiểu Value type, dẫn tới việc bộ nhớ của chúng không ảnh hưởng tới nhau. An toàn hơn khi truy cập và sử dụng

    Vậy với Class, Closure có escaping thì sao, chúng là kiểu reference type -> Câu hỏi đặt là ra là sao để quản lý vùng nhớ của chúng, để tránh tạo instance lẫn lộn, gây ra leak memory. Câu trả lời của Apple đó là ARC

    Noted:
    Class sẽ được khởi tạo trong heap -> nên có thể sử dụng cho data có size lớn
    Struct sẽ được khởi trong stack -> nên không thể dùng cho data có size lớn, nó sẽ dẫn tới phình stack nhanh. Vậy nên chúng ta chỉ nên dùng cho những data có size nhỏ.


    Còn Heap với Stack là gì, hẹn các bạn ở một bài khác nhé.

    2. Vậy ARC là gì?

    Automatic Reference Counting: Dịch theo nghĩa đen tiếng Việt là tự động đếm số lượng reference. 

    Theo Apple định nghĩa về ARC: là cơ chế Swift sử dụng để theo dõi và quản lý bộ nhớ mà ứng dụng sử dụng. Từ đó giúp bạn phát triển ứng dụng mà không cần quan tâm tới việc quản lý bộ nhớ như một số ngôn ngữ khác, hay là Objective – C chưa implement ARC. ARC sẽ tự động frees up( hủy bỏ vùng nhớ của các strong reference = 0)

    ARC chỉ áp dụng cho instance của classes. Structures và Enumerations là kiểu value types, không phải kiểu references types và không được lưu trữ dưới dạng tham chiếu.

    3. ARC hoạt động như thế nào?

    Khi mà chúng ta tạo một instance của một class, ARC sẽ phân bổ một vùng nhớ của bộ nhớ để lưu trữ thông tin của instance đó

    Khi mà instance không được sử dụng trong một thời gian đủ dài, ARC sẽ tự động frees up memory được sử dụng bởi instance đó để giải phóng vùng nhớ để sự dụng vào các mục đích khác,

    Để xác định một biến không còn được sử dụng, ARC sẽ đếm số lượng strong reference của các object khác đang trỏ vào chính nó. Chỉ khi số lượng strong reference = 0 thì biến đó mới được giải phóng khỏi bộ nhớ.

    4. Strong reference


    Ở đây chúng ta có pen1, pen2, pen3 đều là kiểu strong reference chỉ về vùng nhớ của class Pen.

    Khi ta gán:

    pen1 = nil

    pen2 = nil

    => ARC sẽ đếm strong reference trong trường họp này sẽ là = 1. vì còn 1 intance được giữ lại bởi pen3. Nên trong trường hơp này ARC không giải phóng vùng nhớ cho Pen.

    5. Reference giữa các Class

    Khi chúng ta gán person và car = nil thì ta thấy person có car đang chỉ tới vùng nhớ của của Car kiểu strong reference, khi person = nil, thì strong reference này = 1 ( instance chưa bị hủy đi), tương tự với car, tồn tại instance trỏ tới vùng nhớ của Peson chưa được giải phóng:

    -> Trong trường hợp này strong reference = 2

    -> Tạo retain cycle, dẫn tới leak memory ( Khi bộ nhớ bị đầy dẫn tới app bị chậm, hoặc dừng lại, hoặc thoát ra đột ngột)

    Vậy để giải quyết bài toán trên Apple đã đưa ra hai kiểu định nghĩa reference đó là weak và unowned.

    6. Weak reference

    Khi chúng ta tạo một biến là kiểu weak reference, thì ARC sẽ hiểu reference counting của nó = 0 ( vì nó không phải là strong reference nên không làm tăng retain count. Từ bộ đếm counting không tăng lên không dẫn tới việc leak memory.

    Ở đây khi chúng ta set cong = nil, Car sẽ không còn reference nào chỉ đến. Từ đó vùng nhớ được giải phóng, và khi đó strong reference của Person trỏ đến Car cũng bị hủy.

    Set honda = nil, ở thời điểm này không còn strong nào trỏ đến -> instance đuợc giải phóng khỏi vùng nhớ. Không làm leak memory

    7. Unowned reference

    Tương tự như weak thì khi tạo một biến kiểu unowned, Counting Reference không tăng lên. ARC không giữ instance  và không gây ra leak memory.

    Note: “ Unowned không sử dụng cho kiểu dữ liệu optional, một instance A unowned reference ( trỏ ) đến một instance B mà instance B đó có vòng đời bằng hoặc lớn hơn instance A, nên khi bạn truy cập một biến unowned sau khi nó đã giải phóng khỏi vùng nhớ sẽ dẫn tới crash. Vì vậy khi dùng unowned cần cẩn thận!”

    Tương tự như weak khi chúng ta set cong = nil, Car sẽ không còn reference nào chỉ đến. Từ đó vùng nhớ được giải phóng, và khi đó strong reference của Person trỏ đến Car cũng bị hủy.

    set honda = nil, ở thời điểm này không còn strong nào trỏ đến -> instance đuợc giải phóng khỏi vùng nhớ. Không làm leak memory

    8. Strong Reference Cycles for Closures:

    Closure cũng giống như class là reference types.

    Trong một class, nếu một property là closure và trong closure đó lại dùng lại property / method của class nữa ( xài self.property ) thì sẽ xảy ra hiện tượng retain cycle như các ví dụ ở trên.

    Ta có ví dụ sau:

    9. Resolving Strong Reference Cycles for Closures

    Để giải quyết vấn đề retain cycle trong closure, swift đã cung cấp cho chúng ta một giải pháp đó là: closure capture list.

    Capture list sẽ quy định luật để lấy giá trị của property trong closure. Tức là lấy self.property / self.method như thế nào. 

    Ta dùng syntax sau ở phần body của closure:

    [ weak self ] in

    hoặc:

    [ unowned self ] in

    hoặc lấy nhiều property / method cũng được: 

    [ weak self, unowned self.property, … ] in

    carbon-8

    Thanks for reading!

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

  • Học lập trình với ngôn ngữ Swift – Bài 1: Chào mừng bạn đến với Swift

    Học lập trình với ngôn ngữ Swift – Bài 1: Chào mừng bạn đến với Swift

    Trước khi bắt đầu thực hiện những dòng code đầu tiên cho ứng dụng của bạn bằng ngôn ngữ lập trình Swift, chúng ta sẽ tìm hiểu qua về nó để biết rằng tại sao chúng ta lại chọn Swift để làm các ứng dụng, nó có những ưu điểm gì? lịch sử hình thành như nào? Xu hướng phát triển ra sao? Liệu nó có đáng để chúng ta tìm hiểu và học hay không?. Vậy các bạn cùng mình tiếp tục theo dõi bài viết này nhé.

    Định nghĩa về Swift

    Swift là một ngôn ngữ lập trình hướng đối tượng dành cho việc phát triển iOS và macOSwatchOStvOS và z/OS. được giới thiệu bởi Apple tại hội nghị WWDC 2014.Swift được mong đợi sẽ tồn tại song song cùng Objective-C, ngôn ngữ lập trình hiện tại dành cho các hệ điều hành của Apple. Swift được thiết kế để hoạt động với các framework Cocoa và Cocoa Touch của Apple và phần lớn mã Objective-C hiện có được viết cho các sản phẩm của Apple. Nó được biên dịch với trình biên dịch LLVM và đã được đưa vào Xcode kể từ phiên bản 6, phát hành năm 2014. Trên các nền tảng của Apple[12], nó sử dụng thư viện runtime Objective-C cho phép mã CObjective-CC++ và Swift cùng chạy trong một chương trình.[13]

    Apple dự định Swift hỗ trợ nhiều khái niệm cốt lõi liên quan đến Objective-C, đáng chú ý là thu hồi động, các ràng buộc phổ thông, lập trình mở rộng và các tính năng tương tự, nhưng theo cách “an toàn hơn”, giúp dễ dàng bắt lỗi phần mềm hơn; Swift có các tính năng giải quyết một số lỗi lập trình phổ biến như con trỏ rỗng cung cấp cú pháp đặc biệt để giúp tránh kim tự tháp diệt vong. Swift hỗ trợ khái niệm về khả năng mở rộng giao thức, một hệ thống mở rộng có thể được áp dụng cho các kiểu, cấu trúc và lớp, mà Apple khuyến khích như một sự thay đổi thực sự trong mô hình lập trình mà họ gọi là “lập trình hướng giao thức” (tương tự như đặc điểm).

    Swift được giới thiệu tại Worldwide Developers Conference (WWDC) 2014 của Apple.Nó đã trải qua quá trình nâng cấp lên phiên bản 1.2 trong năm 2014 và nâng cấp lớn hơn cho Swift 2 tại WWDC 2015. Ban đầu, ngôn ngữ độc quyền, phiên bản 2.2 được được chuyển sang phần mềm nguồn mở theo Giấy phép Apache 2.0 vào ngày 3 tháng 12 năm 2015, dành cho các nền tảng của Apple và Linux.[17][18]

    Thông qua phiên bản 3.0, cú pháp của Swift đã trải qua quá trình phát triển quan trọng, với nhóm nòng cốt làm cho sự ổn định nguồn trở thành trọng tâm trong các phiên bản sau.[19][20] Trong quý đầu tiên của năm 2018, Swift đã vượt qua Objective-C về mức độ phổ biến.[21]

    Swift 4.0, được phát hành vào năm 2017, đã giới thiệu một số thay đổi đối với một số lớp và cấu trúc tích hợp. Mã được viết bằng các phiên bản trước của Swift có thể được cập nhật bằng chức năng di chuyển được tích hợp trong Xcode

    Vào tháng 3 năm 2017, chưa đầy 3 năm sau khi chính thức ra mắt, Swift đã đứng đầu trong bảng xếp hạng TIOBE hàng tháng về các ngôn ngữ lập trình phổ biến nhất.[22] Một tài liệu 500 trang về Swift cũng được phát hành tại WWDC, miễn phí trên iBooks Store.

    Nguồn: WIKI, bạn có thể đọc thêm về lịch sử hình thành và phát triển và các thông tin khác ở WIKI

    Giới thiệu về Swift

    Apple định nghĩa về ngôn ngữ lập trình Swift là một cách tuyệt vời để viết phần mềm, cho dù đó là phần mềm trên điện thoại, máy tính, server hay bất kể thứ gì khác chạy code. Nó là ngôn ngữ lập trình an toàn, nhanh chóng và có tính tương tác, kết hợp với tư duy ngôn ngữ hiện đại tốt nhất với sự không ngoan từ văn hoá kỹ thuật rộng lớn của Apple và những đóng góp đa dạng từ cộng đồng mã nguồn mở. Trình biên dịch thì tối ưu hoá cho hiệu suất và ngôn ngữ được tối ưu hoá cho sự phát triển mà không ảnh hưởng tới cả hai.

    Swift rất thân thiện với các lập trình viên mới, nó là ngôn ngữ lập trình chất lượng công nghiệp, biểu cảm và thú vị như ngôn ngữ kịch bản. Viết code Swift trong Playground cho phép bạn thử nghiệm code và xem kết quả ngay lập tức mà không tốn chi phí để xây dụng ứng dụng.

    Swift xác định loại bỏ các lớp lớn các lỗi lập trình phổ biến bằng cách áp dụng các mẫu lập trình hiện đại:

    • Các biến luôn được khởi tạo trước khi sử dụng.
    • Các chỉ số mảng được kiểm tra lỗi ngoài giới hạn(out-of-bounds).
    • Số nguyên được kiểm tra tràn(overflow).
    • Optionals đảm bảo rằng các giá trị nil được xử lý rõ ràng.
    • Bộ nhớ được quản lý tự động.
    • Xử lý lỗi cho phép phục hồi có kiểm soát từ các lỗi không mong muốn.

    Mã Swift được biên dịch và tối ưu hóa để tận dụng tối đa phần cứng hiện đại. Cú pháp và thư viện chuẩn đã được thiết kế dựa trên nguyên tắc hướng dẫn rằng cách rõ ràng để viết mã của bạn cũng sẽ hoạt động tốt nhất. Sự kết hợp giữa an toàn và tốc độ khiến Swift trở thành lựa chọn tuyệt vời cho mọi thứ từ “Xin chào, thế giới!” cho toàn bộ hệ điều hành.

    Swift kết hợp suy luận kiểu mạnh mẽ và so khớp mẫu với cú pháp nhẹ, hiện đại, cho phép các ý tưởng phức tạp được thể hiện một cách rõ ràng và ngắn gọn. Kết quả là mã không chỉ dễ viết hơn mà còn dễ đọc và dễ bảo trì hơn.

    Swift đã được phát triển trong nhiều năm và nó tiếp tục phát triển với các tính năng và khả năng mới. Mục tiêu của Apple dành cho Swift rất tham vọng. Apple rất muốn xem bạn tạo thật nhiều ứng dụng với Swift.

    Khả năng tương thích phiên bản

    Phần này mô tả Swift 5.8, phiên bản mặc định của Swift được bao gồm trong Xcode 14. Bạn có thể sử dụng Xcode 14 để xây dựng các target được viết bằng Swift 5.8, Swift 4.2 hoặc Swift 4.

    Khi bạn sử dụng Xcode 14 để xây dựng code Swift 4 và Swift 4.2, hầu hết các chức năng của Swift 5.8 đều khả dụng. Điều đó nói rằng, những thay đổi sau chỉ có sẵn cho code sử dụng Swift 5.8 trở lên:

    Các hàm trả về loại không trong suốt yêu cầu thời gian chạy Swift 5.1.

    Sự cố gắng? biểu thức không đưa ra mức tùy chọn bổ sung cho các biểu thức đã trả về các tùy chọn.

    Biểu thức khởi tạo số nguyên lớn được suy ra là kiểu số nguyên chính xác. Ví dụ: UInt64(0xffff_ffff_ffff_ffff) đánh giá giá trị chính xác thay vì tràn.

    Đồng thời yêu cầu Swift 5.8 hoặc version cao hơn và một phiên bản của thư viện tiêu chuẩn Swift cung cấp các loại đồng thời tương ứng. Trên các nền tảng của Apple, hãy đặt target triển khai ít nhất là iOS 13, macOS 10.15, tvOS 13 hoặc watchOS 6.

    Targer được viết bằng Swift 5.8 có thể phụ thuộc vào Target được viết bằng Swift 4.2 hoặc Swift 4 và ngược lại. Điều này có nghĩa là nếu bạn có một dự án lớn được chia thành nhiều Framework, bạn có thể di chuyển code của mình từ Swift 4 sang Swift 5.8 bằng một Framework.

    Tổng kết

    Vậy là chúng ta đã đã biết qua hầu hết các thông tin về Swift, bài tiếp theo mình sẽ chia sẻ với các bạn cách làm quen với công cụ XCode và thực hiện những dòng code đầu tiên trên ngôn ngữ lập trình Swift.

  • iOS/Swift/Localization: Cách tách Localizable.strings thành nhiều file theo từng màn hình

    iOS/Swift/Localization: Cách tách Localizable.strings thành nhiều file theo từng màn hình

    Xin chào, trong số chúng ta hẳn không ít developer đã từng gặp tình trạng phải ngồi sửa các conflict file localizable strings khi làm việc, nhất là khi dự án của bạn có nhiều người tham gia lúc này file localizable.strings sẽ như là một đấu trường hỗn loạn với rất nhiều developer xâu xé, thi nhau chiếm đất. Người này sửa lên text người kia, người này sửa bug của mình lại tạo thành các bug của người khác. Khi dự án có thêm nhiều tính năng, file Localizable.strings sẽ càng ngày càng phình to ra, khi nó quá lớn thì một số máy MAC của anh em sẽ gặp tình trạng lag, chậm khi sửa file này.

    Câu hỏi đặt ra là để giải quyết bài toán này thì chúng ta cần phải làm gì?

    Chia để trị là cách mình giải quyết vấn đề này.


    Nếu bạn chưa làm ứng dụng có tính năng đa ngôn ngữ bao giờ thì quay lại đọc bài của mình trước rồi quay lại nhé!
    HƯỚNG DẪN THỰC HIỆN ỨNG DỤNG MOBILE HỖ TRỢ ĐA NGÔN NGỮ

    Ưu điểm của việc tách file Localizable.strings theo từng màn hình

    • Mỗi màn hình có một file .strings quản lý riêng vì vậy không bị conflict khi merge code
    • Không bị bug khi fix string màn hình A lại lỗi màn hình B, C, D …
    • Dễ dàng quản lý, dễ dàng maintain
    • Không bị vì lỗi format trên file Localizable.strings mà cả ứng dụng không chạy được đa ngôn ngữ

    Ý tưởng

    Theo cách kiến trúc lập trình phần mềm, chúng ta luôn tìm cách phân tách các thành phần riêng để xử lí các tác vụ, và giảm tải cho các phần càng ngày càng lớn lên khi ứng dụng có nhiều tính năng hơn. Vậy tại sao chúng ta lại để file Localization của mình đến hàng nghìn dòng đến vạn dòng. Không thể chịu được cảnh mở file localizable giật lag, cuộn mỏi tay. Mình đã suy nghĩ đến giải pháp chia ra để trị, mỗi màn hình sẽ có một file .strings riêng để thực hiện Localization.

    Cách thực hiện

    Từ rất lâu Apple đã cung cấp cho chúng ta cách thức để làm được việc này.

    Nếu bạn muốn thực hành luôn thì có thể tài project bắt đầu ở đây nhé.

    Bước 1: Tạo 2 màn hình tương ứng với 2 tính năng của ứng dụng.

    Bước 2: Tạo 2 file .strings tương ứng với 2 màn hình là FirstScreen.strings và SecondScreen.strings như hình dưới, bạn copy nội dung từ File Localizable.strings rồi xoá file đi như hình.

    Bước 3: Sửa lại extension String file LanguageManager.swift như sau

    extension String {
        var localized: String {
            // default language of device
            let currentLanguage = LanguageManager.shared.getAppLanguage().rawValue
            guard let bundlePath = Bundle.main.path(forResource: currentLanguage, ofType: "lproj"),
                  let bundle = Bundle(path: bundlePath),
                  let tableName = self.split(separator: ".").first// lấy table name từ string
            else {
                return self
            }
            
            return NSLocalizedString(self, tableName: String(tableName), bundle: bundle, value: "", comment: "")
        }
    }

    Để sử dụng chúng ta code như sau:

    lbHeaderTitle.text = Localization.SecondScreen.title.localized

    NOTE: Để hàm này hoạt động đúng thì tất cả thành viên trong dự án sẽ phải tuân thủ cách đặt tên nằm trong .strings.
    Ví Dụ: Feature1.strings, thì nội dung bên trong phải là Feature1.xxxx, xxxx ở đây là key string của bạn.

    Nếu bạn cảm thấy việc tuân thủ luật này quá cứng nhắc dễ mắc sai lầm thì chúng ta có một hàm mới như sau:

    // Định nghĩa các table Name, lưu ý đặt tên phải trùng với tên file .strings
    enum LocalizationTableName: String {
        case firstScreen = "FirstScreen"//FirstScreen.strings
        case secondScreen = "SecondScreen"//SecondScreen.strings
    }
    
    extension String {
        func localized(tableName: LocalizationTableName) -> String {
            // default language of device
            let currentLanguage = LanguageManager.shared.getAppLanguage().rawValue
            guard let bundlePath = Bundle.main.path(forResource: currentLanguage, ofType: "lproj"),
                  let bundle = Bundle(path: bundlePath)
            else {
                return self
            }
            
            return NSLocalizedString(self, tableName: tableName.rawValue, bundle: bundle, value: "", comment: "")
        }
    }

    Khi sử dụng chúng ta sẽ code như sau:

    lbHeaderTitle.text = Localization.SecondScreen.title.localized(tableName: .secondScreen)

    Vậy là chúng ta đã thành công tạo localization cho từng màn hình. Mình hi vọng bài viết sẽ giúp ích cho các bạn ở các dự án sau này.