QiitaAPIを用いたiOSアプリ開発におけるエラー処理の方法

初めての"0→1"でiOSアプリを作成

自己紹介

こんにちはshuです。
現在Swiftを用いたiOSのアプリ開発をしています。

概要

案件にアサインされる前の腕試しということで
仕様に従ってアプリを作成する"模擬案件"に挑戦しました。

開発を終えての感想・得られた技術についてまとめていきます。

得られた技術に関しては
調べていて記事が少なかった
"Delegateを用いたエラー処理"
に関する内容をまとめていきます。

作成したアプリ

Qiitaのクライアントアプリ
GitHub

Qiita APIを用いて、記事を簡単に見れるアプリを作成しました。

そもそもDelegateとは

Delegateの概念に関してわかりやすかった記事を紹介します。
【swift】イラストで分かる!具体的なDelegateの使い方。

「あるクラスは、他のクラスのインスタンスに、処理を任せることができる。」

この概念を理解するのに少々時間がかかりました。

今回エラー処理実装に関係するクラスの説明

FeedViewController
サブクラス:UIViewController
記事一欄画面をコントロールするクラス

AirticleDataNetworkService
記事一覧のAPIを叩くクラス
FeedViewControllerでインスタンス化して使用している

ErrorView
サブクラス:UIView
エラーが出たことをユーザーに知らせるクラス

実現させたいエラー処理の内容

Qiita API ドキュメントに記載がある通り
アクセストークンなしのリクエストには回数制限があります。
アプリで連続してリクエストを送って上限回数を超えたら
記事一欄画面の上にErrorViewを表示させます。

Delegateを用いたエラー処理

1. protocolの用意

Delegateメソッドの命名規則や引数の名前に関して
考慮せず作成しました。お許しください。

protocol ErrorDelegate: AnyObject {
    func segueErrorViewController(qiitaError: QiitaError)
    func backToLoginViewController()
    func reload()
}

今回は
segueErrorViewControllerメソッドをどう利用したかについて
説明していきます。

2. FeedViewControllerに準拠させる

extension FeedViewController: ErrorDelegate {    
    func segueErrorViewController(qiitaError: QiitaError) {
        let errorView = ErrorView.make() //ErrorViewインスタンス化
        errorView.checkSafeArea(viewController: self) //レイアウト設定
        errorView.errorDelegate = self
        errorView.qiitaError = qiitaError
        errorView.setConfig()
        view.addSubview(errorView) //記事一覧画面の上に表示させる
    }        
}

FeedViewControllerでしかできないことを意識して実装していきます。

3. AirticleDataNetworkServiceに紐付け

FeedViewControllerのviewDidLoadメソッド内で
AirticleDataNetworkServiceのインスタンス化後に紐付けを行います。

    override func viewDidLoad() {
        super.viewDidLoad()
        //記事データ取得
        articleListDataRequest = AirticleDataNetworkService(searchDict: nil)
        //紐付け
        articleListDataRequest.errorDelegate = self
        getData(requestAirticleData: articleListDataRequest)
    }

4. AirticleDataNetworkServiceクラスでDelegateメソッドを呼び出す

    func showErrorView(qiitaError: QiitaError) {
        if let errorDelegate = self.errorDelegate {
            errorDelegate.segueErrorViewController(qiitaError: qiitaError)
        } else {
            print("⚠️ ErrorDelegate: nil")
        }
    }
    //データ取得
    func fetch(success: @escaping ((_ result: [AirticleModel]?) -> Void),
               failure: @escaping ((_ error: NSError?) -> Void)) {
        //↓URLの設定
        let reqParamModel = RequestParametersCreater(
            dataType: .article,
            pageNumber: pageNumber,
            searchDict: searchDict,
            sortdict: nil)
        let urlText = reqParamModel.assembleItemURL(pageNumber: pageNumber)
        guard let url = URL(string: urlText) else { return }
        //↑URLの設定
        
        let qiitaRequest = QiitaRequest()
        //↓QittaRequestのメソッドrequestは引数にheadersを設置しなくてもリクエスト時にヘッダー情報を組み込むようにしている
        qiitaRequest.request(url: url).response { response in
            guard let data = response.data else {
                return
            }
            //取得したデータを格納
            guard let exportData = try? JSONDecoder().decode([AirticleModel].self, from: data) else {
                print("An error occurred during decoding.")
                if let exceptionData = try? JSONDecoder().decode(ErrorModel.self, from: data) {
                    //1日のリクエスト数が上限に達した場合
                    if let message = exceptionData.message,
                       let type = exceptionData.type {
                        print("message: \(message), type: \(type)")
                        self.showErrorView(qiitaError: .rateLimitExceededError)
                    } else {
                        self.showErrorView(qiitaError: .unexpectedError)
                    }
                } else {
                    print("Failed to get error message.")
                    self.showErrorView(qiitaError: .unexpectedError)
                }
                
                failure(response.error as NSError?)
                return
            }
            success(exportData)
        }
    }

1日のリクエスト数が上限に達した場合に
delegateメソッドを呼び出しています。

動作確認

開発終えての感想

途中何度かつまずくこともありましたが、
コードをレビューしていただいたKAORU MATARAIさんのおかげで
最後まで作りきることができました。
ありがとうございました。
今回得られた貴重な経験を個人アプリ・インターン採用・就活に生かそうと思います。

今回のアプリ開発で携わった方々

KAORU MATARAIさん→アプリ設計・デザイン・コードレビュー
PlayGround内の方々→テスト
shu(私)→主な実装