【Swift】CombineDataSourcesを使ってみた

# はじめに
UIKitもCombineで書きたいなと思って調べていたら、CombineCocoaやCombineDataSourcesなどを見つけました。
今回はCombineDataSourcesの基本的な使い方を紹介できたらと思います。

# データ
GitHubのリポジトリ検索APIを使用します。
Modelは全て一緒なのでここに書いておきます。
```swift: SearchRepositories
import Foundation

// MARK: - SearchRepositories
struct SearchRepositories: Codable, Hashable {
    var items: [Item]

    enum CodingKeys: String, CodingKey {
        case items
    }
}

// MARK: - Item
struct Item: Codable, Hashable {
    let name: String
    let owner: Owner
    let htmlURL: String
    let itemDescription: String?
    let languagesURL: String
    let stargazersCount: Int
    let language: String?
    let forksCount: Int
    let openIssuesCount: Int

    enum CodingKeys: String, CodingKey {
        case name
        case owner
        case htmlURL = "html_url"
        case itemDescription = "description"
        case languagesURL = "languages_url"
        case stargazersCount = "stargazers_count"
        case language
        case forksCount = "forks_count"
        case openIssuesCount = "open_issues_count"
    }
}

// MARK: - Owner
struct Owner: Codable, Hashable {
    let login: String
    let avatarURL: String
    let url: String
    let htmlURL: String

    enum CodingKeys: String, CodingKey {
        case login
        case avatarURL = "avatar_url"
        case url
        case htmlURL = "html_url"
    }
}
```

# 実装
## TableView(セクションなし)
![Simulator Screen Recording - iPhone 14 - 2022-11-22 at 15.59.09.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/b36d7a6e-74d6-f115-a9be-e4e887a65258.gif)
### View
```swift: ViewController
import UIKit
import SnapKit
import Combine
import CombineDataSources

class ViewController: UIViewController {

    private let tableView: UITableView = {
        let view = UITableView(frame: .zero, style: .grouped)
        view.register(UITableViewCell.self, forCellReuseIdentifier: "UITableViewCell")
        return view
    }()

    private let viewModel = ViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()
        setup()
        bind()
    }

    private func setup() {
        view.addSubview(tableView)

        tableView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
    }

    private func bind() {
        viewModel.data
            .bind(subscriber: tableView.rowsSubscriber(cellIdentifier: "UITableViewCell", cellType: UITableViewCell.self, cellConfig: { cell, indexPath, item in
                cell.textLabel?.text = item.name
            }))
            .store(in: &viewModel.subscriptions)
    }
}
```
### ViewModel
```swift: ViewModel
import Combine
import Foundation

final class ViewModel {
    private let url = "https://api.github.com/search/repositories?q=swift"

    var data = PassthroughSubject<[Item], Never>()

    var subscriptions = Set<AnyCancellable>()

    init() {
        fetchSearchRepositories()
    }

    private func fetchSearchRepositories() {
        URLSession.shared.dataTaskPublisher(for: URL(string: url)!)
            .tryMap { data, _ in return data }
            .decode(type: SearchRepositories.self, decoder: JSONDecoder())
            .receive(on: DispatchQueue.main)
            .sink { complete in
                print(complete)
            } receiveValue: { [weak self] response in
                guard let self else { return }
                self.data.send(response.items)
            }
            .store(in: &subscriptions)
    }
}
```

## TableView(セクションあり)
![Simulator Screen Recording - iPhone 14 - 2022-11-22 at 16.01.00.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/583e1a46-124e-eaae-bb02-abed054706ec.gif)
### View
```swift: ViewController
import UIKit
import SnapKit
import Combine
import CombineDataSources

class ViewController: UIViewController {

    private let tableView: UITableView = {
        let view = UITableView(frame: .zero, style: .grouped)
        view.register(UITableViewCell.self, forCellReuseIdentifier: "UITableViewCell")
        return view
    }()

    private let viewModel = ViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()
        setup()
        bind()
    }

    private func setup() {
        view.addSubview(tableView)

        tableView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
    }

    private func bind() {
        viewModel.data
            .bind(subscriber: tableView.sectionsSubscriber(cellIdentifier: "UITableViewCell", cellType: UITableViewCell.self, cellConfig: { cell, indexPath, item in
                cell.textLabel?.text = item.name
            }))
            .store(in: &viewModel.subscriptions)
    }
}
```

### ViewModel
```swift: ViewModel
import Combine
import CombineDataSources
import Foundation

final class ViewModel {
    private let url = "https://api.github.com/search/repositories?q=swift"

    var data = PassthroughSubject<[Section<Item>], Never>()

    var subscriptions = Set<AnyCancellable>()

    init() {
        fetchSearchRepositories()
    }

    private func fetchSearchRepositories() {
        URLSession.shared.dataTaskPublisher(for: URL(string: url)!)
            .tryMap { data, _ in return data }
            .decode(type: SearchRepositories.self, decoder: JSONDecoder())
            .receive(on: DispatchQueue.main)
            .sink { complete in
                print(complete)
            } receiveValue: { [weak self] response in
                guard let self else { return }
                self.data.send([Section(header: "ヘッダー", items: response.items)])
            }
            .store(in: &subscriptions)
    }
}
```

## CollectionView
![Simulator Screen Recording - iPhone 14 - 2022-11-22 at 16.07.10.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/7bbb4683-ef6f-57c1-b3f3-23a52f6b4a6b.gif)
### View
```swift: ViewController
import UIKit
import SnapKit
import Combine
import CombineDataSources

class ViewController: UIViewController {
    private let collectionView: UICollectionView = {
        let collectionViewLayout = UICollectionViewFlowLayout()
        collectionViewLayout.itemSize = .init(width: 100, height: 100)
        collectionViewLayout.sectionInset = .init(top: 10, left: 20, bottom: 10, right: 20)
        let view = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
        view.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "UICollectionViewCell")
        return view
    }()

    private let viewModel = ViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()
        setup()
        bind()
    }

    private func setup() {
        view.addSubview(collectionView)

        collectionView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
    }

    private func bind() {
        viewModel.data
            .bind(subscriber: collectionView.itemsSubscriber(cellIdentifier: "UICollectionViewCell", cellType: UICollectionViewCell.self, cellConfig: { cell, indexPath, item in
                var config = UIListContentConfiguration.cell()
                config.text = item.name
                config.secondaryText = item.owner.login
                cell.contentConfiguration = config
                cell.backgroundColor = .systemGray6
                cell.layer.cornerRadius = 10
            }))
            .store(in: &viewModel.subscriptions)
    }
}
```
### ViewModel
```swift: ViewModel
import Combine
import CombineDataSources
import Foundation

final class ViewModel {
    private let url = "https://api.github.com/search/repositories?q=swift"

    var data = PassthroughSubject<[Item], Never>()

    var subscriptions = Set<AnyCancellable>()

    init() {
        fetchSearchRepositories()
    }

    private func fetchSearchRepositories() {
        URLSession.shared.dataTaskPublisher(for: URL(string: url)!)
            .tryMap { data, _ in return data }
            .decode(type: SearchRepositories.self, decoder: JSONDecoder())
            .receive(on: DispatchQueue.main)
            .sink { complete in
                print(complete)
            } receiveValue: { [weak self] response in
                guard let self else { return }
                self.data.send(response.items)
            }
            .store(in: &subscriptions)
    }
}
```

# おわり
RxSwiftみたいな感じで使えるのでめっちゃいいです！
ただ、3年前から更新がないのでちょっと心配です。