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 4, 2020

Combine vs. UIKit – Hello ViewController

Combine

Contents

  • Chuẩn bị
  • 1. import
  • 2. Subscriptions
  • 3. Publisher
  • 4. Subscribe
  • 5. Operators
  • 6. Handle Events
  • Tạm kết

Chào bạn đến với Fx Studio,

Bài viết này sẽ là bài viết đầu tiên trong chuỗi các bài viết về việc kết hợp Combine với UIKit. Đối tượng tấn công trước hết chính là UIViewController.

Nếu bạn chưa biết về Combine là gì, thì có thể bắt đầu với phần Basic Combine Framework. Còn nếu bạn là fan của Fx Studio và đã đọc các phần trước rồi thì …

Bắt đầu thôi!

Chuẩn bị

  • Xcode 11.0
  • Swift 5.1
  • iOS 13.0

Chúng ta sẽ sử dụng một project iOS để làm code ví dụ và giải thích ý nghĩa trong bài này. Bạn có thể tạo project có hay không có Storyboard cũng đều được. Project của chúng ta như sau:

Trong đó:

  • 1 UIViewController
  • Có 1 UILabel để hiển thị dữ liệu
  • 2 UIButton, 1 cái để tăng giá trị và 1 cái để giảm giá trị

1. import

Công việc đầu tiên là gõ dòng lệnh vào vào file ViewController.

import Combine

Mặc dù Combine đã được Apple âm thầm gài gắm vào nhiều framework. Nhưng để sử dụng full tính năng của nó thì cần import đúng thư viện. Tới đây thì bạn đã hoàn thành 50% công việc rồi đó.

OKAY, I’M FINE!

2. Subscriptions

Làm việc với ứng dụng thì vấn đề đầu tiên cần chú ý đó là quản lý bộ nhớ.

Với phong cách lập trình theo Reactive Programming của RxSwift hay Asynchronous Programming của Combine đó là quản lý các subscriptions của các subscribers tới các publisher.

Muốn quán lý chúng, bạn cần khai báo 1 property cho ViewController

var subscriptions = Set<AnyCancellable>()

Mỗi khi bạn subscribe một Publisher nào đó, sẽ 1 token cho đối tượng Cancellable. Mà chúng ta đã biết thì Class Subscriber cũng kế thừa Cancellable. Công với việc subscribe (bằng sink hay assign) thì cũng để sinh ra một đối tượng cancellable.

Do đó, tốt nhất là lưu trữ các cancellable vào 1 tập hợp với kiểu là Set để tránh trùng lặp các token. Cái này có lợi ích như sau:

  • Tập trung các subscription phát sinh trong ViewController
  • Khi ViewController bị giải phóng, thì các subscription lưu trữ này cũng giải phóng theo
  • Tối ưu được bộ nhớ của ứng dụng

Để cho dễ liên tưởng thì bên RxSwift có anh chàng disposebag , thì cái này tương tự vậy.

3. Publisher

Nhân vật chính bây giờ mới xuất hiện. Publisher là trái tim của Combine. Nó là nguồn phát đi các giá trị nên là thành phần không thể thiếu được trong việc sử dụng Combine Code vào trong UIKit.

Publisher sẽ điều khiển luồng dữ liệu trong ViewController.

Quay lại project của chúng ta. Ta có 1 UILabel để hiện thị biến count. Do đó, ta sẽ cần có 1 Publisher cho count này.

Lựa chọn ở đây thì bạn sẽ phải chọn kiểu của Publisher thuộc 1 trong 2 kiểu sau:

  • CurrentValueSubject
    • Ưu điểm của nó khi có subscription tới thì subscriber sẽ có ngay dữ liệu liền
    • Hiệu quả khi lưu trữ dữ liệu
    • Cần cung cấp giá trị của Output ban đầu khi khai báo subject này
  • PassthroughSubject
    • Ưu điểm là có cái gì là ném cái đó đi. Chứ không quan tâm phải gởi dữ liệu ngay từ đầu cho subscriber
    • Hiệu quả trong việc gởi các sự kiện hoặc call back
    • Không cần cung cấp giá trị ban đầu cho việc khai báo subject này

