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 – Fetching Data from API

Combine

Contents

  • Chuẩn bị
  • 1. Objects
  • 2. Error
  • 3. Models
    • 3.1. EndPoint
    • 3.2. Properties
    • 3.3. Request
  • 4. Implements
  • Tạm kết

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

Bài viết này sẽ là phần tiếp theo của Combine vs. UIKit – Networking. Hướng dẫn việc tích hợp Combine Code vào trong iOS Project. Với yêu cầu chính là lấy dữ liệu từ API và hiển thị nó lên UI Control.

Nếu bạn chưa chuẩn bị kiến thức về tương tác Networking với Combine. Thì bạn nên đọc trước bài viết đó để bổ sung kiến thức. Còn bạn đã oke rồi …

Bắt đầu thôi!

Chuẩn bị

  • Xcode 11.0
  • Swift 5.1
  • iOS 13.0

Project sử dụng trong bài khá bình thường. Chỉ cần 1 UIViewController, với giao diện là 1 UITableView cơ bản. Việc sử dụng hay không sử dụng Storyboard thì không quan trọng.

1. Objects

Chúng ta sử dụng API lấy new feed của Apple, về các bài hát mới nhất trên iTunes.

https://rss.itunes.apple.com/api/v1/us/apple-music/coming-soon/all/100/aexplicit.json

Bạn nên cái các plug-in để hiển thị JSON trên trình duyệt của bạn. Bạn có thể quan sát cấu trúc JSON như sau:

Các phần mình đánh dấu là màu đỏ là các phần bạn cần lưu ý về name . Vì công việc bây giờ của bạn là tạo các class/struct để chứa dữ liệu từ API đó. Nói đúng hơn là cấu trúc của JSON sẽ ảnh hưởng tới các tên và kiểu giá trị các thuộc tính của class/struct đó.

Tạo mới một file có tên là Music.swift và tiến hành khai báo như sau:

struct Music: Codable {
  var name: String
  var id: String
  var artistName: String
  var artworkUrl100: String
}

struct MusicResults: Codable {
  var results: [Music]
  var updated: String
}

struct FeedResults: Codable {
  var feed: MusicResults
}

Trong đó:

  • Lựa chọn struct để tiết kiệm đi việc khai báo thêm hàm init và chỉ dùng để lưu trữ dữ liệu. Không có biến đổi dữ liệu.
  • Các tên của các thuộc tính (như : feed, results, … ) trùng với các key trong cấu trúc JSON của API trả về
  • Codable để có thể map dữ liệu từ JSONDecoder thành đối tượng của struct một cách nhanh chóng

Chúng ta đã xong phần cấu trúc Objects dùng để chứa dữ liệu. Tuỳ thuộc vào API mà bạn tương tác, bạn sẽ có một cấu trúc khác.

Ngoài Codable thì chúng ta còn có 2 class khác:

  • Decodable
  • Encodable

Dễ hiểu thì thèn Codable chứa cả 2 thèn đó. Còn 2 thèn đó, mỗi thèn làm 1 nhiệm vụ riêng.

2. Error

Đây là phần bạn hay bỏ sót. Nhưng bạn sẽ ân hận khá nhiều. Nếu bạn không khai báo và sử dụng chúng trong lúc project của bạn còn sơ khai.

Tạo một file mới và đặt tên là APIError.swift. Tạm thời chúng ta sử dụng lại các case error như ở bài trước.

import Foundation

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"
     }
   }
 }

Trong đó:

  • Kế thừa lại protocol Error để nó là Error và tương thích với hệ thống
  • Các case là các trường hợp chúng ta cần quản lý. Chú ý 2 case
    • error(String) : dành cho việc muốn thông báo 1 lỗi với 1 string
    • unknown : nếu bị lỗi nhưng không xác định nó từ đâu ra
  • localizedDescription : giúp bạn có thể lấy được mô tả lỗi của từng trường hợp

3. Models

Bây giờ mới là phần chính của bài sau khi đã setup xong các đối tượng và error. Tạo một file tên là APIMusic.swift.

Đừng quên việc import Combine.

import Foundation
import Combine

struct APIMusic {
  
}

3.1. EndPoint

Trước tiên bạn phải xác định được URL và thông qua việc có được EndPoint. Và bất kì ai cũng muốn mình có 1 class/struct mà có thể bao quát nhiều trường hợp nhất có thể. EndPoint này cũng không ngoại lệ, khi mà thực tế trong 1 dự án thì có rất nhiều API cần phải xử lý. Nhưng chúng lại giống nhau khá nhiều.

Mở file APIMucis.swift thêm phần khai báo sau:

struct APIMusic {
  
  //MARK: EndPoint
  enum EndPoint {
    static let baseURL = URL(string: "https://rss.itunes.apple.com/api/v1/us/apple-music")!
    
    case coming_soon(Int)
    
    var url: URL {
      switch self {
      case .coming_soon(let limit):
        return EndPoint.baseURL.appendingPathComponent("/coming-soon/all/\(limit)/aexplicit.json")
      }
    }
    
  }
  
}

Chúng ta sử dụng trường hợp đầu tiên là coming_soon với 1 tham số là Int. Để lấy số lượng cần lấy từ API. Bạn có thể áp dụng tương tự cho nhiều link khác. Còn cách sử dụng thì như sau:

let url = EndPoint.coming_soon(100).url

3.2. Properties

Tiếp tục file APIMusic.swift, khai báo thêm các thuộc tính cần thiết.

//MARK: Properties
  private let decoder = JSONDecoder()

  private let apiQueue = DispatchQueue(label: "API", qos: .default, attributes: .concurrent)

Về decoder , dùng để map dữ liệu JSON thành các Object đã được định nghĩa. Class sử dụng ở đây là JSONDecoder. Bạn có thể sử dụng nhiều loại decoder khác.

Cái này khai báo thêm cho sinh động, chứ cũng không cần thiết mấy. Trừ khi bạn có ý đồ config riêng cho việc decode.

Về apiQueue thì ở phần trước mình không nhắc tới. Tuy nhiên, khi tương tác trong Project và để tránh ít va chạm xung đột với Main Thread. Thì việc tốt nhất là subscribe & receive ở một Thead khác.

3.3. Request

Tạo 1 function trong APIMusic để xử lý việc request lấy các bài hát mới nhất từ API.

 func comingSoon(limit: Int) -> AnyPublisher<FeedResults, APIError> {
  
  }

Vẫn là hình bóng cũ, với giá trị trả về là một Publisher. Trong đó

  • Output là FeedResults, trong đó có chứa nhiều dữ liệu mà chúng ta cần lấy
    • Array Music
    • Các thông tin bổ sung
  • Failure là APIError

Áp dụng kiến thức bài trước, sử dụng URLSession để tạo ra 1 DataTaskPublisher. Bạn thêm việc subscribe ở apiQueue vừa tạo ở trên.

 func comingSoon(limit: Int) -> AnyPublisher<[Music], APIError> {
    return URLSession.shared
      .dataTaskPublisher(for: EndPoint.coming_soon(limit).url)
      .subscribe(on: apiQueue)
      .........
      .eraseToAnyPublisher()
  }

Tiếp tục là việc phân tích response. Nếu như không đúng statusCode thì nén ra 1 error. Vâng, bạn có thể làm nhiều thứ ở đây, trước khi sử dụng tới data của response.

Thêm đoạn code sau vào function của mình với tryMap.

func comingSoon(limit: Int) -> AnyPublisher<FeedResults, APIError> {
    return URLSession.shared
      .dataTaskPublisher(for: EndPoint.coming_soon(limit).url)
      .subscribe(on: apiQueue)
      .tryMap { output in
          guard let response = output.response as? HTTPURLResponse, response.statusCode == 200 else {
            throw APIError.invalidResponse
          }
          return output.data
      }
      .....
      .eraseToAnyPublisher()
  }

Công dụng của tryMap thì giups bạn biến đổi và kèm theo đó là nén được error ra. Tiếp tục với decode và mapError để hoàn tất function của chúng ta.

func comingSoon(limit: Int) -> AnyPublisher<FeedResults, APIError> {
    return URLSession.shared
      .dataTaskPublisher(for: EndPoint.coming_soon(limit).url)
      .subscribe(on: apiQueue)
      .tryMap { output in
          guard let response = output.response as? HTTPURLResponse, response.statusCode == 200 else {
            throw APIError.invalidResponse
          }
          return output.data
      }
      .decode(type: FeedResults.self, decoder: decoder)
      .mapError { error -> APIError in
            switch error {
            case is URLError:
              return .errorURL
            case is DecodingError:
              return .errorParsing
            default:
              return error as? APIError ?? .unknown
            }
          }
      .eraseToAnyPublisher()
  }

Về 2 phần decode và mapError, mình đã giải thích ở bài trước rồi. Kết thúc funtion này và chuyển sang phần sử dụng.

4. Implements

Bạn tạo 1 file UIViewController, với giao diện là 1 UITableView. Đặt tên dễ thương là MusicsViewController. Và tiến hành cài đặt đơn giản như sau:

import UIKit
import Combine

