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

Combine vs. UIKit – Networking

Combine

Contents

  • Chuẩn bị
  • 1. Cách truyền thống
  • 2. Data task with Combine
  • 3. Error handling
  • 4. Assign result to property
  • 5. Group multiple requests
  • 6. Multiple subscribers
  • 7. Function for Networking
  • Tạm kết:

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

Chúng ta lại tiếp tục với series Combine vs. UIKit. Nội dung của bài này là về tương tác Networking trong UIKit, sử dụng Combine code để xử lý. Đây cũng là phần nhiều bạn quan tâm nhất. Ngoài ra, việc tương tác Networking cũng là phần quan trọng nhất trong hầu hết các ứng dụng, nhất là các ứng dụng mạng xã hội.

Cũng không cần chuẩn bị gì nhiều …

Bắt đầu thôi!

Chuẩn bị

  • Xcode 11.0
  • Swift 5.1
  • Playground

Về nội dung demo chỉ cần sử dụng Playground là đủ rồi. Chúng ta sẽ áp dụng code của phần này vào iOS Project ở bài tiếp theo.

1. Cách truyền thống

Đầu tiên, chúng ta nhìn lại cách truyền thống khi tương tác với 1 API như sau:

//somewhere in viewDidLoad
let url = URL(string: "abc.xyz.com")!

let task = URLSession.shared.dataTask(with: url) { data, response, error in
    if let error = error {
        fatalError("Error: \(error.localizedDescription)")
    }
    guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
        fatalError("Error: invalid HTTP response code")
    }
    guard let data = data else {
        fatalError("Error: missing response data")
    }

    do {
        let decoder = JSONDecoder()
        let posts = try decoder.decode([Post].self, from: data)
        print(posts.map { $0.title })
    }
    catch {
        print("Error: \(error.localizedDescription)")
    }
}
task.resume()

Bạn sẽ thấy khi tương tác với Networking, chúng ta cần 1 EndPoint. Sau đó biến đổi nó thành một đối tượng URL. Sử dụng đối tượng URLSession. Tạo ra 1 đối tượng dataTask  để thực hiện việc connect & parse khi nhận được response.

Tất nhiên, các bước trên chỉ là setup. Sau cùng là connection. Dùng đối tượng dataTask để thực thi. Kết quả chính là response mà server trả về.

Trong việc parse data thì ta thấy 2 điểm cần quan tâm đó là decode & error handling.

Như vậy làm việc với Networking theo cách truyền thống, chúng ta cần quan tâm 2 thực thể chính:

  • URLSession
  • JSON

Qua bên Combine Framework thì sẽ như thế nào?

2. Data task with Combine

Nếu bạn theo dõi các bài Combine của Fx Studio thì cũng nghe nói tới thuyết âm mưu của Apple. Khi mà Apple đã gài gắm hết các Framework còn lại, đều có mặt Combine code trong đó. Và URLSession cũng không ngoại lệ.

Chúng ta có 1 extension của URLSession nhưng sau:

URLSession.shared.dataTaskPublisher(for: url)

Dòng code trên sẽ trả về cho bạn 1 Publisher. Ngoài tham số là URL thì còn có thêm URLRequest.

Cơ chế chung của quá trình này như sau:

  • Tạo thực thế cancellable
private var cancellable: AnyCancellable?
  • subscription tới Publisher của DataTask. Nôm na chia ra thành 2 phần:
    • Publisher & các setup cần thiết
      • Có được Publisher từ dataTaskPublisher
      • map để lấy phần data từ response trả về. Các dữ liệu khác không cần quan tâm
      • decode để từ JSON mà biến đổi thành đối tượng theo các class/struct đã định nghĩa
      • replaceError để handler các error. Trường hợp này nếu có lỗi thì trả về 1 array rỗng
      • eraseToAnyPublisher xoá sạch dấu vết để lại
    • Subscriber
      • Sử dụng sink để bạn muốn handler nhiều hơn
      • Sử dụng assign để bạn muốn binding dữ liệu trực tiếp lên thuộc tính của đối tượng
self.cancellable = URLSession.shared.dataTaskPublisher(for: url)
  .map { $0.data }
  .decode(type: [Post].self, decoder: JSONDecoder())
  .replaceError(with: [])
  .eraseToAnyPublisher()
  .sink(receiveValue: { posts in
      print(posts.count)
  })
  • Kết thúc cả quá trình