Vì luồng dữ liệu của ta là luồng dữ liệu bất đồng bộ, ta không biết thời điểm nào người dùng bấm vào 2 button đó. Cho nên giá trị count cũng không biết thời điểm nào sẽ tăng hay giảm. Và Subject là lựa chọn hàng đầu. Nó có nhiều ưu điểm như sau:

  • Vừa lưu trữ được dữ liệu và vừa phát dữ liệu đi được
  • Kết nối được phần Combine code với Non-Combine code
  • Có thể phát đi các giá trị theo mong muốn
  • Nhiều subscriber có thể subscribe tới nó
  • Tự do trong việc sử dụng kiểu dữ liệu cho Output
  • Handle Error một cách dễ dàng

Tiếp tục với file HomeViewController.swift, thêm 1 property sau:

var countPublisher = CurrentValueSubject<Int, Never>(0)

Trong đó:

  • Output : Int
  • Failure : Never

Thử xem cách dùng của nó như thế nào?

  • Cách 1: set lại giá trị cho thuộc tính value của Subject
countPublisher.value = 1
  • Cách 2: send giá trị mới
let newValue = countPublisher.value + 1
countPublisher.send(newValue)

Tiếp tục là vấn đề mới, bạn để code send dữ liệu của Publsher ở đâu?. Việc này thì càng dễ hơn. Nơi nào có sự kiện thì tại đó sẽ thực hiện việc phát dữ kiệu đi. Có 2 loại sự kiện:

  • Sự kiện người dùng, thông qua các IBAction
  • Sự thay đổi trạng thái của các View, thông qua Delegate & Datasource của các View đó

Trong bài ví dụ, ta sẽ tăng và giảm giá trị của count  một cách đơn giản như sau:

  @IBAction func increase(_ sender: Any) {
    countPublisher.value += 1
    
  }
  
  @IBAction func reduce(_ sender: Any) {
    countPublisher.value -= 1
  }

Để kiểm chứng thì ta thử subscription như trước đây. Vào function viewDidLoad thêm đoạn code này vào:

countPublisher
      .print("Publisher:" )
      .sink(receiveCompletion: { (completion) in
          print(completion)
        }) { (value) in
          print(value)
        }
      .store(in: &subscriptions)

Build project và nhấn test thử, ta sẽ đc kết quả như sau được in ra ở console

Publisher:: receive subscription: (CurrentValueSubject)
Publisher:: request unlimited
Publisher:: receive value: (0)
0
Publisher:: receive value: (1)
1
Publisher:: receive value: (2)
2
Publisher:: receive value: (3)
3
Publisher:: receive value: (4)
4
Publisher:: receive value: (5)
5
Publisher:: receive value: (6)
6
Publisher:: receive value: (7)
7
Publisher:: receive value: (8)
8
Publisher:: receive value: (9)
9
Publisher:: receive value: (10)
10
...

Toán tử print trong Publisher sẽ giúp bạn debug được dữ liệu hay sự kiện mà liên quan tới Publisher đó.

Chờ một chút …

Tới đây, nhiều bạn sẽ liên tưởng tới RxSwift hay họ hàng nhà Rx. Là nó có button.tag , còn Combine sao không có? Việc gì phải dùng nó ở IBAction?

Phần này mình xin phép không lý giải. Nhưng bạn phải hiểu được là bạn đang sử dụng Combine trên nên tảng nào. Với UIKit, thì để UIKit quản lý các action của nó, còn Combine giúp bạn biến đổi phần còn lại của thế giới hỗn tạp này.

Nếu bạn vẫn còn thấy ấm ức, thì hãy đợi vài phần nữa, mình sẽ tới bài SwiftUI , khi đó mọi thứ sẽ sáng tỏ.

4. Subscribe

Chúng ta đã có nguồn phát dữ liệu và thấy được dữ liệu thay đổi như thế nào sau mỗi lần có sự kiện người dùng tác động lên UI. Tiếp theo công việc là đưa nó lên giao diện.

Bạn sử dụng tiếp đoạn code trên và tiến hành gán giá trị cho IBOutlet của ViewController.

override func viewDidLoad() {
    super.viewDidLoad()
    
    countPublisher
      .print("Publisher:" )
      .sink(receiveCompletion: { completion in
          print(completion)
        
        }) { value in
          self.counterLabel.text = "\(value)"
          
        }
      .store(in: &subscriptions)
  }

Build và test chương trình của chúng ta hoạt động ra sao. Ta thấy:

  • 2 IBAction chỉ có trách nhiệm xét lại giá trị
  • Phần UI sẽ hiển thị dựa theo dữ liệu mà Publisher phát đi

