LoginSignup
5
6

More than 3 years have passed since last update.

【Swift】Combine + APIKit で複数リクエストを直列で実行するサンプル (ついでに並列も)

Posted at

title.png

Combine.frameworkを体験する取っ掛かりとして
APIKitのRequestにPublisherを生やして
複数の非同期通信を直列・並列で叩くサンプルを実装してみました。

まだまだCombineニュービーですのでオススメの方法や
間違い等有りましたらご指摘頂けると幸いです🙇🏻‍♂️

環境

  • Xcode 12.1
  • APIKit 5.1.0
  • SwiftUI

サンプルコード

プロジェクト構成

APIKitにPublisherを生やす実装方法については
Developers.IO さんの以下の記事が
非常にわかりやすかったのでほぼコピさせていただきました。
[iOS 13] SwiftUI + Combine + APIKitでインクリメンタルサーチ

今回のサンプルコードでは複数の通信を叩くために
GitHub REST APIを使う形に変更しています。

スクリーンショット 2021-01-21 16.00.27.png

処理の流れ

主な登場人物は上記赤枠の3つです。
処理の流れは以下になります。

  1. ContentViewUISearchBarの入力を受け取る
  2. SampleModelでリクエストを作成して直列の通信を開始する
  3. 最初に/search/repositoriesを叩く
  4. 上記のレスポンスを利用して/repos/{owner}/{repositry}を叩く
  5. 通知された結果を表示する

直列で叩く(Publisher)

最初に/search/repositories を叩き、レスポンスを利用して
/repos/{owner}/{repositry} を叩いてみます。
正直最初のリクエストで欲しいデータは取得できますが学びということで。

        // `/search/repositories` のリクエスト作成
        let searchRepositories = SearchRepositoriesRequest(query: searchText).publisher.eraseToAnyPublisher()
        self.requestCancellable = searchRepositories
            // レスポンスはSearchRepositoriesResponse型なのでitems.firstをとりあえず取得
            .compactMap { $0.items.first }
            // 上記レスポンスを利用して GetRepositoyRequest を作成
            .flatMap { (repository) -> AnyPublisher<GetRepositoyRequest.Response, Error> in
                /* 処理を繋げるとコンパイラが型がネストして解釈して複雑な型になるため
                 * eraseToAnyPublisher()で型消去した
                 * AnyPublisher<GetRepositoyRequest.Response, Error> を返す
                 */ 
                GetRepositoyRequest(owner: repository.owner.login, repositry: repository.name).publisher.eraseToAnyPublisher()
            }
            .receive(on: DispatchQueue.main)
            .sink { (completion) in
                switch completion {
                case .finished:
                    debugPrint("request finished")
                case let .failure(error):
                    debugPrint("request failed : \(error)")
                }
            } receiveValue: { [weak self] repository in
                // `/repos/{owner}/{repositry}` のレスポンスをContentViewに渡す
                self?.items = [repository]
            }

無事直列で叩けてる模様です。
スクリーンショット 2021-01-21 17.18.37.png

補足リンク eraseToAnyPublisher()での型消去について

Futureを使って直列で叩く

FutureはSubscribeされなくても
インスタンスが生成されたタイミングで即時実行されるので
とりあえずクロージャーで返すメソッドを作って処理を繋げてみました。
こちらも本来であれば再利用のためにAPIKitに生やすのが望ましいでしょうか。

        // MARK: - Futureを使って直列で叩く

        self.requestCancellable = fetchRepos(query: searchText)
            .compactMap { $0.items.first }
            .flatMap { [unowned self] repository in
                self.fetchRepository(owner: repository.owner.login, repo: repository.name)
            }.sink { (completion) in
                switch completion {
                case .finished:
                    debugPrint("request finished")
                case let .failure(error):
                    debugPrint("request failed : \(error)")
                }
            } receiveValue: { [weak self] repository in
                self?.items = [repository]
            }


    // MARK: - 各リクエストの生成処理

    // ※Futureは即時実行される => インスタンスが生成されたタイミングでSubscribeをしなくても処理が走る
    private func fetchRepos(query: String) -> Future<SearchRepositoriesRequest.Response, Error> {
        return Future<SearchRepositoriesRequest.Response, Error> { promise in
            Session.send(SearchRepositoriesRequest(query: query), callbackQueue: .main) { (result) in
                switch result {
                case .success(let res):
                    // 処理成功
                    promise(.success(res))
                case .failure(let error):
                    // 処理失敗
                    promise(.failure(error))
                }
            }
        }
    }

    // ※Futureは即時実行される => インスタンスが生成されたタイミングでSubscribeをしなくても処理が走る
    private func fetchRepository(owner: String, repo: String) -> Future<GetRepositoyRequest.Response, Error> {
        return Future<GetRepositoyRequest.Response, Error> { promise in
            Session.send(GetRepositoyRequest(owner: owner, repositry: repo), callbackQueue: .main) { (result) in
                switch result {
                case .success(let res):
                    // 処理成功
                    promise(.success(res))
                case .failure(let error):
                    // 処理失敗
                    promise(.failure(error))
                }
            }
        }
    }

こちらも無事に直列で叩けてる模様です。
スクリーンショット 2021-01-21 17.19.50.png

ついでに並列でも叩いてみる

Publishers.Zipに渡せば並列で叩けるので
/search/repositories/search/usersを同じクエリで叩いてみます。

        let searchRepositories = SearchRepositoriesRequest(query: searchText).publisher.eraseToAnyPublisher()
        let searchUsers = SearchUsersRequest(query: searchText).publisher.eraseToAnyPublisher()

        self.requestCancellable = Publishers.Zip(searchRepositories, searchUsers)
            .receive(on: DispatchQueue.main)
            .sink { _ in } receiveValue: { (repos, users) in
                print(repos)
                print(users)
            }

無事叩けてました。
スクリーンショット 2021-01-21 17.21.01.png

ついでに補足比較: RxSwiftで直列で叩く場合

RxSwiftの場合はSessionにObservableを生やしてflatMapで繋ぐ感じでしょうか。

extension APIKit.Session {
    func rx_sendRequest<T: Request>(request: T) -> Observable<T.Response> {
        return Observable.create { observer in
            let task = self.send(request) { result in
                switch result {
                case let .success(res):
                    observer.on(.next(res))
                    observer.on(.completed)
                case let .failure(err):
                    observer.onError(err)
                }
            }
            return Disposables.create {
                task?.cancel()
            }
        }
    }
}

   // Observableで包んで返す

    func searchRepositories(request: SearchRepositoriesRequest) -> Observable<SearchRepositoriesRequest.Response> {
        return Session.rx_sendRequest(request: request)
    }

    func getRepository(request: GetRepositoryRequest) -> Observable<GetRepositoryRequest.Response> {
        return Session.rx_sendRequest(request: request)
    }

    // もろもろ省略

    let observable = searchRepositories(request: searchRepositoriesRequest)
            .flatMap { [unowned self] repos -> Observable<GetRepositoryRequest.Response> in
                let item = repos.items.first //とりあえず成功してアンラップできてるものとみなす
                let getRepositoryRequest = getRepositoryRequest(owner: item.owner.login, repo: item.name)
                return self.getRepository(request: getRepositoryRequest)
            }.もろもろ続く

参考にさせて頂きました。ありがとうございます。

5
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6