self.cancellable?.cancel()

Combine cung cấp cho chúng ta 1 operator là decode để biến đổi data json thành đối tượng. Quá tiện lợi!

3. Error handling

Đây là phần không thể thiếu. Với Combine thì có nhược điểm như thế này:

Các toán tử được gọi một cách liên tục với nhau. Bạn rất khó chen vào giữa để bắt các error phát sinh. Hoặc bạn không thể biết được Error nào là của đối tượng nào hay quá trình nào đã sinh ra.

Chúng ta có một số loại Error có thể phát sinh trong quá trình tương tác với Networking như sau:

  • Error mang ý nghĩa chung chung. Không xác định rõ thuộc đối tượng nào.
  • URLError là error liên quan tới các URL và việc connection của URLSession
  • JSONDecoder error liên quan tới việc phân tích dữ liệu nhận được
  • Custom Error là phần chúng ta tự định nghĩa thêm cho project của mình

Trước tiên bạn định nghĩa một enum Error cho riêng bạn

  enum APIError: Error {
    case error(String)
    case errorURL
    case invalidResponse
    case errorParsing
    case unknown
    
    var localizedDescription: String {
      switch self {
      case .error(let string):
        return string
      case .errorURL:
        return "URL String is error."
      case .invalidResponse:
        return "Invalid response"
      case .errorParsing:
        return "Failed parsing response from server"
      case .unknown:
        return "An unknown error occurred"
      }
    }
  }

Ví dụ trên chỉ là cá nhân của mình. Bạn có thể tự custom lại theo project của bạn.

Theo như trên thì phần tạo Publisher thì sẽ không thay đổi gì

self.cancellable = URLSession.shared.dataTaskPublisher(for: url)

Tuy nhiên, chúng ta sẽ không sử dụng map mà thay vào đó là tryMap. Để phân tích thêm dữ liệu của response có ổn hay không.

self.cancellable = URLSession.shared.dataTaskPublisher(for: url)
.tryMap { output in
    guard let response = output.response as? HTTPURLResponse, response.statusCode == 200 else {
        throw APIError.statusCode
    }
    return output.data
}
.......

Trong đó:

  • Chính xác là vẫn trả về data
  • Nếu có lỗi (statusCode khác 200) thì sẽ throw ra 1 Error.

Bản chất của tryMap chính là nó có thể nén ra 1 error.

Công việc tiếp theo sau khi tryMap là decode. Xem tiếp code ví dụ sau:

self.cancellable = URLSession.shared.dataTaskPublisher(for: url)
  .tryMap { output in
      guard let response = output.response as? HTTPURLResponse, response.statusCode == 200 else {
          throw APIError.statusCode
      }
      return output.data
  }
  .decode(type: [Post].self, decoder: JSONDecoder())
  .mapError { error -> APIError in
        switch error {
        case is URLError:
          return .errorURL
        case is DecodingError:
          return .errorParsing
        default:
          return error as? API.APIError ?? .unknown
        }
      }
  .eraseToAnyPublisher()

Nhưng lần này đi kèm với việc decode là mapError. Tại sao vậy?

Vì 2 toán tử tryMap & decode đều sinh ra Error. Và Error có từ nhiều loại khác nhau. mapError để biến chúng thành 1 loại Error mà ta định nghĩa.

Chốt chặn cuối cùng sẽ là ở việc subscribe. Xem tiếp code ví dụ sau:

.sink(receiveCompletion: { completion in
    switch completion {
    case .finished:
        break
    case .failure(let error):
        fatalError(error.localizedDescription)
    }
}, receiveValue: { posts in
    print(posts.count)
})

Bạn sẽ thấy:

  • Các error phát sinh thì sẽ làm publisher phát đi completion
  • Chúng ta phải phân tích trong completion
    • finished là thành công
    • failure là có lỗi

Toán tử sink sẽ giúp bạn quản lý phần dữ liệu nhận được với đầy đủ error và output.

Một chú ý thêm:

Các toán tử trên được sắp xếp theo ý đồ của mình. Bạn nên xem bài toán thực tế của bạn, để có thể chọn thêm hoặc lượt bỏ đi vài toán tử trong quá trình xử lý Networking.

