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

Combine vs. UIKit – Navigation

Combine

Contents

  • Chuẩn bị
  • 1. Present view
  • 2. Talking to other ViewController
  • 3. Multiple subscriptions
  • 4. Binding
  • Tạm kết

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

Chúng ta lại tiếp tục với phần Combine vs. UIKit. Đọc qua cái tên bài, có thể bạn suy nghĩ đó là UINavigationController. Thì cái này không phải nha. Phần này sẽ tổng hợp các cách điều hướng View/ViewController cho các trường hợp chung nhất. Bên cạnh đó còn giải quyết vấn đề truyền tải dữ liệu giữa các View/ViewController.

Bắt đầu thôi!

Chuẩn bị

  • Xcode 11.0
  • Swift 5.1
  • iOS 13.0

Tiếp tục sử dụng lại project của bài trước để làm ví dụ demo code cho bài này. Ta thêm một màn hình SettingsViewController để:

  • Cập nhật lại số đếm một cách nhanh nhất
  • Truyền dữ liệu lại cho HomeViewController biết sự cập nhật

1. Present view

Bắt đầu bằng câu chuyện là hiển thị 1 Alert View để thông báo cho người dùng biết thông tin hay lỗi phát sinh trong quá trình sử dụng. Chúng ta giải quyết bài toán này bằng Combine nha, để cho có luồng gió mới.

Việc đầu tiên là ta cần 1 function và 1 Publisher. Đó là những gì cần thiết nhất. Mở file HomeViewController tiến hành khai báo 1 function như vậy

func alert(title: String, text: String?) -> AnyPublisher<Void, Never> {
    // ....
}

Bạn chú ý, đối tượng lần này chúng ta sử dụng là AnyPublisher. Với

  • Output là Void, thường sử dụng cho việc thông báo sự kiện
  • Failure là Never, không bao giờ có lỗi

Tiến hành implement code logic của UI Control

func alert(title: String, text: String?) -> AnyPublisher<Void, Never> {
    let alertVC = UIAlertController(title: title, message: text, preferredStyle: .alert)
    
  }

Vẫn không có thay đổi chi mới ở đây, bạn có thể tạo ra các View khác. Update các property của chúng … trước khi sử dụng.

Giờ qua phần chính là Publisher trả về. Tiếp tục với đoạn code sau:

func alert(title: String, text: String?) -> AnyPublisher<Void, Never> {
    let alertVC = UIAlertController(title: title, message: text, preferredStyle: .alert)
    
    return Future { resolve in
      alertVC.addAction(UIAlertAction(title: "Close", style: .default, handler: { _ in
        resolve(.success(()))
      }))
      
      self.present(alertVC, animated: true, completion: nil)
    }
    .handleEvents(receiveCancel: {
      self.dismiss(animated: true, completion: nil)
    })
    .eraseToAnyPublisher()
  }

Tới đây thì phải căng não ra cho thẳng, nếu không đọc code xí là xoắn não liền.

  • Sử dụng Future để đảm bảo việc phát ra dữ liệu ở tương lai thực thi.
  • Output của Future là 1 closue, ta sẽ coding vào đó
  • Bạn sẽ hiển thị  Alert ở closure đó, bên cạnh đó bạn cài đặt cho button của Alert ở closure với việc phát đi success
  • Sử dụng toán tử handleEvents với tham số receiveCancel. Có nghĩa khi kết thúc Future thì sẽ dismiss alert.
  • Cuối cùng, quan trọng nhất là eraseToAnyPublisher –> xoá đi dấu viết Future, biến nó thành 1 Publisher

Cuối cùng là việc sử dụng nó. Vì return về là Publisher nên cần phải subscribe để dùng.

self.alert(title: "Error", text: error.localizedDescription)
            .sink { _ in
              // tự sướng trong này
          }
          .store(in: &self.subscriptions)

Thực sự thì trong trường hợp hiển thị Alert này, cũng không cần thiết phải return về Publisher. Tuy nhiên, với các trường hợp hiển thị các Custom View thì cũng nên cần.

Xem code sử dụng ở function lưu dữ liệu. Sau khi có call back trở về.

