Category: iOS

  • Ứng dụng đa ngôn ngữ cho APNs

    Ứng dụng đa ngôn ngữ cho APNs

    Để làm đa ngôn ngữ cho APNs rất đơn giản, đầu tiên chúng ta cần những thứ sau đây:
    1. File Localizable.strings define các localization
    2. Đăng ký Push Notification cho ứng dụng
    Ở đây mình mặc định hai công việc trên đã được hoàn thành, và tập trung vào việc xử lý đa ngôn ngữ cho APNs.

    Thông thường, một Notification đơn giản sẽ có nội dung như sau:

    // Notification payload
    {
        "aps":{
           "alert":{
              "title":"Đây là tiêu đề thông báo",
              "body":"Còn đây chắc là nội dung thông báo"
           }
        }
    }
    Nội dung notification hiển thị trên device

    Như ví dụ trên, giá trị tương ứng với key titlebody sẽ được hiển thị trực tiếp vào tiêu đề và nội dung của notification như ảnh. Để áp dụng đa ngôn ngữ, ta sẽ thay thế titlebody bằng hai key khác là title-loc-keyloc-key, tiếp đó ta truyền giá trị của hai trường trên bằng key của localization đã define trong file Localizable.strings:

    // Notification payload
    {
        "aps":{
           "alert":{
              "title-loc-key":"notification_title",
              "loc-key":"notification_content"
           }
        }
    }
    // Localizable.strings content
     "notification_title" = "Thông báo ";
     "notification_content" = "Nội dung thông báo"; 
    Nội dung notification hiển thị trên device

    Như vậy là ta xong với các Notification có nội dung cố định. Đối với các Notification có sử dụng thêm tham số ví dụ như “Bạn có 10 thông báo mới” hay “NhatHM đã ném tiền vào mặt bạn”, chúng ta sử dụng thêm hai key title-loc-argsloc-args, và setting như sau:

    // Notification payload
    {
        "aps":{
           "alert":{
              "title-loc-key":"notification_title_args",
              "title-loc-args":["10"],
              "loc-key":"notification_content_agrs",
              "loc-args":["tinh thần"]
           }
        }
     }
    // Localizable.strings content
    "notification_title_args" = "Bạn có thêm %@ thông báo mới"; "notification_content_agrs" = "Nội dung thông báo với giá trị %@";
    Nội dung notification hiển thị trên device

    Để đề phòng trường hợp ứng dụng chưa định nghĩa các key trong file Localizable.strings, ta chỉ cần cung cấp thêm hai key titlebody và truyền vào giá trị mặc định cho Notification. Hai giá trị này sẽ được hiển thị khi ứng dụng client không tìm thấy strings được trả về trong Notification payload. Nên để đẹp trai thì Notification sẽ như thế này:

    // Notification payload
    {
        "aps":{
           "alert":{
              "title":"Đây là tiêu đề thông báo",
              "body":"Còn đây chắc là nội dung thông báo",
              "title-loc-key":"notification_title_args",
              "title-loc-args":["10"],
              "loc-key":"notification_content_agrs",
              "loc-args":["tinh thần"]
           }
        }
     }

    Như vậy là đủ để quẩy đa ngôn ngữ cho Notification rồi đấy 😀

  • CoreData with MultiThreading

    CoreData with MultiThreading

    Nội dung

    • Tóm tắt CoreData
    • Sử dụng CoreData với MultiThreading
    • Debug CoreData MultiThreading
    • Kết luận

    Tóm tắt về CoreData:

    Các thành phần chính của CoreData:

    • Managed Object Model
    • Managed Object Context
    • Persistent store coordinator

    MultiThreading with CoreData:

    Khi khởi tạo managedObjectContext(MOC) thì sẽ có thể lựa chọn 1 trong 2 loại queue để khởi tạo MOC, đó là:

    • NSMainQueueConcurrencyType (main Thread)
    • NSPrivateQueueConcurrencyType (background Thread)

    NSMainQueueConcurrencyType chỉ có thể được sử dụng trên main queue.
    NSPrivateQueueConcurrencyType tạo ra 1 queue riêng để sử dụng. Vì queue này là private, nên chỉ có thể access queue thông qua hàm perform(_:)performAndWait(_:) của MOC .

    Nếu ứng dụng sử dụng nhiều thao tác data processing (parse JSON to data, …) thì việc sử dụng trên main queue sẽ gây ra block main. Khi đó, có thể khởi tạo 1 context dùng private queue và thực hiện xử lí data trên đó.

    Trước khi sử dụng CoreData với MultiThread, chú í đến điều Apple recommend:

    Hãy chắc chắn rằng MOC được sử dụng trên thread(queue) mà chúng được liên kết khi khởi tạo.

    Nếu MOC không được sử dụng trên thread(queue) mà chúng được liên kết, trong trường hợp MOC liên kết với mainQueue nhưng được sử dụng trên background thread, hoặc ngược lại, sẽ khiến app đôi lúc sẽ gặp những lỗi crash lạ.

    Vì vậy để chắc chắn MOC luôn được sử dụng trên thread mà MOC được liên kết, thì có thể sử dụng perform( _:) và performAndWait( _:) như sau:

    • perform( _:) và performAndWait( _:) sẽ tự động đưa đoạn code bên trong nó thực hiện trên queue mà context đó được khởi tạo -> Điều đó sẽ chắc chắn rằng context được sử dụng trên đúng queue.
    • perform(_:) sẽ thực hiện async hàm bên trong nó.
    • performAndWait(_:) sẽ thực hiện sync hàm bên trong nó -> Nó sẽ block thread hiện tại gọi đến hàm đó cho đến khi hàm bên trong thực hiện xong -> Không nên gọi trên main.

    Debug Concurrency:

    Để đảm bảo Context được chạy trên đúng luồng nó được liên kết khi khởi tạo, có thể bật debug CoreData Concurrency như sau:

    • Chọn Edit Scheme -> Run -> Thêm "-com.apple.CoreData.ConcurrencyDebug 1".
    • Khi bật debug này lên, nó sẽ dừng app của bạn lại tại nơi context bị dùng sai Thread.

    Ví dụ:

    • Khởi tạo 1 context bằng private queue:
    private(set) lazy var managedObjectContext: NSManagedObjectContext = {
       let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
       managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator
       return managedObjectContext
    }()
    • Sử dụng 1 context trên main:
    func createNewEntity() {
       DispatchQueue.main.async {
           let user = User(context: self.manager.managedObjectContext)
           user.name = "Hoang Anh Tuan"
                
           let account = Account(context: self.manager.managedObjectContext)
           account.username = "sunlight"
           account.password = "123"
                
           user.account = account
           account.user = user
                
           do {
               try self.manager.managedObjectContext.save()
           } catch let error {
               print("Save error: \(error.localizedDescription)")
           }
        }
    }
    • Kết quả khi bật debug:
    Có thể sử dụng perform( _:) / performAndWait( _:) để giải quyết tình huống này.

    Kết luận:

    • Nên dùng background thread cho context để tránh block main thread.
    • Nên dùng các hàm perform( _:) và performAndWait( _:) để đảm bảo context được chạy trên đúng luồng.
    • Ngoài cách ở trên thì còn 1 vài cách như sử dụng child/parent context, nhưng sẽ không được đề cập ở bài viết này.
  • Hướng dẫn tạo plugin cho dự án Cordova/Ionic

    Hướng dẫn tạo plugin cho dự án Cordova/Ionic

    Table of contents

    • Tại sao cần tạo plugin cho Cordova
    • Tạo plugin bằng plugman
    • Hoàn thiện plugin

    Tại sao cần tạo plugin cho Cordova

    Về cơ bản, thì Cordova là framework phát triển các app iOS/Android (là chính) sử dụng html/js/css làm UI, và các bộ plugin làm cầu nối để call xuống source native của platform (iOS/Android)

    Cordova bao gồm:

    • Bộ html/js/css làm UI.
    • Native webview engine làm bộ render hiển thị UI
    • Cordova framework chịu trách nhiệm cầu nối giữa function call js và funtion native.
    • Source code native làm plugin cùng các config và các pulic js method.
    Cordova project struct

    Bình thường đối với người làm Cordova thì chủ yếu họ sẽ focus vào tầng UI bằng html/js/css. Việc sử dụng các chức năng native của platform thì sẽ sử dụng các plugin được cung cấp sẵn. Vậy nên về cơ bản, một lập trình viên làm Cordova chỉ cần làm được html/js/css là đủ.

    Tuy nhiên trong một số trường hợp, các plugin có sẵn không đảm bảo giải quyết được vấn đề bài toán, lúc này việc phát triển riêng một plugin thực hiện được logic của project và support được các platform là điều cần phải làm.

    Trong một vài trường hợp khác, có thể dự án đã có sẵn source native, tuy nhiên cần chuyển sang Cordova để support multi platform và tận dụng source code native có sẵn.

    -> Đo đó, hiểu biết về cách tạo một plugin để giải quyết nhu câu bài toán sẽ nảy sinh. Bài viết này sẽ tập trung vào việc

    • Làm thế nào để tạo plugin
    • Luồng xử lý từ js xuống native source của plugin như nào
    • Install plugin vào Cordova project
    • Build và test plugin trên iOS và Androd

    Tạo plugin bằng plugman

    Tạo Cordova project

    Để tạo Cordova plugin sample thì trước tiên cần có một Cordova project để test việc add plugin và kiểm tra hoạt động của plugin trên từng platform.

    • Install Cordova CLI: sudo npm install -g cordova
    • Create Cordova project: cordova create SamplePlugin com.nhathm.samplePlugin SamplePlugin

    Install plugman và tạo plugin template

    plugman là command line tool để tạo Apache Cordova plugin. Install bằng command: npm install -g plugman

    Create plugin:

    • Command: plugman create –name pluginName –plugin_id pluginID –plugin_version version
    • Ví dụ: plugman create –name GSTPlugin –plugin_id cordova-plugin-gstplugin –plugin_version 0.0.1

    Thêm platform mà plugin sẽ hỗ trợ:

    • plugman platform add –platform_name android
    • plugman platform add –platform_name ios

    Sau khi cài đặt xong thì thư mục plugin sẽ có struct như dưới.

    .
    └── GSTPlugin
    ├── plugin.xml
    ├── src
    │ ├── android
    │ │ └── GSTPlugin.java
    │ └── ios
    │ └── GSTPlugin.m
    └── www
    └── GSTPlugin.js

    Ở đây, plugin.xml là file config cho plugin, bao gồm các thông tin như tên của plugin, các file assets, resources. Define js-module như file js của plugin, define namespace của plugin, define các plugin phụ thuộc của plugin đang phát triển…

    Hoàn thiện plugin

    Cùng view file plugin.xml của plugin mới tạo:

    <?xml version='1.0' encoding='utf-8'?>
    <plugin id="cordova-plugin-gstplugin" version="0.0.1"
    xmlns="http://apache.org/cordova/ns/plugins/1.0"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <name>GSTPlugin</name>
    <js-module name="GSTPlugin" src="www/GSTPlugin.js">
    <clobbers target="cordova.plugins.GSTPlugin" />
    </js-module>
    <platform name="android">
    <config-file parent="/*" target="res/xml/config.xml">
    <feature name="GSTPlugin">
    <param name="android-package" value="cordova-plugin-gstplugin.GSTPlugin" />
    </feature>
    </config-file>
    <config-file parent="/*" target="AndroidManifest.xml" />
    <source-file src="src/android/GSTPlugin.java" target-dir="src/cordova-plugin-gstplugin/GSTPlugin" />
    </platform>
    <platform name="ios">
    <config-file parent="/*" target="config.xml">
    <feature name="GSTPlugin">
    <param name="ios-package" value="GSTPlugin" />
    </feature>
    </config-file>
    <source-file src="src/ios/GSTPlugin.m" />
    </platform>
    </plugin>

    Trong file này có một vài điểm cần hiểu như dưới:

    • <clobbers target="cordova.plugins.GSTPlugin" /> đây là namespace phần js của plugin. Từ file js, call xuống method native của plugin thì sẽ sử dụng cordova.plugins.GSTPlugin.sampleMethod
    • <param name="android-package" value="cordova-plugin-gstplugin.GSTPlugin" /> đây là config package name của Android, cần đổi sang tên đúng => <param name="android-package" value="com.gst.gstplugin.GSTPlugin" />. Như vậy Cordova sẽ tạo ra file GSTPlugin.java trong thư mục com/gst/gstplugin.

    Trong sample này, chúng ta sẽ sử dụng Swift làm ngôn ngữ code Native logic cho iOS platform chứ không dùng Objective-C, do đó phần platform iOS cần update.

    • Trong thẻ <platform name="ios> thêm tag <dependency id="cordova-plugin-add-swift-support" version="2.0.2"/>. Đây là plugin support việc import các file source Swift vào source Objective-C. Mà bản chất Cordova sẽ generate ra source Objective-C cho platform iOS.
    • Vì sử dụng Swift nên ta thay thế <source-file src="src/ios/GSTPlugin.m" /> bằng <source-file src="src/ios/GSTPlugin.swift" />. Và đổi tên file GSTPlugin.m sang GSTPlugin.swift

    Tiếp theo, chỉnh sửa các file js và native tương ứng cho plugin.

    File GSTPlugin.js

    • File này export các public method của plugin. Update file như dưới
    var exec = require('cordova/exec');
    
    exports.helloNative = function (arg0, success, error) {
        exec(success, error, 'GSTPlugin', 'helloNative', [arg0]);
    };
    • File này sẽ export method helloNative ra js và call method helloNative của native platform tương ứng.

    File GSTPlugin.swift

    • File này chứa logic và implementation cho iOS platform. Chỉnh sửa file như dưới
    @objc(GSTPlugin) class GSTPlugin : CDVPlugin {
        @objc(helloNative:)
        func helloNative(command: CDVInvokedUrlCommand) {
            // If plugin result nil, then we should let app crash
            var pluginResult: CDVPluginResult!
    
            if let message = command.arguments.first as? String {
                let returnMessage = "GSTPlugin hello \(message) from iOS"
                pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: returnMessage)
            } else {
                pluginResult = CDVPluginResult (status: CDVCommandStatus_ERROR, messageAs: "Expected one non-empty string argument.")
            }
    
            commandDelegate.send(pluginResult, callbackId: command.callbackId)
        }
    }

    File GSTPlugin.java

    package com.gst.gstplugin;
    
    import org.apache.cordova.CordovaPlugin;
    import org.apache.cordova.CallbackContext;
    
    import org.json.JSONArray;
    import org.json.JSONException;
    import org.json.JSONObject;
    
    import android.util.Log;
    
    public class GSTPlugin extends CordovaPlugin {
    
        @Override
        public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
            if (action.equals("helloNative")) {
                String message = args.getString(0);
                this.helloNative(message, callbackContext);
                return true;
            }
            return false;
        }
    
        private void helloNative(String message, CallbackContext callbackContext) {
            if (message != null && message.length() > 0) {
                callbackContext.success("GSTPlugin hello " + message + " from Android");
            } else {
                callbackContext.error("Expected one non-empty string argument.");
            }
        }
    }

    Tại thư mục của plugin, chạy command plugman createpackagejson . và điền các câu trả lời, phần nào không có thì enter để bỏ qua

    Switch sang thư mục chứa project Cordova SamplePlugin đã tạo, run command cordova plugin add --path-to-plugin.

    -> Ví dụ cordova plugin add /Users/nhathm/Desktop/Cordova-plugin_sample/GSTPlugin/GSTPlugin

    Bây giờ, plugin đã được add vào project Cordova. Tiếp theo sẽ chỉnh sửa source của index.html và index.js để test hoạt động của project.

    File index.html

    <div class="app">
          <h1>Apache Cordova</h1>
          <div id="deviceready" class="blink">
              <p class="event listening">Connecting to Device</p>
              <p class="event received">Device is Ready</p>
          </div>
    
          <button id="testPlugin">Test Plugin</button><br/>
    </div>

    File index.js

    • Add vào onDeviceReady()
    document.getElementById("testPlugin").addEventListener("click", gstPlugin_test);

    Thêm function:

    function gstPlugin_test() {
        cordova.plugins.GSTPlugin.helloNative("NhatHM",
            function (result) {
                alert(result);
            },
            function (error) {
                alert("Error " + error);
            }
        )
    }

    Sau khi chỉnh sửa hoàn chỉnh, build source cho platform iOS và Android

    • cordova platform add ios
    • cordova platform add android

    Build project native đã được generate ra và kiểm tra kết quả

    cordova plugin sample

    Đối với việc tạo UI cho project Cordova, chúng ta nên dùng các framework support như Ionic: https://ionicframework.com/

    Note: các dự án hybrid rất hạn chế về mặt performance, do đó nên cân nhắc khi bắt đầu dự án mới bằng hybrid

  • Working with EAAcessory & NSStream

    Working with EAAcessory & NSStream

    Nội dung:

    • EAAcessory là gì?
    • Cấu hình project để làm việc với EAAcessory.
    • Lấy thông tin accessory
    • Tạo stream để gửi & nhận data
    • Gửi data đến accessory
    • Nhận data từ accessory

    EAAcessory là gì?

    • 1 đối tượng kiểu EAAcessory đại diện cho 1 thiết bị ngoại vi đang kết nối với app thông qua Lightning connector hoặc Bluetooth.
    • EAAcessory gồm các thuộc tính chứa các thông tin quan trọng về thiết bị ngoại vi: isConnected, name, manufacturer, serialNumber, protocols mà thiết bị ngoại vi dùng, firmware version, …
    • Ngoài ra, cũng có thể lấy macAddress của thiết bị ngoại vi:
     let macAddress = accessory.value(forKey: PrinterConstant.MACAddress) as? String
    • Từ những thông tin đó, bạn có thể mở 1 session tới thiết bị ngoại vi để trao đổi dữ liệu.

    Config Project:

    • Không phải cứ kết nối Bluetooth là app sẽ scan thấy hoặc lấy được thông tin của thiết bị ngoại vi đó.
    • Để app có thể giao tiếp với thiết bị ngoại vi, thì app cần khai báo các protocols mà thiết bị ngoại vi đó hỗ trợ vào Info.plist.

    Đọc thông tin accessory:

    • Sau khi config project và kết nốt với thiết bị ngoại vi thông qua Bluetooth, có thể bắt đầu đọc thông tin của thiết bị ngọai vi:

    Đầu tiên, import ExternalAccessory:

    import ExternalAccessory
    • Đọc thông tin:
    let listAccessoryAvailable = EAAccessoryManager.shared().connectedAccessories
    accessory = listAccessoryAvailable.first
    print(accessory?.name)
    print(accessory?.serialNumber)
    print(accessory?.protocolStrings)
    ...

    Tạo socket để gửi/nhận data:

    • Để gửi/nhận data đến thiết bị ngoại vi thì cần mở 1 socket đến nó.
    func openSession() {
        guard let accessory = self.accessory else {
            return
        }
            
        guard let protocolString = accessory.protocolStrings.first else {
            return
        }
            
        session = EASession(accessory: accessory, forProtocol: protocolString)
        if session != nil {
            session?.inputStream?.delegate = self
            session?.inputStream?.open()
            session?.inputStream?.schedule(in: .current, forMode: .default)
                
            session?.outputStream?.delegate = self
            session?.outputStream?.open()
            session?.outputStream?.schedule(in: .current, forMode: .default)
        }
    }

    conform to StreamDelegate để handle event của socket:

    extension ViewController: StreamDelegate {
        func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
            switch eventCode {
            case .openCompleted:
                print("Open session complete")
            case .hasBytesAvailable:
                print("Has bytes available")
            case .hasSpaceAvailable:
                print("Has space available")
            case .errorOccurred:
                let error = aStream.streamError
                print("Error occur")
            case .endEncountered:
                print("End stream")
                aStream.close()
                aStream.remove(from: .current, forMode: .default)
            default:
                return
            }
        }
    }

    Note:

    • Gửi data đến thiết bị ngoại vi thông qua outputStream, nhận data từ thiết bị ngoại vi thông qua inputStream.

    Gửi data:

    • Sau khi đã open được session, bắt đầu gửi data sang thiết bị ngoại vi thông qua outputStream:
    guard let outputStream = session?.outputStream else {
        return
    }
    
    outputStream.write(data, maxLength: 128)

    There is no firm guideline on how many bytes to write at one time. Although it may be possible to write all the data to the stream in one event, this depends on external factors, such as the behavior of the kernel and device and socket characteristics. The best approach is to use some reasonable buffer size, such as 512 bytes, one kilobyte, or a page size (four kilobytes) – Apple

    Nhận data:

    • Khi thiết bị ngoại vi gửi dữ liệu đến app của bạn, StreamDelegate sẽ trigger event hasSpaceAvailable, khi đó app sẽ đọc data thông qua inputStream:
    func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
        switch eventCode {
    ...
            case .hasBytesAvailable:
                print("Has bytes available")
                
                let dataBuffer = [UInt8](repeating: 0, count: 128)
                guard let inputStream = session?.inputStream else {
                    return
                }
                while inputStream.hasBytesAvailable {
                    inputStream.read(UnsafeMutablePointer<UInt8>(mutating: dataBuffer), maxLength: 128)
                    print("Read Data: \(dataBuffer)")
    ...
         }
    }

    Xử lí notification khi connect/disconnect với thiết bị ngoại vi:

    • Để nhận được notification, app cần phải đăng kí nhận thông báo trước:
    EAAccessoryManager.shared().registerForLocalNotifications()
    • Tiếp theo là add Observer khi 1 accessory connect/disconnect.
    NotificationCenter.default.addObserver(self,
                                           selector: #selector(self.accessoryDidConnect(notification:)),
                                           name: NSNotification.Name.EAAccessoryDidConnect,
                                            object: nil)
    NotificationCenter.default.addObserver(self,
                                           selector: #selector(self.accessoryDidDisconnect(notification:)),
                                           name: NSNotification.Name.EAAccessoryDidDisconnect,
                                           object: nil)
    • Lấy thông tin của thiết bị ngoại vi thay đổi trạng thái:
    guard let connectedAcessory = notification.userInfo?[EAAccessoryKey] as? EAAccessory else {
        return
    }

    Scan Accessory Available:

    • App có thể scan các accessory available ở gần và kết nối Bluetooth đến chúng từ trong app mà không cần phải mở System.
    • Đầu tiên, show view scan Accessory Available:
    EAAccessoryManager.shared().showBluetoothAccessoryPicker(withNameFilter: nil) { (error) in
        
    }
    • Bạn có thể tạo predicate để filter các accessory available.
    Nguồn: internet
    • Chọn 1 accessory để thực hiện kết nối, hàm sẽ trả về error nếu xảy ra lỗi; nếu không có lỗi tức là bạn đã connect thành công.
  • Debug Conflict Constraint iOS

    Debug Conflict Constraint iOS

    Có thể bạn đã từng gặp lỗi constraint bị breaking với kiểu log như thế này:

    Thường thì Xcode sẽ tự loại bỏ 1 constraint để view không bị conflict nữa. Điều này sẽ dẫn đến UI hiển thị trên màn hình đúng hoặc không, tùy thuộc vào việc Xcode loại bỏ constraint nào.

    Tuy nhiên, kể cả trong trường hợp UI hiển thị đúng, thì chúng ta vẫn nên đi fix cái lỗi này.

    Đối với những màn hình phức tạp, nhiều subview, việc biết view nào đang bị lỗi constraint là khó có thể phán đoán được.

    Vì vậy ở bài viết này, hãy cùng nhau đi đọc đống log kia để biết view nào đang bị conflict constraint 🙂

    Tìm xem view nào bị lỗi bằng cách đọc địa chỉ

    Để í đến dòng log sau:

    Đoạn log này hướng dẫn rằng code sẽ recover bằng cách loại bỏ constraint 0x60000336d7c0 của view có địa chỉ 0x7fccafe06a10.
    Giờ thì bắt đầu đi tìm view có địa chỉ 0x7fccafe06a10:

    • Chọn Show Debug Navigator (Command + 7).
    • Chọn View Memory Graph Hierachy
    • Nhập địa chỉ của View cần tìm vào phần Filter.
    • Chọn vào View có địa chỉ cần tìm, Xcode sẽ hiển thị kết quả như sau:
    • Click chuột phải vào View, chọn Quick Look để xem đó là View nào:
    • Với Quick Look, Xcode sẽ hiển thị View đang bị conflict constraint:

    Đổi màu background của View bằng lldb:

    Nếu trong trường hợp có quá nhiều View giống nhau, và bạn vẫn chưa thể xác định đó là view nào?
    -> Dùng lldb để đổi màu background view đó để có thể xác định dễ hơn.

    Đầu tiên, pause chương trình lại và thực hiện lệnh sau ở cửa sổ lldb:

    expression [(UIView*)0x7fccafe06a10 setBackgroundColor:[UIColor redColor]]
    • Với lệnh này, chọn Quick Look với View vừa rồi thì View đã chuyển sang màu đỏ rồi, nhưng vẫn chưa hiển thị trên màn hình simulator/device.
    • Để View đổi màu ngay lập tức trên simulator/device, thực hiện tiếp 1 câu lệnh sau:
    expression (void)[CATransaction flush]

    OK, khi đó thì View bị lỗi constraint đã ngay lập tức đổi màu rồi.

    Xem tất cả constraint của View:

    • Mở Debug View Hỉeachy.
    • Tìm và chọn view bị lỗi constraint.
    • Chọn Show the size inspector (Option + Command + 5)
    • Khi đó, ở mục Constraint thì sẽ hiển thị tất cả constraint đang gắn với view đó. Cuối cùng thì chỉ cần bỏ đi constraint nào không cần thiết là được.

    Vừa leading, vừa centerX, vừa set width gây ra bị conflic constraint. Bỏ width constraint hoặc leading constraint tùy vào mục đích.

    Tham khảo: https://medium.com/ios-os-x-development/dynamically-modify-ui-via-lldb-expression-1b354254e1dd

  • Bluetooth, Beacon & iOS CoreBluetooth

    Bluetooth, Beacon & iOS CoreBluetooth

    Bluetooth

    Bluetooth, một công nghệ không dây năng lượng thấp, tầm ngắn ra đời từ những ngày đầu của điện thoại di động, được sử dụng rộng rãi và có mặt trong hàng tỉ thiết bị và vật dụng hàng ngày. Bluetooth tạo ra một thế giới không cần dùng dây và là hình thức đầu tiên kết nối các thiết bị khác nhau, sản sinh ra những tính năng và hành vi tiêu dùng mới.

    Bluetooth low energy (BLE)

    Bluetooth Low Energy là chuẩn kết nối không dây hướng tới các ứng dụng tiết kiệm năng lượng (giải thích: việc truyền không dây thường tốn nhiều năng lượng, do đó, người ta cố gắng nghiên cứu những công nghệ, kỹ thuật sao cho việc truyền nhận dữ liệu không dây ít tiêu tốn năng lượng nhất có thể và thế là BLE ra đời).​

    Bluetooth version

    Trong quá trình phát triển của Bluetooth, Bluetooth SIG đã công bố nhiều version và vẫn đang tiếp tục phát triển các version mới

    • Bluetooth 1.x: Phiên bản Bluetooth đầu tiên, gần như không còn được sử dụng. Có tốc độ lý thuyết là 1Mbps – tốc độ này được giọi là Basic rate (BR).
      Capability : BR​
    • Bluetooth 2.x: Phiên bản nâng cấp của version 1.x cho tốc độ truyền tải lý thuyết cao hơn – 3 Mbps – tốc độ này được gọi là Enhanced data rate (EDR). Lưu ý: EDR là optional, thiết bị hỗ trợ Bluetooth 2.x vẫn có thể kết nối với tốc độ BR.
      Capability: BR + EDR​
    • Bluetooth 3.x: Hỗ trợ việc thay đổi lower layer. Các ứng dụng có lower layer thiết kế theo chuẩn của Bluetooth có thể chuyển qua sử dụng của chuẩn của giao thức 802.1 với tốc độ truyền tải dữ liệu cao hơn. Bluetooth 3.x có thể đạt tốc độ truyền tải lý thuyết lên đến 23Mbps – tốc độ này gọi là High speed (HS) và là một option của thiết bị hỗ trợ Bluetooth 3.x.
      Capability: BR + EDR + HS​
    • Bluetooth 4.x: Bổ sung thêm chuẩn giao tiếp Bluetooth Low Energy. Với ưu điểm là thu thập dữ liệu từ các thiết bị có tần số gửi data thấp, mỗi lần gửi một lượng data nhỏ (ví dụ: heart rate sensor, temperature sensor, humid sensor, …).
      Capability: BR + EDR + HS + LE​
    • Bluetooth 5.x: Go Faster. Go Further. Highlight Improvement: Range, Speed, bandwidth. Khoảng cách truyền dữ liệu lên đến 120m so với 30m của version 4.x. Tốc độ truyền tối đa của Bluetooth 5 low energy theo lý thuyết là 2Mbps, gấp đôi Bluetooth Low Energy 4.2 mà không làm tăng power consumption. Capability trong trường hợp này là khả năng hỗ trợ của version Bluetooth. Ví dụ như thiết bị mình mua về, nhà sản xuất kêu nó hỗ trợ Bluetooth 2.0 thì bạn không thể nào mong nó có khả năng kết nối với thiết bị hỗ trợ Bluetooth 3.0 với tốc độ HS. Tuy nhiên hai thiết bị này vẫn có thể kết nối với nhau với tốc độ EDR Note: Refer wiki dưới đây biết version bluetooth mà iOS device đang sử dụng:
      https://en.wikipedia.org/wiki/List_of_iOS_devices

    Các kiểu thiết bị Bluetooth

    • Thiết bị chỉ hỗ trợ Bluetooth Smart (Bluetooth Low Energy) không thể kết nối trực tiếp tới thiết bị chỉ hỗ trợ Bluetooth Classic (Bluetooth ít tiết kiệm năng lương)

    • Thiết bị hỗ trợ Bluetooth Smart Ready có thể kết nối trực tiếp với cả thiết bị hỗ trợ Bluetooth Low Energy và Bluetooth Classic. Ví dụ như iphone sẽ là 1 thiết bị Bluetooth smart ready và nó có thể kết nối với tai nghe không dây thông qua chuẩn Bluetooth Classic và cũng có thể kết nối với smart watch thông qua chuẩn Bluetooth Low Energy.

    Beacon

    Beacons là các máy phát không dây nhỏ, sử dụng công nghệ Bluetooth Low Energy để gửi tín hiệu đến các thiết bị thông minh khác gần đó. Đây là một trong những phát triển mới nhất trong công nghệ vị trí và tiếp thị gần (Proximity Marketing). Nói một cách đơn giản, chúng kết nối và truyền thông tin đến các thiết bị thông minh giúp cho việc tìm kiếm và tương tác dựa trên vị trí trở nên dễ dàng và chính xác hơn.

    Ưu thế lớn nhất của BLE là tiết kiệm năng lượng, cho phép beacons truyền thông tin liên tục lên đến 2-3 năm chỉ với một viên pin nhỏ. Khoảng cách truyền BLE cũng lên đến 100m như Classic Bluetooth.

    BLE có hai chế độ trao đổi thông tin:

    Advertising: chỉ truyền một chiều

    Connecting: trao đổi hai chiều

    Beacons chỉ sử dụng chế độ truyền advertising (chỉ gửi thông tin một chiều). Beacons theo một chu kỳ sẽ phát thông tin quảng bá để các thiết bị khác như smartphone nhận. Beacons có thể phát với chu kỳ từ 20ms đến 10s, chu kỳ càng dài thì thời lượng pin càng lâu.

    Thiết bị Beacon rất đơn giản. Mỗi thiết bị chứa CPU, radio và pin, và nó hoạt động bằng cách liên tục phát ra một mã định danh (ID) . Mã nhận dạng này được chọn bởi thiết bị của bạn, thường là điện thoại di động và đánh dấu một vị trí quan trọng trong môi trường của bạn. Mã định danh là số ID duy nhất mà điện thoại thông minh của bạn nhận ra là duy nhất cho Beacon. Sau khi kết nối, Beacon sẽ thực hiện bất kỳ chức năng nào nó đã được lập trình bao gồm : quảng cáo, điều hướng, theo dõi…

    Chúng ta có thể xem xét ví dụ điển hình về quảng cáo trong Mall:

    Bước 1: Người dùng tìm kiếm ở Google về “Black Shoes”
    Bước 2: Quảng cáo tìm kiếm Google của bạn xuất hiện.
    Bước 3: Người dùng nhấp vào quảng cáo tìm kiếm, duyệt sản phẩm, sau đó đóng điện thoại của họ.
    Bước 4: Người dùng này quyết định họ muốn thử giày trước khi mua, vì vậy họ bước vào cửa hàng của bạn.
    Bước 5: Khi họ vào cửa hàng, điện thoại của họ nhận một số nhận dạng ID từ Beacon của cửa hàng của bạn.
    Bước 6: Beacon nhận ra rằng điện thoại này giống với điện thoại đã nhấp vào quảng cáo tìm kiếm của bạn và liên kết dữ liệu này với tài khoản Google Ads của bạn dưới dạng chuyến thăm cửa hàng trên mạng.

    Bằng cách ghi nhật ký lượt truy cập cửa hàng thực tế từ quảng cáo tìm kiếm của bạn, công nghệ này sẽ giúp bạn hiểu được tác động và hiệu quả của quảng cáo tìm kiếm của bạn. Nếu bạn thấy họ đang thu hút rất nhiều lượt truy cập vào cửa hàng của bạn, bạn có thể muốn đầu tư nhiều hơn vào tìm kiếm.

    Beacons gửi dữ liệu gì?

    Một gói tin để các thiết bị đọc được phải tuân theo các chuẩn đã được định trước, trước tiên là gói dữ liệu advertising. Một gói tin advertising có độ dài lên đến 47 bytes.

    • Preamble (1 byte)
    • Access Address (4 bytes) – 8E 89 BE D6
    • PDU Header (2 bytes)
    • PDU MAC address (6 bytes)
    • PDU Data (0-31 bytes)
    • CRC (3 bytes)

    Phân loại Beacon

    IBeacon Là giao thức BLE được Apple đưa ra 12/2013, đây là một bộ giao thức chính thức đầu tiên về BLE, đa số mọi beacons đều hỗ trợ. Giao thức này được hỗ trợ chỉ trên iOS, nhưng hiện nay có thể tìm rất nhiều hàm API hỗ trợ tìm kiếm các iBeacon trên Android. Cần có một ứng dụng để tìm kiếm beacons và thực hiện các thao tác với chúng.

    • iBeacon hỗ trợ hai kiểu tương tác, giám sát (monitoring) và vùng phủ
      (ranging). Với chế độ giám sát ứng dụng sẽ cảnh báo ngay cả khi ứng
      dụng đã tắt. Khác chế độ giám sát, chế độ cự ly chỉ hoạt động khi ứng
      dụng đang chạy.
    • Với iBeacon thì các beacon sẽ phát ra dữ liệu gì?, chúng phát ba
      thông tin UUID, Major, và Minor. Sẽ không bao giờ có hai beacon cùng
      UUID, Major, và Minor.

    Eddystone Là giao thức BLE do Google công bố 7/2015, được hỗ trợ chính thức trên cả hai nền tảng iOS và Android. Là một giao thức mở và hỗ trợ nhiều gói tin khác nhau. Chỉ hỗ trợ một kiểu tương tác cơ bản, gần giống với vùng phủ của iBeacon.

    Các gói tin của Eddystone gồm có:

    • Eddystone-UID: gần giống gói tin của iBeacon, gồm các thông tin
      Namespace (chức năng giống UUID của iBeacon) và Instance (chức năng
      giống Major và Minor của iBeacon)
    • Eddystone-URL: gửi thông tin một đường dẫn site . Với gói tin này
      trên điện thoại sẽ mở site và ko cần cái ứng dụng.
    • Eddystone-TLM: là gói tin gửi các thông tin của beacons như điện áp
      pin, nhiệt độ, số gói tin đã gửi, và thời gian bật beacons. Gói tin
      này sẽ gửi với chu kỳ dài hơn hai gói trên.

    Ứng dụng của Beacon

    Công nghệ Beacon còn rất nhiều mở rộng trong tương lai, nên việc sử dụng nó vẫn đang được phát hiện. Hiện tại, trọng tâm chính vẫn là trong lĩnh vực điều hướng và và quảng cáo. Đây là nơi bạn thấy các công ty đầu tư mạnh vào thời điểm này.

    •Sản xuất
    •Quản lý chuỗi cung ứng
    •Bán buôn / bán lẻ / thương mại
    •Nhà hàng, khách sạn
    •Du lịch
    •Giáo dục
    •Chăm sóc sức khỏe

    Core bluetooth là gì ?

    Core Bluetooth là một framework cung cấp các lớp cần thiết cho ứng dụng iOS và MacOS có thể giao tiếp với các thiết bị có hỗ trợ công nghệ không dây bluetooth được trang bị các chuẩn low energy (LE) và Basic Rate / Enhanced Data Rate (BR/EDR) .

    Central, Peripheral

    Có hai đối tượng chính trong quá trình giao tiếp Bluetooth: central và peripheral.

    • Một peripheral thường là một đối tượng có chứa dữ liệu, và các thiết
      bị khác sẽ cần sử dụng dữ liệu đó.
    • Một central thường là một đối tượng mà sử dụng thông tin được cung
      cấp bởi peripheral để thực hiện một nhiệm vụ nào đó.

    Ví dụ: Một chiếc máy đo nhịp tim (peripheral) chứa những thông tin hữu ích mà các ứng dụng trên điện thoại và máy tính (central) cần để hiển thị nhịp tim của người sử dụng theo một cách dễ hình dung trên màn hình.

    Central

    Central phát hiện và kết nối tới các Peripheral đang advertising

    • Peripheral phát tán một vài dữ liệu của chúng ở dạng advertising packet. Một advertising packet là như một gói dữ liệu nhỏ chứa những thông tin mà peripheral phải cung cấp để nhận dạng, ví dụ như tên của peripheral và chức năng chính của nó. Ví dụ, một máy điều hòa nhiệt độ phải advertise rằng chúng cung cấp nhiệt độ hiện tại của căn phòng. Trong công nghệ Bluetooth, advertising là cách chính để peripheral thể hiện sự có mặt của nó.

    • Một central, mặt khác, sẽ quét và lắng nghe bất cứ thiết bị peripheral nào đang advertising thông tin mà nó muốn, sau đó có thể yêu cầu quyền truy cập tới thiết bị đó.

    Dữ liệu của một Peripheral được cấu trúc như thế nào ?

    Mục đích của việc kết nối tới peripheral là dò xét và tương tác với với dữ liệu mà nó cung cấp, và trước khi làm vậy, sẽ tốt hơn nếu bạn hiểu dữ liệu của peripheral được cấu trúc ra sao.

    • Peripheral có thể bao gồm một hoặc nhiều service hoặc cung cấp thông tin hữu ích về cường độ tín hiệu kết nối. Một service là một tập dữ liệu và hành vi đi kèm để thực hiện một chức năng hay một tính năng của thiết bị. Ví dụ, một service của máy đo nhịp tim là đưa ra dữ liệu nhịp tim từ bộ cảm biến.

    • Service được tạo nên từ các characteristic hoặc từ việc sử dụng các service khác. Một characteristic cung cấp những thông tin chi tiết hơn về một service của peripheral. Ví dụ, service nhịp tim vừa mô tả ở trên có thể bao gồm một characteristic mô tả vị trí hướng cơ thể của bộ cảm biến và một characteristic khác trao đổi dữ liệu tính toán nhịp tim. Hình dưới đây cung cấp một cấu trúc có thể có về service và characteristic của một máy đo nhịp tim.

    Central dò xét và tương tác với dữ liệu của Peripheral

    • Sau khi central thiết lập kết nối thành công tới peripheral, nó có thể thấy được toàn bộ service và characteristic mà peripheral cung cấp (dữ liệu lúc advertising có thể chỉ là một phần của những service hiện có).

    • Một central có thể tương tác với service của peripheral bằng việc đọc và ghi các characteristic của service đó. Ví dụ, ứng dụng của bạn có thể yêu cầu thông tin về nhiệt độ phòng từ một máy điều hòa nhiệt độ hoặc cung cấp cho máy điều hòa nhiệt độ một giá trị để thiết lập nhiệt độ phòng.

    Khi thiết bị của bạn đóng vai trò là Central

    Phần lớn các hoạt động khi xử lý với Bluetooth của bạn nằm ở phía central

    Ở phía central, một thiết bị local central được thể hiện bởi một đối tượng CBCentralManager. Đối tượng này được sử dụng để quản lý việc phát hiện và kết nối tới thiết bị remote peripheral (thể hiện bởi đối tượng CBPeripheral), bao gồm việc quét, phát hiện, và kết nối tới peripheral đang advertising.

    Thiết bị central sẽ có nhiệm vụ tìm và kết nối tới các peripheral, sau đó sẽ đọc và tương tác với dữ liệu của peripheral đó.

    • Khởi tạo central manager object
    • Tìm kiếm và kết nối tới một peripheral nào đó đang phát sóng.
    • Đọc dữ liệu trên peripheral sau khi kết nối thành công.
    • Gửi các request đọc và ghi một characteristic nào đó của các dịch vụ trong peripheral.
    • Đăng ký nhận thông báo khi một characteristic cập nhật.

    Refer:
    https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/PerformingCommonCentralRoleTasks/PerformingCommonCentralRoleTasks.html#//apple_ref/doc/uid/TP40013257-CH3-SW1

    Khi thiết bị của bạn đóng vai trò là Peripheral

    Máy Mac sử dụng MacOS 10.9 trở lên, thiết bị iOS sử dụng iOS 6.0 trở lên có chứa các tính năng để tương tác như một peripheral, cung cấp dữ liệu cho các thiết bị khác, bao gồm các máy Mac, iPhone và iPad khác. Khi thiết lập thiết bị của bạn để thực thi trong vai trò peripheral, bạn đang thực hiện những hoạt động ở phía peripheral trong việc giao tiếp giữa các thiết bị Bluetooth.

    • Ở phía peripheral, một thiết bị local peripheral được thể hiện bởi đối tượng CBPeripheralManager. Đối tượng này được sử dụng để quản lý các service công khai bên trong cơ sở dữ liệu về service và characteristic của thiết bị local peripheral, và có nhiệm vụ advertising các service này tới các thiết bị remote central (thể hiện bởi đối tượng CBCentral). Đối tượng peripheral manager còn được sử dụng để trả lời các yêu cầu đọc và ghi từ remote central.

    Thiết bị peripheral có nhiệm vụ cung cấp, phát tán các service của nó, và trả lời các request từ central.

    • Khởi tạo một peripheral manager object.
    • Thiết lập các service và characteristic trên thiết bị.
    • Publish các service và characteristic tới database của thiết bị.
    • Phát tán các service.
    • Trả lời các yêu cầu đọc và ghi từ các central được kết nối.
    • Gửi các giá trị của characteristic cho các central đã đăng ký (subscribe).

    Refer:
    https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/PerformingCommonPeripheralRoleTasks/PerformingCommonPeripheralRoleTasks.html#//apple_ref/doc/uid/TP40013257-CH4-SW1

    Lời kết

    Qua bài viết trên đây hi vọng mọi ngừời sẽ nắm được những thông tin cơ bản của bluetooth cũng như các ứng dụng của nó tới đời sống, ngoài ra các bạn hãy code demo về corebluetooth như guide của apple mình đã refer link ở trên nhé.

  • [RxSwift] Traits

    Contents

    • Traits là gì?
    • Các loại Traits
      • Single
      • Completable
      • Maybe

    Traits là gì?

    Traits là observables nhưng với 1 phạm vi hành vi hẹp hơn so với các Observables thông thường.

    Lợi ích của Traits:

    • Vì phạm vi hẹp hơn, nên khi sử dụng Traits thì sẽ làm cho người đọc clear hơn về í nghĩa của task.
    • Traits luôn được observe và subcribe trên Main.

    Các loại Traits:

    • Trait trong RxSwift gồm 3 loại, đó là: Single, Completable

    Single:

    • Single sẽ chỉ emit ra duy nhất 1 event, và event đó phải thuộc 1 trong 2 kiểu .success(value) hoặc .error.
    • .success(value) về bản chất là sự kết hợp của .next và .complete event của Observable.
    • Sau khi emit event, single sẽ tự terminate.

    Demo

    enum DevideError: Error {
        case divideByZero
    }
    func divide(number1: Int, number2: Int) -> Single<Int> {
        return Single<Int>.create(subscribe: { observer in
            if number2 == 0 {
                 observer(.error(DevideError.divideByZero))
            } else {
                 let result = number1 / number2
                 observer(.success(result))
            }
            return Disposables.create()
        })
    }
    divide(number1: 10, number2: 2)
         .subscribe { (event) in
              switch event {
               case .success(let result):
                    print(result)
               case .error( let error):
                    print(error)
          }
    }

    Vì number2 khác 0, nên khi thực hiện hàm divide 10 cho 2 thì sẽ trả về 1 Single và nó sẽ emit ra .success(5).

    Completable:

    • Completable sẽ chỉ emit ra duy nhất 1 event, và event đó phải thuộc loại .completed hoặc .error.
    • Sau khi emit ra event thì sẽ tự động terminate.
    func divide(number1: Int, number2: Int) -> Completable {
        return Completable.create(subscribe: { observer in
            if number2 == 0 {
                observer(.error(DevideError.divideByZero))
            } else {
                let result = number1 / number2
                observer(.completed)
            }
            return Disposables.create()
        })
    }

    Maybe:

    • Maybe là sự kết hợp của Single và Completable. Nó có thể emit .success(value), .completed hoặc .error event.
    • Vẫn như 2 loại traits trên, Maybe sẽ chỉ emit ra 1 event duy nhất.
    • Sau khi emit ra event thì sẽ tự động terminate.
    Maybe cho phép bạn emit ra 3 loại event khác nhau.
    func divide(number1: Int, number2: Int) -> Maybe<Int> {
        return Maybe<Int>.create(subscribe: { observer in
            if number2 == 0 {
                observer(.error(DevideError.divideByZero))
            } else {
                let result = number1 / number2
                observer(.success(result))
            }
            return Disposables.create()
        })
    }

    Convert Traits thành Observable và ngược lại

    • Có thể convert 1 traits thành Observable đơn giản như sau:
    single.asObservable()
    • Việc convert từ Observable thành Traits cũng đơn giản như vậy:
    observable.asSingle()

    Tuy nhiên Traits thì chỉ emit ra 1 event, còn Observable của bạn thì có thể sẽ emit ra nhiều event. Vì vậy nếu convert 1 Observable emit ra nhiều event sang Traits thì traits đó sẽ emit ra error. Ví dụ như sau:

    let observable = Observable<String>.of("abc", "def")
    observable.asSingle().subscribe({ event in
        switch event {
        case .success(let value):
            print(value)
        case .error(let error):
            print(error)
        }
    })
    Lỗi được emit ra.

    Khi nào thì sử dụng Single, Completable & Maybe:

    Single:

    • Single có tác dụng cho những tiến trình chỉ chạy 1 lần và sẽ thành công với 1 value, hoặc sẽ bị error.
    • Ví dụ như download File, load data, …

    Completable:

    • Dùng cho những task mà bạn chỉ quan tâm xem nó thành công hay thất bại.
    • Ví dụ: Write file…

    Maybe:

    • Dùng cho những task có thể success hoặc fail, và tùy lúc sẽ emit ra 1 value khi success.
  • [RxSwift] Observable

    Contents

    • Observable là gì?
    • Các loại event
    • Khởi tạo Observable
    • Subcribe
    • Create Observable

    Observable là gì?

    • Observable là phần quan trọng nhất của RxSwift. có thể coi nó như 1 sequence, có tác dụng phát ra (emit) các event, và các object khác có thể lắng nghe những event đó.
    • Các event có thể đính kèm các giá trị với các kiểu dữ liệu quen thuộc như Int, String, … hay các kiểu dữ liệu mà bạn tự tạo ra.
    Hình trên là ví dụ cho các event 1, 2 ,3 lần lượt được emit ra theo thời gian.

    Các loại Event:

    Các event được emit ra thuộc 1 trong 3 loại:

    • next: Khi 1 Observable emit 1 element, thì nó được coi là 1 next event. Next event có thể đính kèm giá trị, và Observable có thể emit nhiều next event trong 1 life cycle của nó.
    • completed: Khi Observable hoàn thành việc emit event của mình, thì nó sẽ emit ra event completed và tự động terminate.
    • error: Khi Observable gặp lỗi trong quá trình emit event, thì nó sẽ emit ra event error và tự động terminate.

    Note:
    Tóm gọn lại, Observable có thể phát ra nhiều next event trong 1 life cycle; ngược lại thì Observable chỉ có thể phát ra 1 event complete hoặc error, sau đó sẽ tự hủy và không phát ra bất cứ event nào nữa.

    Khởi tạo Observable:

    of:

    let observable = Observable.of(1,2,3)
    let observable2 = Observable.of([4,5,6])
    • Khởi tạo bằng func of cùng với các giá trị khởi tạo.
    • Cho mỗi next event, Observable sẽ lần lượt emit ra các element được cung cấp.
    • Các giá trị cung cấp phải cùng kiểu dữ liệu: Cùng Int, String, …
    Sau khi phát ra hết các element, observable sẽ emit ra completed event và terminate.

    from:

    let observable = Observable.from([1,2,3])
    • Khi khởi tạo 1 Observable bằng func from, thì giá trị được khởi tạo mặc định phải thuộc kiểu Array.
    • Cho mỗi event next, Observable sẽ lần lượt emit ra các giá trị trong mảng.

    Sau khi emit hết các phần tử trong mảng, observable sẽ emit ra event completed và terminate.

    Ngoài of và from, thì còn các cách khởi tạo khác như just, never, empty.

    create:

    • Với các cách ở trên, thì observable sẽ tự động emit các event được cung cấp sẵn 1 cách lần lượt và tự emit 1 complete sau khi hoàn thành.
    • Bằng cách sử dụng hàm create để tạo 1 Observable, bạn có thể custom Observable theo cách riêng của ban để có thể emit ra completed hoặc error bất cứ khi nào bạn muốn
    let observable = Observable<String>.create({ observer in
        observer.onNext("Global")
        observer.onNext("Smart")
        observer.onCompleted()
        observer.onNext("Technology")
        return Disposables.create()
    })

    Kết quả được in ra: (Sau khi emit ra event complete thì observable sẽ bị terminate, do đó nó sẽ không emit ra "Technology" nữa.

    Subcribe:

    Chỉ copy đoạn code ở trên và run thì bạn sẽ không thấy có gì được print ra màn hình cả. Tại sao vậy?

    1 Observable chỉ có thể emit ra event khi nó được subcribe.

    Đoạn code ở trên, ta chưa subcribe Observable đó.Vậy làm thế nào để subcribe 1 Observable?

    subcribe event

    Để subcribe tới 1 Observable thì vô cùng đơn giản với hàm subcribe:

    let observable = Observable.of(1,2,3)
    observable.subscribe { (value) in
        print(value)
    }

    Để í phần Summary của hàm subcribe này: Đó là hàm có tác dụng subcribes 1 event. Vì vậy, các giá trị được print ra sẽ có cả kiểu của event được emit ra (next/complete/error).

    Kết quả print ra trên màn hình:

    Vậy nếu bạn muốn chỉ print ra giá trị của value mà không cần print ra kiểu event?

    subcribe element

    • Cách 1: Mỗi event sẽ có thuộc tính element để lấy ra giá trị đính kèm với event đó.
    observable.subscribe { (value) in
        if let element = value.element {
            print(element)
        }
    }
    • Cách 2: Sử dụng hàm subcribe:
    observable.subscribe(onNext: { (element) in
        print(element)
    })

    Theo như summary, hàm subcribe này sẽ subcribe đến element, vì vậy nó sẽ chỉ in ra giá trị của element.
    Cả 2 cách đều cho 1 kết quả như sau:

    Ngoài ra , với hàm trên, bạn có thể thực hiện các hành động khi observable emit ra event completed, error, … như sau:

    let observable = Observable<String>.create({ observer in
        observer.onNext("Global")
        observer.onNext("Smart")
        observer.onCompleted()
        observer.onNext("Technology")
        return Disposables.create()
    }).subscribe(onNext: { (element) in
        print(element)
    }, onCompleted: {
         print("Completed")
    })

    Kết luận:

    Observable là 1 thứ đơn giản nhưng vô cùng quan trọng trong RxSwift.

    Tham khảo: RxSwift – Reactive Programming with Swift v1.1

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

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

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

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

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

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

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

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

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

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

    B1. Tạo certificate cho app

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

    2. Chọn nút (+)

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

    4. Tạo certificate signing request

    4.1. Mở app Keychain Access ở máy

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

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

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

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

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

    B2. Đăng ký AppID

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

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

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

    B3. Đăng ký devices

    Đăng ký một device:

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

    Đăng ký nhiều device:

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

    B4. Tạo provisioning profile

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

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

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

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

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

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

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

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

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

  • MVVM with Swift P2

    MVVM with Swift P2

    Demo MVVM với RxSwift:

    Model Layer:

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

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

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

    ViewModel Layer

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

    View Layer:

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

    Tác dụng của MVVM:

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

    MVVM vs MVP:

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

    Kết luận:

    • Mục tiêu chính của MVVM là tách biệt logic khỏi View. Tuy nhiên, những phần logic như truy vấn database, networking, … thì chưa được xử lí. Những logic như vậy có thể được đặt ở presenter, hoặc tạo 1 class riêng ở Model tùy theo bản thân bạn.
      Tuy nhiên, nên tách biệt các phần logic Database, networking, … ra các class riêng để tuân thủ nguyên tắc Single Responsibility Principle của SOLID.
    • Theo nguyên tắc MVVM thì ViewModel không import UIKit để tách biệt logic và View.