Alamofire v4からv5への更新で苦労した話

はじめに

こんにちは

Rettyアプリ開発チームの@レイと申します。
今回で2回目の記事ですね、前の記事ではRettyにJoinしてから振り返りの話をしました。
そのため今回は技術的な話をしようと思います。

その前に最近行ったお店がすごく良かったので、皆さんにおすすめしたいと思います。
こちらのお店です!

retty.me

  • すごく料理やカクテルが美味しい

  • 店員さんのパフォーマンスが素晴らしい

  • 店員さんがすごく親切

このようなすごく良いお店だったので、ぜひ皆さんも一度行ってみてください!

では、今回の記事の本題をお話しようと思います。

Alamofireのバージョンを更新したい!

皆さんはAlamofire5からバックグラウンドセッションのサポートが無くなるのはご存じでしょうか?
Rettyアプリチームでは

  • システムの安定稼働
  • Alamofire v5.5から追加されたSwift Concurrencyを利用できるようにする

という背景でバージョン更新を行うことになりました。
しかしながら、今回のバージョン更新では二つの問題に直面しました。

一つ目は

Rettyでは写真のアップロードについて使われているBackground Session ManagerがAlamofire v5から使えなくなったため、直接Background Task Managerを作らないといけない

二つ目は

Alamofire v5.5.0はSwift Concurrencyが追加され、これがXcode13.3のReleaseバージョンだとアプリが落ちる

といったものです。

本記事では、これらの問題をRettyアプリチームではどのように解決したのか紹介したいと思います。

Background Task Managerを作ろう!

通信のためにBackground Taskを使う技術は二つあります。

  1. UIApplication.shared.beginBackgroundTask

  2. URLSession Background Session

最初は2番の方法を用いてコードを作成しました。

しかし

  • Background URLSessionはDelegateパターンを使わねばならない
  • 現状のRettyアプリでは非同期通信にPromiseKitが利用されている
  • これをdelegateパターンに変更するには、あまりにも大きいタスクになる

以上の理由から1番の方法を利用することにしました。

Background URLSessionのコード例

private lazy var urlSession: URLSession = {
    let config = URLSessionConfiguration.background(withIdentifier: "MySession")
    return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()

urlSession.downloadTask(with: url).resume()

func application(_ application: UIApplication,
                 handleEventsForBackgroundURLSession identifier: String,
                 completionHandler: @escaping () -> Void) {
        backgroundCompletionHandler = completionHandler
}

詳しくはこちら

そのため作られたBackground Task Managerはこんな感じになりました。

BackgroundTaskManagerのコード

import Foundation

final class BackgroundTaskManager {
    static let shared = BackgroundTaskManager()

    private init() {}

    func beginBackgroundTask(_ callback: (UIBackgroundTaskIdentifier) -> Void) {
        var backgroundTaskIdentifier = UIBackgroundTaskIdentifier.invalid

        backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask { [weak self] in
            self?.endBackgroundTask(identifier: backgroundTaskIdentifier)
        }
        callback(backgroundTaskIdentifier)
    }

    func endBackgroundTask(identifier: UIBackgroundTaskIdentifier) {
        UIApplication.shared.endBackgroundTask(identifier)
    }
}

このようにシングルトンを作っておけば、今後も簡単に使うことができるので、BackgroundTaskを管理するManagerを作りました。

BackgroundTaskManagerを使う例のコード

func postInBackground(path: String, parameters: Parameters? = nil, method: HTTPMethod = .post) -> Promise<[String: Any]> {
        var currentIdentifier: UIBackgroundTaskIdentifier?

        return Promise { seal in
            ・・・
            BackgroundTaskManager.shared.beginBackgroundTask { [weak self] identifier in
                currentIdentifier = identifier

                guard let self = self else {
                    seal.reject(エラー)
                    return
                }

                self.manager.upload(・・・)
                .validate()
                .responseData { response in
                    switch response.result {
                    case let .success(data):
                        do {
                        } catch {
                            seal.reject(error)
                        }
                    case let .failure(error):
                        seal.reject(error)
                    }
                }
            }
        }.ensure {
            currentIdentifier?.endBackgroundTask()
        }
    }

あとは既存通信コードをBackgroundTaskManagerのcallbackとして呼ぶだけなので、本当に少ない修正のみでBackgroundTask対応タスクが終わりました。👏👏👏

expirationHandlerは何?

これでバックグラウンドタスク対応は終わりましたが、個人的に少し気になるところがありました。

あれ?

UIApplication.shared.beginBackgroundTask

を呼ぶときexpiration Handlerって何だっけ?🤔

developer.apple.com

A handler called shortly before the task’s background time expires.

The expiration handler takes no arguments and has no return value. Use the handler to cancel any ongoing work and to do any required cleanup in as short a time as possible. The handler may be called before the background process uses the full amount of its allocated time.

Apple Documentにはこのように書いていまいましたが、

この以上は検索してもあまり情報がなくて、以下のようなコードを記述して動作を確認しました。

動作確認用コード

BackgroundTaskManager.shared.beginBackgroundTask { identifier in
    NSLog("Running in the background\n")

    // while main thread expirationHandler not called
    // https://developer.apple.com/documentation/uikit/uiapplication/1623031-beginbackgroundtaskwithexpiratio#:~:text=This%20method%20can%20be%20safely%20called%20on%20a%20non%2Dmain%20thread
    DispatchQueue.global().async {
        while(UIApplication.shared.applicationState == .background)
        {
            NSLog("Background time Remaining: \(UIApplication.shared.backgroundTimeRemaining)" )
            sleep(1)
        }
    }
}

結果

なるほど!
expirationHandlerは約5秒前に呼ばれるし、先にexpirationHandler中でendBackgroundTaskを呼ばれても、残りの時間 background sessionは問題なく実行されるということを確認できました。👍

Alamofireの5.5.0ではSwift Concurrencyを使っているよ!

最初はAlamofireのバージョンを5.5.0で作業をしましたが、なぜかRelease版ではアプリが落ちていました。
そのため原因を探したら、ちょうどRettyではXcode13.3を利用していて、このバージョンではSwift Concurrencyを利用したらアプリがクラッシュするというISSUEを見つけました。

結局、しばらくXcodeを前のバージョンである13.2.1を利用することで問題は解決できました。👏👏👏

今後はバージョンアップデートする際に、ISSUEを予め確認して、事前に予防できる動きをしていきたいと思います。

おわりに

今回の記事ではAlamofireを更新するため色々解決しないといけなかったというテーマで紹介しました。

結果的に、解決方法は簡単でしたが、その過程で作動の流れを学んだり、問題解決の過程を学んだりすることで、多くの学びを得ることができました。 そしてその学びをチームで共有することで、チームと一緒に成長することができたと思います。

アプリ開発チームのメンバーとMeetyでお話しできます!本記事でRettyのアプリ開発チームについて興味が湧きましたら、ぜひMeetyで気軽にお話ししましょう!

meety.net