Skip to content
  • Home
  • Code
  • iOS & Swift
  • Combine
  • RxSwift
  • SwiftUI
  • Flutter & Dart
  • Tutorials
  • Art
  • Blog
Fx Studio
  • Home
  • Code
  • iOS & Swift
  • Combine
  • RxSwift
  • SwiftUI
  • Flutter & Dart
  • Tutorials
  • Art
  • Blog
Written by chuotfx on March 1, 2020

Combine – Transforming Operators trong 10 phút

Combine

Contents

  • Chuẩn bị
  • 1. publisher
  • 2. Collecting values
  • 3. Mapping values
    • 3.1. map
    • 3.2. Map key paths
    • 3.3. tryMap
    • 3.4. flatMap
  • 4. Replacing upstream output
    • 4.1. replaceNil(with:)
    • 4.2. replaceEmpty(with:)
  • 5. Scan
  • Tóm tắt

Chào bạn,

Chúng ta lại tiếp tục với series Combine từ Fx Studio. Bài viết này sẽ mang tính chất liệt kê & giải thích ý nghĩa của các Operator trong nhóm Transforming Operators của Combine Framework. Đây là một trong những bài giúp bạn chuẩn bị kiến thức để tiến tới sử dụng Combine trong UIKit.

Bắt đầu thôi!

Chuẩn bị

  • Xcode 11
  • Swift 5.1
  • Playground

Trước tiên thì xin định nghĩa đơn giản nhất cho nhóm Operator này.

Transforming Operators là nhóm các toán từ làm biết đổi giá trị dữ liệu của publisher hoặc biến đổi cả publisher thành 1 publisher khác.

1. publisher

Ta xem qua các ví dụ sau:

let publisher1 = "Hello world".publisher

let publisher2 = (1...19).publisher

let publisher3 = ["A": 1, "B": 2, "C": 3].publisher

Bạn đã bắt gặp chúng ở các bài đầu tiên khi giới thiệu về Combine rồi. Vậy với toán tử publisher sử dụng cho một số kiểu dữ liệu Collection (Array, String, Set, Dictionary … ) thì chúng ta có được một Publisher, với:

  • Input chính là kiểu dữ liệu của phần tử trong đó
  • Failure là Never

Ngoài ra, bạn có thể tạo các extension để custom và đặt tên các Publisher đó publisher . Nhằm mục đích đồng nhất cách code trong Combine.

2. Collecting values

Tư tưởng lớn ở đây, chính là bạn mệt mỏi khi phải làm việc với từng giá trị đơn riêng lẻ. Đôi lúc bạn muốn tổng hợp lại và xử lý nhanh gọn 1 lần nhiều giá trị. Thì các operator liên quan tới collecting sẽ giúp đỡ bạn.

Cách dùng đơn giản, cầm đầu thèn publisher nào đó. và gọi function sau:

  • Gôm hết các giá trị lại 1 lần
.collect()
  • Gôm theo số lượng chỉ định
.collect(2)

Về bản chất, nó cũng trả về là một Publisher mà thôi. Nên sau đó bạn có thể subscribe như bình thường.

Ví du code mình hoạ

var subscriptions = Set<AnyCancellable>()

let publisher = (1...99).publisher

publisher
  .collect()
  .sink(receiveCompletion: { complete in
    print(complete)
  }) { value in
    print(value)
  }
  .store(in: &subscriptions)

Với ví dụ trên, ta được 1 Array Int thay vì từng Int khi sử dụng .collect(). Còn nếu dùng collect(3), thì ta được mỗi giá trị là 1 Array Int với 3 phần tử Int.

3. Mapping values

Đây là những toán tử chuyển đổi kiểu giá trị này thành kiểu giá trị khác. Dựa theo việc ánh xạ từ phần tử này sang phần tử kia. Với một quy luật nào đó mà mình đặt ra.

Ví dụ: Có 1 danh sách tên học sinh, mỗi cái tên ứng với một loài hoa -> sau khi biến đổi thì ta có 1 danh sách các loài hoa. Nghe hơi vô lý phải không nào 😀