4. Assign result to property

Sự linh hoạt của sink thì bạn đã thấy cách sử dụng như thế nào. Chúng ta hoàn toàn có thể sử dụng tuỳ ý vào bất kì đâu trong code. Bây giờ, chuyển sang assign thì sẽ như thế nào.

Bạn xem tiếp đoạn code ví dụ giả tưởng sau:

class ViewController: UIViewController {

    private var cancellable: AnyCancellable?

    private var posts: [Post] = [] {
        didSet {
            print("posts --> \(self.posts.count)")
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        let url = URL(string: "xxxxxxx")!

        self.cancellable = URLSession.shared.dataTaskPublisher(for: url)
        .map { $0.data }
        .decode(type: [Post].self, decoder: JSONDecoder())
        .replaceError(with: [])
        .eraseToAnyPublisher()
        .assign(to: \.posts, on: self)
    }
}

Chúng ta sử dụng 1 ViewController. Có 1 property là posts , dùng để lưu trữ dữ liệu nhận được. didSet sẽ chạy khi giá trị đã được gán cho thuộc tính. Có thể sử dụng để reload UI , như các TableView hay CollectionView ….

Tại viewDidLoad thì sẽ gọi connect API. Bạn chú ý toán tử assign. Nó sẽ đưa dữ liệu nhận được thẳng tới property posts của ViewController. Một điểm cần chú ý khi dùng assignlà

Không bao giờ có lỗi. Hoặc nếu có lỗi thì phải thay thế nó bằng một giá trị mặc định nào đó.

Trong ví dụ chúng ta dùng toán tử replaceError để thay thế error phát sinh bằng một array rỗng.

5. Group multiple requests

Thực tế, chúng ta phải gọi một lúc nhiều API trong cùng một ViewController. Cách giải quyết vấn đề này thì có rất nhiều cách. Lần này, mình giới thiệu một giải pháp đơn giản nhất.

Giả sử có 2 API liên quan tới Post và Todo. Chúng ta lại tạo các Publisher tương tứng như sau:

let url1 = URL(string: "posts")!
let url2 = URL(string: "todos")!

let publisher1 = URLSession.shared.dataTaskPublisher(for: url1)
.map { $0.data }
.decode(type: [Post].self, decoder: JSONDecoder())

let publisher2 = URLSession.shared.dataTaskPublisher(for: url2)
.map { $0.data }
.decode(type: [Todo].self, decoder: JSONDecoder())

Chọn phương pháp là Publishers.Zip để sử dụng việc nhóm các Publisher trên lại với nhau.

self.cancellable = Publishers.Zip(publisher1, publisher2)
.eraseToAnyPublisher()
.catch { _ in
    Just(([], []))
}
.sink(receiveValue: { posts, todos in
    print(posts.count)
    print(todos.count)
})

Ưu điểm việc này là khi nào nhận được đầy đủ dữ liệu của cả 2 Publisher, thì zip sẽ phát về 1 lúc. Nên yên tâm là lúc nhận được giá trị, thì có nghĩa mọi connect đã hoàn thành.

6. Multiple subscribers

Vấn đề tiếp theo chắc bạn cũng gặp nhiều lần rồi. Là có 1 cái link mà phải gọi đi gọi lại nhiều lần. Như vậy sẽ tốn tài nguyên và bộ nhớ của máy.

  • Toán tử share() có thể giải quyết nó. Nhưng bạn cần phải hoàn thành và setup xong tất cả các subscriber trước khi nhận được data. Điều này thì khá hên xui đó à.

Giải pháp ở đây sử dụng toán tử multicast

  • Lưu trữ lại
  • Tạo ra 1 subject ConnectablePublisher, để phát ra giá trị
  • Cho phép nhiều subscriber subscribe vào trước khi gọi connect

Xem code ví dụ sau:

  guard let url = URL(string: "xxxxxx") else { return }
  let publisher = URLSession.shared
    .dataTaskPublisher(for: url)
    .map(\.data)
    .multicast { PassthroughSubject<Data, URLError>() }
  

