Category: Swift

  • 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.
  • MVVM with swift P1

    MVVM with swift P1

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

    Content:

    Phần 1:

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

    Phần 2:

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

    MVVM là gì?

    MVVM gồm 3 phần:

    Model:

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

    View:

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

    ViewModel:

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

    Note:

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

    Demo MVVM with Closure/Callback:

    Model Layer

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

    ViewModel Layer:

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

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

    View Layer

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

    Optimize Closure?

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

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

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

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

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

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

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

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

    Kết luận:

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

  • MVP with Swift

    MVP with Swift

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

    Content

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

    Tổng quan về MVC:

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

    Model

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

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

    View

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

    Controller

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

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

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

    Vấn đề của MVC?:

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

    Ý tưởng của MVP:

    MVP gồm 3 phần:

    View:

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

    Presenter:

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

    Model:

    Tương tự như MVC.

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

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

    Demo:

    Model Entity:

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

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

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

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

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

    Lợi ích của MVP:

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

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

    Kết luận:

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

    Dependency Injection trong iOS

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

    Content

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

    Ý tưởng của DI:

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

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

    Note:

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

    Implement DI trong swift:

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

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

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

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

    Constructor Injection

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

    Property Injection

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

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

    Ưu điểm:

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

    Nhược điểm:

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

    Interface Injection

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

    Method Injection

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

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

    Ambition Context:

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

    Ưu điểm:

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

    Nhược điểm:

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

    Kết luận:

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

    Basic CAShapeLayer iOS (P2)

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

    Content

    • Stroke
    • Fill Color
    • Line

    Stroke

    Stroke Color

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

    Code vẽ hình vuông:

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

    Stroke Start & Stroke End

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

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

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

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

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

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

    shapeLayer.strokeEnd = 0.7

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

    shapeLayer.strokeStart = 0.3

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

    Fill Color

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

    Code vẽ hình tròn:

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

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

    Line

    Line Width

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

    shapeLayer.lineWidth = 20.0

    Kết quả thu được:

    Line Cap

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

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

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

    Line Dash Pattern

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

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

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

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

    Line Dash Phase

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

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

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

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

  • Basic CAShapeLayer iOS (P1)

    Basic CAShapeLayer iOS (P1)

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

    Contents:

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

    How to draw a line?

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

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

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

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

    Draw shape with shape layer:

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

    ShapeLayer là gì?

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

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

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

    Vẽ hình vuông:

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

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

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

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

    Hình bầu dục:

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

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

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

    Vẽ hình tròn:

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

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

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

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

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