3.1. map

Xem đoạn code sau và hình dung nha

var subscriptions = Set<AnyCancellable>()

let formatter = NumberFormatter()
formatter.numberStyle = .spellOut

    [22, 7, 1989].publisher
        .map {
            formatter.string(for: NSNumber(integerLiteral: $0)) ?? "" }
        .sink(receiveValue: { print($0) })
        .store(in: &subscriptions)

Giải thích:

  • Tạo ra một formatter của Number. Nhiệm vụ nó biến đổi từ số thành chữ
  • Tạo ra 1 publisher từ một array Integer
  • Sử dụng toán tử .map để biến đối tường giá trị nhận được thành kiểu string
  • Các toán tử còn lại thì như đã trình bày các phần trước rồi

Toán tử map giúp biến đổi kiểu giá trị Output của Publisher.

3.2. Map key paths

Bổ sung cho toán tử map trên thì Combine hỗ trợ cho chúng ta thêm 3 function của nó như sau:

map<T>(_:)
map<T0, T1>(_:_:)
map<T0, T1, T2>(_:_:_:)

Thay vì tấn công biến đổi chính đối tượng khi nó là Output của 1 publisher nào đó. Thì ta có thể biến đổi publisher đó thành một publisher khác. Mà phát ra kiểu giá trị mới, chính là kiểu giá trị của 1 trong các thuộc tính đối tượng. Xem ví dụ đi cho chắc.

struct Dog {
  var name: String
  var age: Int
}

let publisher = [Dog(name: "MiMi", age: 3),
                 Dog(name: "MiLy", age: 2),
                 Dog(name: "PoChi", age: 1),
                 Dog(name: "ChiPu", age: 3)].publisher

publisher
  .map(\.name)
  .sink(receiveValue: { print($0) })
  .store(in: &subscriptions)

Giải thích:

  • Ta có class Dog
  • Tạo 1 publisher từ việc biến đổi 1 Array Dog. Lúc này Output của publisher là Dog
  • Sử dụng map(\.name) để tạo 1 publisher mới với Output là String. String là kiểu dữ liệu cho thuộc tính name của class Dog
  • sink và store như bình thường

3.3. tryMap

Khi bạn làm những việc liên quan tới nhập xuất, kiểm tra, media, file … thì hầu như phải sử dụng try catch nhiều. Nó giúp cho việc đảm bảo chương trình của bạn không bị crash. Tất nhiên, nhiều lúc bạn phải cần biến đổi từ kiểu giá trị này tới một số kiểu giá trị mà có khả năng sinh ra lỗi. Khi đó bạn hãy dùng tryMap như một cứu cánh.

Khi gặp lỗi trong quá trình biến đổi thì tự động cho vào completion hoặc error . Bạn vẫn có thể quản lí nó và không cần quan tâm gì tới bắt try catch …

Xem ví dụ sau:

Just("Đây là đường dẫn tới file XXX nè")
        .tryMap { try FileManager.default.contentsOfDirectory(atPath: $0) }
        .sink(receiveCompletion: { print("Finished ", $0) },
              receiveValue: { print("Value ", $0) })
        .store(in: &subscriptions)

Giải thích:

  • Just là 1 publisher, sẽ phát ra ngay giá trị khởi tạo
  • sử dụng tryMap để biến đổi Output là string (hiểu là đường dẫn của 1 file nào đó) thành đối tượng là file (data)
  • Trong closure của tryMap thì tiến hành đọc file với đường dẫn kia
  • Nếu có lỗi (trong trường hợp ví dụ) thì sẽ nhận được ở completion với giá trị là failure

OKE, rất khoẻ phải không nào. Giờ thì yêu Combine cmnr!

3.4. flatMap