@objc func save() {
    DataManagement.share.save(value: self.countPublisher.value)
      .sink(receiveCompletion: { [unowned self] completion in
        if case .failure(let error) = completion {
          self.alert(title: "Error", text: error.localizedDescription)
            .sink { _ in
              // tự sướng trong này
          }
          .store(in: &self.subscriptions)
        }
        
        
      }) { [unowned self] id in
        print("SAVED SUCCESS!")
        self.alert(title: "HOME", text: "SAVED SUCCESS!").sink { _ in 
            // tự sướng trong này
        }.store(in: &self.subscriptions)
      }
      .store(in: &subscriptions)
    
  }

Mình sẽ hoàn thiện nó hơn ở 1 bài Custom View với Combine (ở thời gian nào đó). Chúng ta tiếp tục sang phần tiếp theo.

2. Talking to other ViewController

Bắt đầu, bằng việc cài đặt function để push một ViewController khác vào HomeViewController. Ta chọn BarButtonItem. Mở file HomeViewController, tại viewDidLoad thêm đoạn code sau:

let settingsBarButton = UIBarButtonItem(title: "Settings", style: .plain, target: self, action: #selector(gotoSettingsVC))
self.navigationItem.rightBarButtonItem = settingsBarButton

Thêm một function để điều hướng sang màn hình SettingsViewController.

@objc func gotoSettingsVC() {
    // vc
    let settingsVC = SettingsViewController()
    
    
    // push
    self.navigationController?.pushViewController(settingsVC, animated: true)
}

 

Chúng ta sẽ có vấn đề đầu tiên là truyền dữ liệu từ A sang B.

Tạm thời ta sẽ gọi A là HomeViewController và B là SettingsViewController. Bước đầu tiên là truyền dữ liệu từ A sang B. Trong ví dụ của mình thì TextField của mình sẽ hiện thị giá trị count ở HomeViewController.

  • Thêm property count cho SettingsViewController
class SettingsViewController: UIViewController {
  
  @IBOutlet weak var countTexyField: UITextField!
  
  var count: Int = 0
  
  override func viewDidLoad() {
    super.viewDidLoad()
    countTexyField.text = "\(count)"
  }
  
  @IBAction func done(_ sender: Any) {
  }
  
}
  • Truyền dữ liệu A sáng B
@objc func gotoSettingsVC() {
    // vc
    let settingsVC = SettingsViewController()
    settingsVC.count = countPublisher.value
    
    // push
    self.navigationController?.pushViewController(settingsVC, animated: true)
  }

Giải quyết cái này khá là đơn giản, bạn chỉ cần gán các giá trị cho các thuộc tính của B. Hoặc gọi function setup/config data cho B với đối số là các giá trị mà bạn muốn truyền từ A sang B.

Vì đối tượng B được tạo ở function của đối tượng A.

 

Mọi việc sẽ phức tạp khi chiều truyền dữ liệu là từ B sang A.

Trước đây, để giải quyết vấn đề này chúng ta sử dụng con trỏ.

Tạo con trỏ A trong class B.

Nói cho nó sang chãnh vậy, chứ Swift thì dùng Protocol thôi. Hay các delegate và datasouce mà lâu nay chúng ta đã dùng. Rồi chúng ta tiến hoá lên việc call back bằng closure.

Vâng, tất cả vẫn là Non-Combine code. Giờ chúng ta đang ở trong thời đại mới rồi. Nên phải sử dụng được Combine Code vào để giải quyết vấn đề truyền dữ liệu này.

 

Giải pháp như thế nào?

Theo tư tưởng của Combine thì:

B sẽ là publisher và A sẽ là subscriber

Tư tưởng là như vậy. Nhưng không đời nào lại đi biến cả 1 UIViewController thành 1 publisher. Quá nhiều thứ dư thừa. Chúng ta chỉ cần cài đặt cho dữ liệu nào cần thiết mà thôi. Cụ thể là chúng ta tạo ra các các property là các publisher.

Quay lại ví dụ giả tưởng trên, ta khai báo thêm các đoạn code sau trong class B:

  var countPublisher = PassthroughSubject<Int, Never>()
  
  var count: Int = 0 {
    didSet {
      countPublisher.send(count)
    }
  }

Giải thích:

  • countPublisher dùng để phát dữ liệu đi. Nó là 1 subject
  • Khi có dữ liệu mới được gán cho count thì sử dụng countPublisher để phát đi.
  • Ngoài ra, bạn có thể lượt bỏ đi biến count , vì subject cũng có khả năng lưu trữ dữ liệu. Chính là thuộc tính value của nó.

Phát dữ liệu đi ở B như sau:

@IBAction func done(_ sender: Any) {
    guard let value = Int(countTexyField.text ?? "0") else { return }
    count = value
    
    self.navigationController?.popViewController(animated: true)
  }

Vấn đề đơn giản là gán dữ liệu mới count. Vì theo trên thì didSet của count đã sử dụng publisher. Nên ta không cần cài đặt gì thêm ở đây nữa. Sau đó, thoát B để về A.

Khi không muốn gởi gì hết đi, kết thúc câu chuyện tình này thì code như sau:

countPublisher.send(completion: .finished)

 

Nhận dữ liệu như thế nào?

Còn 1/2 câu chuyện nữa cần giải quyết. Tới đây thì bạn quay lại function gotoSettingsVC tại A. Việc tiếp theo là bạn phải subscribe tới publisher của B. Xem tiếp ví dụ code:

  @objc func gotoSettingsVC() {
    // vc
    let settingsVC = SettingsViewController()
    settingsVC.count = countPublisher.value
    
    // publisher
    let publisher = settingsVC.countPublisher
    
    // subscriptions
    publisher
      .sink { value in
        self.countPublisher.value = value
    }
    .store(in: &subscriptions)
    
    
    // push
    self.navigationController?.pushViewController(settingsVC, animated: true)
  }

Bạn sẽ thấy:

  • Tạo 1 đối tượng tham chiếu tới publisher của B
  • Nếu cần biến đổi dữ liệu thì sử dụng map
  • Sau đó là subscribe , có thể dùng assign hoặc sink
  • Cuối cùng là lưu trữ subscription

Cài đặt trước các hành động, mọi thứ sẽ phản ứng lại đúng như ý đồ của chúng ta. Đó là tư tưởng code của Combine để giải quyết vấn đề này.

 

3. Multiple subscriptions

Bạn hay nghe câu nói:

Đời không như là mơ.

Thì đời cũng như code vậy. Nó không đơn giản khi mỗi function thực hiện 1 nhiệm vụ. Hay 1 ViewController chỉ cần giải quyết 1 vấn đề. Mà đôi khi từ 1 dữ liệu chung, bạn cần phải giải quyết nhiều việc nữa. Quay về câu chuyện tình giữa A và B.

Ta xem đoạn code mới của function gotoSettingsVC này:
  @objc func gotoSettingsVC() {
    // vc
    let settingsVC = SettingsViewController()
    settingsVC.count = countPublisher.value
    
    // publisher
    let publisher = settingsVC.countPublisher
    
    // subscription 2
    publisher
      .sink { value in
        self.countPublisher.value = value
    }
    .store(in: &subscriptions)
    
    // subscription 2
    publisher
      .map { "\($0)" }
      .assign(to: \.text, on: self.counterLabel)
      .store(in: &subscriptions)
    
    
    // push
    self.navigationController?.pushViewController(settingsVC, animated: true)
  }

Bạn dễ dàng thấy được có tới 2 subscription tới cùng 1 publisher để thực hiện 2 việc.

  • Lấy giá trị của nó
  • Biến đổi giá trị của nó

Bạn có đảm bảo được tính toàn vẹn của các Publisher đó hay không?

Chúng ta đều biết các Transforming Operators, giúp biến đổi Publisher để đạt cái mà mình mong muốn. Nên rất có thể sau 1 vài operator thì Publisher của mình đã không còn như trước nữa.

Quay lại function trên, ta edit đoạn này:

let publisher = settingsVC.countPublisher.share()

Toán tử share() có đề cập trong phần operator. Sử dụng khi có nhiều subscription tới cùng 1 publisher và cũng cùng trỏ tới publisher gốc. Giúp publisher gốc khi emit dữ liệu cho nhiều subscriber thì dữ liệu được giở đi an toàn hơn.

4. Binding

Ở bài trước, chúng ta đã tìm hiểu về call back. Phần này sẽ là binding. Dành cho bạn nào chưa hiểu lắm thì cứ hiểu đơn giản như sau:

Binding là việc ràng buộc 2 đối tượng tượng với nhau. Khi dữ liệu của đối tượng này thay đổi thì đối tượng kia cũng sẽ thay đổi theo.

@Published

Thật là không sai khi nói Apple đã có âm mưu ngay từ đầu. Và thật là tinh tế, khi đã gài và cài cắm Combine code và trong các framework truyền thống. Mà quan trọng là nó không thay đổi gì nhiều code cũ.

Có 2 từ khoá mới:

  • @Published
  • @ObservedObject

Đã được khai sinh ra vào bạn có thể dùng trong bất class/struct/enum nào cũng được. Trong phần này chúng ta chỉ tìm hiểu về @Published thôi. Vậy nó là gì?

  • Cách đơn giản nhất và nhanh nhất để bạn tạo ra 1 property là publisher
  • Không ảnh hưởng gì tới code của class chứa nó, chỉ khai báo thêm từ khoá @Publisher phía trước
  • Với Output là cùng kiểu dữ liệu với property đó. Và không bao giờ có lỗi.
  • Vừa lưu trữ được giá trị và phát đi được giá trị
  • Real-time, bất cứ khi nào bạn thay đổi giá trị thì đồng thời nó sẽ phát đi giá trị đó cho các subscriber
  • private hay public để được
  • Phải yêu cầu có giá trị lúc khai báo

Ví dụ cú pháp khai báo:

struct Person {
  @Published var age: Int = 0
}

Sử dụng thì

var person = Person()

person.$age
	.sink { ... }
	.store(...)

Thêm toán tử dấu $ để truy cập tới nó.

 

Áp dụng vào cái ví dụ trên

Ở class B, chỉnh sửa lại một chút như sau:

import UIKit
import Combine

class SettingsViewController: UIViewController {
  
  @IBOutlet weak var countTexyField: UITextField!
  
//  var countPublisher = PassthroughSubject<Int, Never>()
//
//  var count: Int = 0 {
//    didSet {
//      countPublisher.send(count)
//    }
//  }
  
  @Published var count: Int = 0
  
  
  override func viewDidLoad() {
    super.viewDidLoad()
    countTexyField.text = "\(count)"
  }
  
  @IBAction func done(_ sender: Any) {
    guard let value = Int(countTexyField.text ?? "0") else { return }
    count = value
    
    self.navigationController?.popViewController(animated: true)
  }
  
}

Trong đó:

  • Xoá đi countPublisher
  • Thêm @Published vào trước khai báo biến count

Mọi thứ trong B vẫn giữ nguyên như cũ. Giờ sang A:

  • Xoá đi các subscription tới countPublisher , vì nó không còn nữa
  • Thêm subscription mới như sau
settingsVC.$count
      .sink { value in
        self.countPublisher.value = value
    }.store(in: &subscriptions)

Run project và test lại mọi thứ đã hoạt động đúng ý đồ chưa.

 

OKAY! Bạn đã đủ kiến thức để quản lý việc điều hướng và truyền tải dữ liệu giữa các View. Mình xin kết thúc bài viết này và bạn có thể download code demo tại đây.

Tạm kết

  • Sử dụng Future để handle việc hiển thị View trong việc điều hướng
  • Quản lý truyền dữ liệu giữa 2 view với Combine
  • Quản lý tương tác nhiều subscribe tới cùng 1 Publisher
  • Binding dữ liệu theo chiều ngược lại.

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

FacebookTweetPinYummlyLinkedInPrintEmailShares27
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

  • [Swift 6.2] Raw Identifiers – Đặt tên hàm có dấu cách, tại sao không?
  • 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

Archives

  • May 2025 (1)
  • 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 (11)
  • Combine (22)
  • Flutter & Dart (24)
  • iOS & Swift (102)
  • 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.