Đó chính là nguyên lý đầu tiên khi sử dụng Combine trong UIKit. Ta tiến hành subscribe tới các Publisher và công việc này sẽ khai báo tất cả (subscriptions) tại lúc ViewController được khởi tạo (đó là viewDidLoad).

Khi người dùng có hành động, ta chỉ cần thay đổi dữ liệu từ nguồn phát và không quan tâm gì thêm. Mọi thứ còn lại sẽ dựa vào đó mà biến đổi tiếp.

Quay lại ví dụ code của chúng ta. Về bản chất hoạt động khi subscribe thì đã OKE rồi. Tuy nhiên, bạn nhận thấy công việc update dữ liệu lên UI được thực hiện ở closure của sink , để biến đổi giá trị Int thành String và sau đó là phép gán lên thuộc tính text của UILabel. Chúng ta cần nâng cấp một chút nữa.

Xoá đoạn code trên và chỉnh sửa một số chỗ như sau.

  • Thay đổi lại Output của Publisher và thêm 1 property để lưu trữ giá trị
var countPublisher = CurrentValueSubject<String?, Never>("0")

var count = 0
  • Update lại subscribe tại viewDidLoad
override func viewDidLoad() {
    super.viewDidLoad()
    
    countPublisher
      .assign(to: \.text, on: self.counterLabel)
      .store(in: &subscriptions)
    
  }
  • Edit lại chỗ thay đổi dữ liệu và phát dữ liệu đi bằng Publisher tại các function IBAction
  @IBAction func increase(_ sender: Any) {
    count += 1
    countPublisher.send("\(count)")
    
  }
  
  @IBAction func reduce(_ sender: Any) {
    count -= 1
    countPublisher.send("\(count)")
  }

OKAY, run project và test lại xem ntn. Mình giải thích 1 chút

  • Dùng assign vì chúng ta cần đưa dữ liệu trực tiếp từ giá trị mà Publisher phát đi tới UI Control
  • 2 đầu này phải cùng kiểu dữ liệu
    • Output của Publisher phải là String?
    • text của UILabel thì đương nhiên đã là String?
  • Edit lại cách phát dữ liệu, thay vì Int như lúc nãy mà bây giờ là String
  • assign để đưa dữ liệu nhận được tới thẳng proprety của đối tượng

Đây là cách binding dữ liệu lên View.

Một điều nữa bạn cần chú ý khi sử dụng assign là Failure của Publisher phải là Never.

5. Operators

Tới đây, chắc nhiều bạn thắc mắc tiếp là Combine đâu có gì hay đâu. Cũng bình thường và cũng phải tốn nhiều property của ViewController mới hoạt động được. Vâng, chúng ta lại nâng cấp tiếp chương trình của mình.

Quay về đoạn code trước khi thay đổi Output của Publisher từ Int thành String?. Và chỉnh sửa phần subscribe như sau:

countPublisher
      .map { "\($0)"}
      .assign(to: \.text, on: self.counterLabel)
      .store(in: &subscriptions)

Build project và test lại xem ViewController của chúng ta đã hoạt động được hay không.

Bạn cũng biết món ăn ngon được thì phải cần chế biến nguyên liệu, sau đó là nấu. Quá trình đó biến thịt, cá .. thành các món đồ ăn thơm ngon bổ dưỡng. Nhân câu chuyện đó thì mình muốn nhắn gởi là:

Phải có biến đổi dữ liệu và sẽ biến đổi nhiều lần để được dữ liệu mà mình mong muốn.

Xem đoạn code trên bạn sẽ thấy toán tử map. Nó thuộc họ hành nhà Transforming Operators, chuyên đi biến đổi dữ liệu. Trong đoạn code trên, khi subject phát đi 1 int, nhưng chúng ta cần chính là 1 cái string mà thôi. Nên sẽ viết hàm biến đổi dữ liệu tại đó.

Vì vậy, Operators là thành phần quan trọng tiếp theo trong Combine cũng đã được sử dụng vào trong ví dụ cực kì cơ bản này rồi.

6. Handle Events

Ở trên là thao tác trên luồng dữ liệu. Tất nhiên, bạn còn phải thao tác với luồng sự kiện nữa. Và trong ứng dụng, khi bạn tác động tới 1 UI Control trên giao diện, thì có thể hành động đó của bạn sẽ gây ảnh hưởng tới rất nhiều UI Control khác.