Trước tiên thì ta cần hệ thống lại một chú về em map và em flatMap

  • map là toán tử biến đổi kiểu dữ liệu Output. Ví dụ: Int -> String…
  • flatMap là toán tử biến đổi 1 publisher này thành 1 publisher khác
    • Mới hoàn toàn
    • Khác với thèn publisher gốc kia

Thường sử dụng flatMap để truy cập vào các thuộc tính trong của 1 publisher. Để hiểu thì bạn xem minh hoạ đoạn code sau:

Trước tiên tạo 1 struct là Chatter, trong đó có name và message. Chú ý, message là một CurrentValueSubject, nó chính là publisher.

public struct Chatter {
    public let name: String
    public let message: CurrentValueSubject<String, Never>
    public init(name: String, message: String) {
        self.name = name
        self.message = CurrentValueSubject(message)
    }
}

Ta tạo các đối tượng sau, là 2 nhân vật sẽ tham gia đàm thoại với nhau:

let teo = Chatter(name: "Tèo", message: " --- TÈO đã vào room ---")
let ti = Chatter(name: "Tí", message: " --- TÍ đã vào room ---")

Tạo room chát là một publisher với PassthroughSubject với Output là Chatter và không bao giờ lỗi. Tiến hành subscribe nó. Nhưng trước tiên là phải sử dụng flatMap để biến đổi pulisher với kiểu Output Chatter thành publisher với kiểu Output là  String. Chúng ta chỉ subscribe publisher String đó thôi.

let chat = PassthroughSubject<Chatter, Never>()
    
    chat
        .flatMap { $0.message }
        .sink { print($0) }
        .store(in: &subscriptions)

OKE, chát thôi

//let's go chat
    
    //1 : Tèo vảo room
    chat.send(teo)
    //2 : Tèo hỏi
    teo.message.value = "TÈO: Tôi là ai? Đây là đâu?"
    //3 : Tí vào room
    chat.send(ti)
    //4 : Tèo hỏi thăm
    teo.message.value = "TÈO: Tí khoẻ không."
    //5 : Tí trả lời
    ti.message.value = "TÍ: Tao không khoẻ lắm. Bị Thuỷ đậu cmnr mày."
    
    let thuydau = Chatter(name: "Thuỷ đậu", message: " --- THUỶ ĐẬU đã vào room ---")
    //6 : Thuỷ đậu vào room
    chat.send(thuydau)
    thuydau.message.value = "THUỶ ĐẬU: Các anh gọi em à."
    
    //7 : Tèo sợ
    teo.message.value = "TÈO: Toang rồi."

Bạn run code vào xem kết quả. Mình sẽ giải thích như sau:

  • chat là 1 publisher, chúng ta send các giá trị của nó đi (Chatter). Đó là các phần tử được join vào room
  • Vì mỗi phần tử đó có thuộc tính là 1 publisher (messgae). Để đối tượng chatter có thể phát tin nhắn đi, thay vì phải join lại room.  Nên khi subscribe nếu không dùng flatMap thì sẽ ko nhận được giá trị từ các stream của các publisher join vào trước.
  • flatMap giúp cho việc hợp nhất các stream của các publisher thành 1 stream và đại diện chung là 1 publisher mới với kiểu khác các publisher kia.
  • Tất nhiên, khi các publisher riêng lẻ send các giá trị đi, thì chat vẫn nhận được và hợp chất chúng lại cho subcriber của nó.

Cuối câu chuyện bạn cũng thấy là THUỶ ĐẬU đã join vào. Vì vậy, muốn khống chế số lượng publisher thì sử dụng thêm tham số maxPublishers

      chat
        .flatMap(maxPublishers: .max(2)) { $0.message }
        .sink { print($0) }
        .store(in: &subscriptions)

OKE, em nó đã bị cấm cửa. Nếu không có giá trị max thì nó tương đường với unlimited.

