[iOS]APIKit + Combine + SwiftUIでGitHubリポジトリ検索


iOS13に出てきたSwiftUI・Combineと、ライブラリのAPIKitを使い、通信部分の実装手法を紹介したいと思います。
今回は、GitHub APIを使用します。

## どのようなアプリを作るか

以下のようなアプリを作ります。
リポジトリ名を入力し、結果を一覧で取得します。

<img width="40%" alt="apikit_02.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/297014/928ca8ad-e10d-341a-5532-8686d5078901.gif">


## 実装のポイント

- できるだけシンプル：冗長なコードはなるべく書かない
- 保守性がある：読みやすいコード
- 再利用性がある：別の異なるAPIでも少しの変更で利用が可能

## 開発環境・使用するライブラリなど

### 開発環境

- OS：10.15.6
- Xcode：10.6
- iOS：iOS13以降（iOS14では確認していません）

### 使用するライブラリ

[APIKit](https://github.com/ishkawa/APIKit/)
今回は、APIKitを拡張し、CombineのPublisherでAPIを叩いた結果を取得します。

### 使用するフレームワーク

- SwiftUI
- Combine

また、Swift標準のDecodableを使い、JSONをパースします。

### 使用する考え方

- リポジトリパターン：APIやDBにアクセスするための定義を抽象化

## 実装

### 全体像

![APIKit.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/297014/3dc4f080-cc25-b4b8-5ca0-f7ee859b7d4c.png)

- ContentView：UI表示
- GitHubSearchModel：ロジックを管理するクラス
- GitHubRepository：APIへのアクセス方法を定義

### API基本部分

API部分のクラス相関図は以下のようになります。

![APIKit-2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/297014/bfd1d936-a33d-b4ba-6d79-51336b0eb864.jpeg)


[GitHubRepository.swift](https://github.com/usk-lab/APIKitCombine/blob/master/APIKitCombine/API/GitHub/GitHubRepository.swift)については、中身は以下のようになっています。

```GitHubRepository.swift
import Foundation
import APIKit

class GitHubRepository {
    
    //GitHubレスポンス用のデコーダー
    static let decoder: GitHubDecoder = .init()
    
    //リポジトリを検索
    struct SearchRepositories: GitHubRequestProtocol {
        //https://developer.github.com/v3/search

        //検索クエリ
        let query: String
        
        let method: HTTPMethod = .get
        let path: String = "/search/repositories"
        var decoder: JSONDecoder {
            return GitHubRepository.decoder
        }
        
        var params: [String: Any] {
            return [
                "q": query
            ]
        }
        
        typealias Response = SearchResponse

    }
    
    private init() { }
}
```

`GitHubRepository`はリポジトリなので、APIアクセスに必要な情報だけを定義（抽象化）しています。
[SearchResponse](https://github.com/usk-lab/APIKitCombine/blob/master/APIKitCombine/API/GitHub/Response/SearchResponse.swift)はDecodableに準拠しています。
[GitHubRequestProtocol.swift](https://github.com/usk-lab/APIKitCombine/blob/master/APIKitCombine/API/GitHub/GitHubRequestProtocol.swift)と[GitHubDecoder.swift](https://github.com/usk-lab/APIKitCombine/blob/master/APIKitCombine/API/GitHub/GitHubDecoder.swift)と[APIDataParser.swift](https://github.com/usk-lab/APIKitCombine/blob/master/APIKitCombine/API/Base/APIDataParser.swift)はソースをご確認ください。


ここまでで、GitHub APIを叩くための準備ができました。

## Modelの実装

GitHub APIを使い、情報を取ってくるロジック（モデル）を実装します。
今回はViewとしてSwiftUIを使用するので、ロジックはObservableObjectを継承したクラスにします。

```GitHubSearchModel.swift
/// 検索モデル
class GitHubSearchModel: ObservableObject {

    @Published var items: [SearchItem] = [] //検索結果
・・・・
    /// 検索を行う
    func search() {
        debugPrint("search")
        
        // 1)APIリクエストを作成
        let request = GitHubRepository.SearchRepositories(query: searchText)
        
        // 2)CombineのPublisherを作成し、通信処理を行う
        self.requestCancellable = request.publisher //Publisherに変換（この時点で通信処理を行なっている）
            .receive(on: DispatchQueue.main) //メインスレッドで受け取る
            .sink(receiveCompletion: { result in
・・・・
            }, receiveValue: { [weak self] response in
                // 3)結果をitemsに保存
                self?.items = response.items
            })
        // 4)itemsの更新はCombineを通して通知される
        
    }

```

1) APIリクエストを前述のリポジトリから作成します。
2) 通信処理を行います。
3) 結果を`items`に格納します
4) `items`の変更はCombineによりUIに通知されます。

## UI

[ContentView](https://raw.githubusercontent.com/usk-lab/APIKitCombine/master/APIKitCombine/ContentView.swift)
こちらをご確認ください。

## ソースコード 

今回のソースコードはこちらに置いてあります。
https://github.com/usk-lab/APIKitCombine

## 再利用性について

今回はGitHub APIを使用しました。
`GitHubRepository`, `GitHubRequestProtocol`, `GitHubDecoder`を少し変えることで他のAPIサービスでも利用できます。

## 参考
- https://github.com/ishkawa/APIKit/
- https://developer.github.com/v3/search/
- https://qiita.com/woxtu/items/ec689cf7709c3ce7d954
- https://qiita.com/renchild8/items/6cdaaad52c900f6efd38
- https://qiita.com/sgr-ksmt/items/e822a379d41462e05e0d
- https://dev.classmethod.jp/articles/rxswift-apikit-decodable-incremental-search/
- https://dev.classmethod.jp/articles/ios13-swiftui-combine-incremental-search/