  //1
  publisher
    .sink(receiveCompletion: { completion in
      if case .failure(let err) = completion {
        print("Sink1 Retrieving data failed with error \(err)")
      }
    }, receiveValue: { object in
      print("Sink1 Retrieved object \(object)")
    })
    .store(in: &subscriptions)
  

  //2
  publisher
    .sink(receiveCompletion: { completion in
      if case .failure(let err) = completion {
        print("Sink2 Retrieving data failed with error \(err)")
      }
    }, receiveValue: { object in
      print("Sink2 Retrieved object \(object)")
    })
  .store(in: &subscriptions)
  
  // connect
  publisher.connect().store(in: &subscriptions)

Trong đó:

  • multicast sẽ biến đổi cái gì đó thành 1 subject với kiểu là PassthroughSubject<Data, URLError>
  • Tiến hành nhiều subscribe tới subject
  • connect sau khi đã setup xong hết các subscriber

7. Function for Networking

Các phần trên là nêu ra nhiều vấn đề bạn gặp phải trong quá trình xử lý tương tác Networking. Và bạn không thể khai báo hết tất cả trong 1 đoạn code, code chạy từ trên xuống dưới như thế… Cần phải tách function riêng ra, với mục đích thực hiện việc connect.

Việc đầu tiên bạn cần viết là 1 function với giá trị trả về là 1 Publisher.

func getPosts() -> AnyPublisher<[Post], Error> {

    let url = URL(string: "xxxxxx")!

    return URLSession.shared.dataTaskPublisher(for: url)
        .map { $0.data }
        .eraseToAnyPublisher()

}

Chỉ cần như trên là ổn rồi. Bạn có đầy đủ các thành phần cần thiết:

  • Return về 1 Publisher với
    • Output chính là kiểu dữ liệu bạn mong muốn nhận được
    • Error là kiểu lỗi của bạn, có thể dùng của hệ thống hoặc của riêng bạn
  • URL là cái cần thiết để trỏ tới server
  • Return về Publisher bằng toán tử dataTaskPublisher của URLSession
  • Phải xoá hết dấu vết bằng toán tử eraseToAnyPublisher

Tiếp theo là bạn thêm các xử lý khác đã trình bày tại mục Error Handling.

Và cuối cùng chọn 1 trong 2 cách subscribe (sink hay assign). Xem ví dụ với sink:

let publisher = getPosts()

publisher
  .sink(receiveCompletion: { completion in 
      switch completion { 
        case .finished: break 
        case .failure(let error): 
           fatalError(error.localizedDescription) 
      } 
   }, receiveValue: { posts in 
       print(posts.count) 
   })
   .store(in: &subscriptions)

Bạn thấy nó quen không nào. Vẫn như các cách subscribe ở các bài trước.

 

OKAY! Tới đây, mình xin kết thúc phần Networking với Combine. Tuỳ thuộc vào yêu cầu mà bạn nên sử dụng các toán tử sao cho hợp lý.

Tạm kết:

  • Cách tương tác Networking với URLSession bằng Combine Code
  • Quản lý Error
  • Parse dữ liệu nhận được
  • 2 cách subsription cho DataTaskPublisher
  • Nhóm các requests
  • Xử lý nhiều lần gọi cùng 1 API
  • Viết function cho việc tương tác API (đơn giản nhất). Tạo tiền đề cho việc viết Model.

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

FacebookTweetPinYummlyLinkedInPrintEmailShares24

Related Posts:

  • feature_bg_swiftui_7
    Tích hợp SwiftUI vào UIKit Project - SwiftUI Notes #13
  • feature_bg_swiftui_7
    Tích hợp UIView (UIKit) vào SwiftUI Project -…
  • feature_bg_swiftui_7
    Tích hợp UIViewController (UIKit) vào SwiftUI…
  • feature_bg_swift_9
    SwiftUI - Phần 3 : Tích hợp SwiftUI và UIKit
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:

  • Tích hợp UIView (UIKit) vào SwiftUI Project -…
    feature_bg_swiftui_7
  • SwiftUI - Phần 3 : Tích hợp SwiftUI và UIKit
    feature_bg_swift_9
  • Tích hợp UIViewController (UIKit) vào SwiftUI…
    feature_bg_swiftui_7
  • Tích hợp SwiftUI vào UIKit Project - SwiftUI Notes #13
    feature_bg_swiftui_7

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!