class MusicsViewController: UIViewController {
  
  // Outlets
  @IBOutlet weak var tableView: UITableView!
  
  // Properties
  var musics: [Music] = [] {
    didSet {
      self.tableView.reloadData()
    }
  }
      
  private var subscriptions = Set<AnyCancellable>()
  
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    // Title
    title = "Musics"
    
    // TableView
    tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
    tableView.delegate = self
    tableView.dataSource = self
  }
  
}

// UITableView Delegate & DataSource
extension MusicsViewController: UITableViewDelegate, UITableViewDataSource {
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    musics.count
  }
  
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
    
    let item = musics[indexPath.row]
    cell.textLabel?.text = item.name
    
    return cell
  }
}

Chú ý:

  • musics dùng để lưu trữ dữ liệu nhận được từ API
  • didSet của musics dùng để reload lại TableView sau khi được gán dữ liệu
  • extension cơ bản cho delegate & dataSource của UITableView
  • subscriptions dùng để lưu trữ các subscribe phát sinh trong ViewController. Và nó mang chức năng tự huỹ để đảm bảo bộ nhớ được giải phóng.

Bây giờ, chúng ta thêm 1 property của APIMusic

private var api = APIMusic()

Tại function viewDidLoad, thêm phần fetch dữ liệu và tiến hành subscribe như sau:

// fetch
    api.comingSoon(limit: 100)
      .receive(on: DispatchQueue.main)
      .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            break
        case .failure(let error):
            fatalError(error.localizedDescription)
        }
      }) { apiResult in
        self.musics = apiResult.feed.results
      }.store(in: &subscriptions)

Giải thích:

  • receive đảm bảo việc nhận được dữ liệu tại ViewController sẽ ở Main Thread, nhằm update UI
  • Subscribe bằng sink để xử lý được cả 2 trường hợp
    • Có dữ liệu
    • Completion với finished & failure
    • Tạm thời trong ví dụ chúng ta không update UI với trường hợp có lỗi xãy ra.
  • Tại phần nhận được dữ liệu, chúng ta gán dữ liệu cần thiết cho thuộc tính musics của ViewController.

Tới đây bạn run project và tận hưởng kết quả nhận được.

Chưa hết!

Mình sẽ bổ sung thêm một cách ngầu hơn để gọi API. Bạn sử dụng assign để binding dữ liệu nhận được tới thẳng thuộc tính musics của ViewController. Thay đoạn code sink trên bằng đoạn code sau.

  • map khi bạn chỉ muốn sử dụng Array Music làm Output mới. Và nó trùng với kiểu dữ liệu của musics.
  • catch để bắt Error. Trường hợp này thì thay Error bằng 1 array rỗng (được gọi là giá trị mặc định)
api.comingSoon(limit: 100)
      .receive(on: DispatchQueue.main)
      .map { $0.feed.results }
      .catch{ _ in Empty() }
      .assign(to: \.musics, on: self)
      .store(in: &subscriptions)

Muốn sử dụng assign thì bạn phải triệt tiêu hết các error.

Run project và kiểm tra nó đã hoạt động đúng như mong muốn hay chưa. Nếu hiển thị dữ liệu nhận được lên TableView, thì chúc mừng bạn đã thành công. Còn nếu không thì bạn bình tĩnh debug nó. EZ game!

 

OKAY! Bạn đã đủ kiến thức để có thể lấy dữ liệu từ một API bằng Combine Code rồi đó. Và 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

  • Áp dụng kiến thức tương tác Networking vào iOS Project với Combine Code
  • Thiết kế các đối tượng theo cấu trúc JSON của API
  • Thiết kế và quản lý Error
  • Tạo class/struct cho một Model API, với các thành phần cơ bản (như EndPoint, queue, request …)
  • Tích hợp vào ViewController với 2 kiểu subscribe

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

 

FacebookTweetPinYummlyLinkedInPrintEmailShares68

Related Posts:

  • dart
    Data Type - Dart Tour
  • API Testing
    API Testing (UnitTest) with OHHTTPStubs
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

  • Role-playing vs. Persona-based Prompting
  • [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?

You may also like:

  • Data Type - Dart Tour
    dart
  • API Testing (UnitTest) with OHHTTPStubs
    API Testing

Archives

  • May 2025 (2)
  • 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 (44)
  • Code (11)
  • Combine (22)
  • Flutter & Dart (24)
  • iOS & Swift (102)
  • No Category (1)
  • RxSwift (37)
  • SwiftUI (80)
  • Tutorials (87)

Newsletter

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

    Copyright © 2025 Fx Studio - All rights reserved.