Tóm tắt nhanh cho các em map nha:

  • map dùng để biến đối kiểu giá trị này thành kiểu giá trị khác
  • Map key paths : dùng để biến đổi các thuộc tính của 1 đối tượng, thành cái gì đó mới hoặc cho vui cũng được.
  • tryMap dùng để biến đổi như map, nhưng sử dụng với các kiểu dữ liệu có nguy cơ sinh ra lỗi. Khi có lỗi thì tự động chúng sẽ vào completion với error
  • flatMap dùng để biến đổi 1 publisher này thành 1 publisher khác. Bên cạnh đó còn quản lí các stream của các publisher trong đó. Hiểu nôm na là hợp nhất các stream thành 1 steam và khống chế số lượng các steam lắng nghe.

4. Replacing upstream output

Cái này nghe cái tên thì cũng đoán ra được ít nhiều phần nào rồi. Đôi khi một số kiểu dữ liệu cho phép việc vắng mặt giá trị (Optional) hoặc khi giá trị là nil. Combine cung cấp cho chúng ta các toán tử để thay thế như sau:

4.1. replaceNil(with:)

["A",  nil, "B"].publisher
        .replaceNil(with: "-")
        .sink { print($0) }
        .store(in: &subscriptions)

Đơn giản là publisher phát ra giá trị nào nil thì sẽ thay thế bằng giá trị nào đó được chỉ định. Tuy nhiên chúng sẽ là kiểu Optional và muốn code sạch đẹp hơn thì bạn phải khử Optional đó. Ví dụ:

["A",  nil, "B"].publisher
        .replaceNil(with: "-")
        .map({$0!})
        .sink { print($0) }
        .store(in: &subscriptions)

4.2. replaceEmpty(with:)

Khi mà publisher không chịu phát gì hết thì sao? Khi đó toán tử replaceEmpty sẽ chèn thêm giá trị nếu pulisher không phát đi bất cứ gì mà lại complete. (choá thật)

let empty = Empty<Int, Never>()
    
    empty
        .replaceEmpty(with: 1)
        .sink(receiveCompletion: { print($0) },
              receiveValue: { print($0) })
        .store(in: &subscriptions)

5. Scan

Chuyển đổi từng phần tử trên upstream của publisher. Bằng cách cung cấp phần tử hiện tại, là một closure với giá trị cuối cùng kèm theo.

Nghe qua thì khá mơ hồ, tạm thời bạn qua ví dụ sau:

let pub = (0...5).publisher
    
    pub
        .scan(0) { $0 + $1 }
        .sink { print ("\($0)", terminator: " ") }
        .store(in: &subscriptions)

Giải thích:

  • Tạo 1 publisher bằng cách biến đổi 1 Array Integer từ 0 tới 5 thông qua toán tử publisher
  • Biển đổi từng phần tử của pub bằng toán tử scan với giá trị khởi tạo là 0
  • Scan sẽ phát ra các phần tử mới bằng cách kết hợp 2 giá trị lại
  • Cái khởi tạo là đầu tiên -> cái nhận được là thứ 2 -> cái tạo ra mới được phát đi và trở thành lại cái đầu tiên.
  • Lặp lại cho đến hết

Tóm tắt

  • Họ nhà map Giúp biến đổi Output hay kể cả Publisher.
  • map có 2 phiên bản, trực tiếp biến đổi đối tượng. Hoặc biến đổi các thuộc tính của đối tượng.
  • tryMap dành cho việc tương tác với các kiểu dữ liệu có nguy cơ sinh ra lỗi
  • flatMap biển đổi Publisher này thành Publisher khác
  • replaceNil thay các giá trị nil thành 1 giá trị nào đó
  • replaceEmpty cho nó một giá trị nếu publisher không phát đi bất cứ giá trị nào hết mà kết thúc
  • scan quét sạch hết các giá trị nhận được từ publisher. Kết hợp chúng lại để phát ra một giá trị cuối cùng theo luật riêng được định nghĩa ở closure

Cảm ơn bạn đã đọc bài viết này!

 

FacebookTweetPinYummlyLinkedInPrintEmailShares37