Vì vậy, cần phải nắm được thời cơ mà dữ liệu có sự thay đổi. Thời cơ đó chính là lúc:

Subscriber nhận được giá trị mà phát ra bởi Publisher.

Chúng ta sử dụng toán tử handleEvents của Publisher. Chúng ta có thể bắt được các loại sự kiện phát ra khi có sự thay đổi trên nguồn phát như sau:

  • receiveSubscription
  • receiveOutput
  • receiveCompletion
  • receiveCancel
  • receiveRequest

Quay lại với ví dụ của chúng ta. Edit lại đoạn code subscribe ở viewDidLoad

countPublisher
      .handleEvents(receiveOutput: { [weak self] value in
        // Viết code ở đây
        // hoặc
        // Gọi các function khác ở đây

        // ví dụ nha
        self?.view.backgroundColor = (value % 2 == 0) ? UIColor.white : UIColor.lightGray
      })
      .map { "\($0)"}
      .assign(to: \.text, on: self.counterLabel)
      .store(in: &subscriptions)

Chú ý bạn nên sử dụng weak self để chương trình mình đẹp hơn. Còn tại sao dùng nó thì mình sẽ có giải thích ở các bài sau.

 

OKAY. Tới đây thì mình xin kết thúc bài viết này. Và bạn bây giờ đã có đủ tự tin mà áp dụng Combine Code vào trong ViewController của bạn rồi. Mã nguồn của ví dụ demo thì bạn có thể download tại đây.

Tạm kết

  • Thực hiện binding dữ liệu lên View
  • Kết hợp với các thành phần cơ bản như IBOutlet & IBAction
  • Tương tác được với luồng dữ liệu & luồng sự kiện
  • Quản lý các subcriptions trong ViewController
  • Biến đổi dữ liệu bằng các Operators

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

FacebookTweetPinYummlyLinkedInPrintEmailShares2

Related Posts:

  • Flutter
    Cài đặt Flutter SDK & Hello world
  • SwiftData
    SwiftData - Hello world!
  • testing
    Hello Testing iOS
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 AI api AppDistribution autolayout basic ios tutorial blog ci/cd closure collectionview combine concurrency crashlytics dart dart basic dart tour Declarative delegate deploy design pattern fabric fastlane firebase flavor flutter GCD gradients iOS MVVM optional Prompt engineering protocol Python rxswift safearea Swift Swift 5.5 SwiftData SwiftUI SwiftUI Notes tableview testing TravisCI unittest

Recent Posts

  • Vibe Coding là gì?
  • Cách Đọc Sách Lập Trình Nhanh và Hiệu Quả Bằng GEN AI
  • Nỗ Lực – Hành Trình Kiến Tạo Ý Nghĩa Cuộc Sống
  • Ai Sẽ Là Người Fix Bug Khi AI Thống Trị Lập Trình?
  • Thời Đại Của “Dev Tay To” Đã Qua Chưa?
  • Prompt Engineering – Con Đường Để Trở Thành Một Nghề Nghiệp
  • Vấn đề Ảo Giác (hallucination) khi tương tác với Gen AI và cách khắc phục nó qua Prompt
  • Điều Gì Xảy Ra Nếu… Những Người Dệt Mã Trở Thành Những Người Bảo Vệ Cuối Cùng Của Sự Sáng Tạo?
  • Khi Cô Đơn Gặp Python
  • Học vì tồn tại

You may also like:

  • Hello Testing iOS
    testing
  • SwiftData - Hello world!
    SwiftData
  • Cài đặt Flutter SDK & Hello world
    Flutter

Archives

  • April 2025 (1)
  • March 2025 (8)
  • January 2025 (7)
  • December 2024 (4)
  • September 2024 (1)
  • July 2024 (1)
  • June 2024 (1)
  • May 2024 (4)
  • April 2024 (2)
  • March 2024 (5)
  • January 2024 (4)
  • 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 (43)
  • Code (10)
  • Combine (22)
  • Flutter & Dart (24)
  • iOS & Swift (101)
  • No Category (1)
  • RxSwift (37)
  • SwiftUI (80)
  • Tutorials (86)

Newsletter

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

    Copyright © 2025 Fx Studio - All rights reserved.