Related Posts:

  • RxSwift - Filtering Operators
    RxSwift - Filtering Operators
  • RxSwift - Time Based Operators
    RxSwift - Time Based Operators
  • RxSwift - Tìm hiểu Operators & Hello world!
    RxSwift - Tìm hiểu Operators & Hello world!
  • Image View trong 10 phút - SwiftUI Notes #26
    Image View trong 10 phút - SwiftUI Notes #26
Tags: combine
Written by chuotfx

Hãy ngồi xuống, uống miếng bánh và ăn miếng trà. Chúng ta cùng nhau đàm đạo về đời, về code nhóe!

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Donate – Buy me a coffee!

Fan page

Fx Studio

Tags

Actor Advanced Swift api AppDistribution Asynchronous autolayout basic ios tutorial blog callback ci/cd closure collectionview combine concurrency CoreData Core Location crashlytics darkmode dart dart basic dart tour Declarative decoding delegate deploy fabric fastlane firebase flavor flutter GCD iOS mapview MVVM optional protocol rxswift Swift Swift 5.5 SwiftUI SwiftUI Notes tableview testing TravisCI unittest

Recent Posts

  • Raw String trong 10 phút
  • Dispatch Semaphore trong 10 phút
  • Tổng kết năm 2022
  • KeyPath trong 10 phút – Swift
  • Make color App Flutter
  • Ứng dụng Flutter đầu tiên
  • Cài đặt Flutter SDK & Hello world
  • Coding Conventions – người hùng hay kẻ tội đồ?
  • Giới thiệu về Flutter
  • Tìm hiểu về ngôn ngữ lập trình Dart

You may also like:

  • Convenience Initializer trong 10 phút
    Convenience Initializer trong 10 phút
  • Task & Task Group trong 10 phút - Swift 5.5
    Task & Task Group trong 10 phút - Swift 5.5
  • Race Condition và giải pháp trong 10 phút - Swift
    Race Condition và giải pháp trong 10 phút - Swift
  • Cơ bản về Actor trong 10 phút - Swift 5.5
    Cơ bản về Actor trong 10 phút - Swift 5.5
  • Image View trong 10 phút - SwiftUI Notes #26
    Image View trong 10 phút - SwiftUI Notes #26

Archives

  • February 2023 (1)
  • January 2023 (2)
  • November 2022 (2)
  • October 2022 (1)
  • September 2022 (5)
  • August 2022 (6)
  • July 2022 (7)
  • June 2022 (8)
  • May 2022 (5)
  • April 2022 (1)
  • March 2022 (3)
  • February 2022 (5)
  • January 2022 (4)
  • December 2021 (6)
  • November 2021 (8)
  • October 2021 (8)
  • September 2021 (8)
  • August 2021 (8)
  • July 2021 (9)
  • June 2021 (8)
  • May 2021 (7)
  • April 2021 (11)
  • March 2021 (12)
  • February 2021 (3)
  • January 2021 (3)
  • December 2020 (3)
  • November 2020 (9)
  • October 2020 (7)
  • September 2020 (17)
  • August 2020 (1)
  • July 2020 (3)
  • June 2020 (1)
  • May 2020 (2)
  • April 2020 (3)
  • March 2020 (20)
  • February 2020 (5)
  • January 2020 (2)
  • December 2019 (12)
  • November 2019 (12)
  • October 2019 (19)
  • September 2019 (17)
  • August 2019 (10)

About me

Education, Mini Game, Digital Art & Life of coders
Contacts:
contacts@fxstudio.dev

Fx Studio

  • Home
  • About me
  • Contact us
  • Mail
  • Privacy Policy
  • Donate
  • Sitemap

Categories

  • Art (1)
  • Blog (22)
  • Code (4)
  • Combine (22)
  • Flutter & Dart (24)
  • iOS & Swift (86)
  • RxSwift (37)
  • SwiftUI (76)
  • Tutorials (70)

Newsletter

Stay up to date with our latest news and posts.
Loading

    Copyright © 2023 Fx Studio - All rights reserved.

    Share this ArticleLike this article? Email it to a friend!

    Email sent!