SwiftでAPIを使ってHTTPリクエストとMVVMによるアプリ作成

SwiftでAPIを使ってHTTPリクエスト

本記事では初学者を対象に、以下3つを実例を交えて解説

  • APIの基本的な使い方
  • HTTPリクエストの作成
  • MVVMモデルの実装

開発環境等

  • Xcode: Version 13.3
  • SwiftUIを使用

作成物

COvidSImple_Demo
本記事で紹介するアプリの完成形

作成物として、上記のようなコロナウイルスのデータを取得するアプリを作ります。GitHub上でもCovidSimpleとして公開していますので遊んでみてくださいね。
なお、本アプリは海外の学習動画サイトを参考にアレンジして作成しました。英語が得意な方は本家様をご覧ください。

ファイル構造
ファイル構造

アーキテクチャ設計

アーキテクチャ
MVVMモデルとデータフロー

MVVMモデルを採用しています。APIServer.swiftで本記事の主要部分を解説し、その後APIServer.swiftの使用方法を説明します。それぞれのソースコードを記載しています。

大まかなデータフローは上記のように。

  1. MainViewがMainVIewModelへ表示データのリクエスト
  2. MainViewModelがModel(TotalData)の作成
  3. MainVIewModelがAPIServerへのリクエスト
  4. MainViewModelがMainVIewへ応答

本記事では特にAPIServer.swiftで何をしているのか?に焦点を当てて説明していきます。

APIServer.swift

本記事で解説するメインであるファイルです。APIsever.swiftというファイルでAPIの基本的な使い方を説明していきます。使用するのは、RapiAPIで無料で提供されているCovid-19-statistics です。RapidAPIを初めて使用する場合はアカウント登録する必要がありますが、グーグルアカウント等で20秒ほどで作れます。

//
//  APIService.swift
//  CovidStats
//  https://rapidapi.com/axisbits-axisbits-default/api/covid-19-statistics/

import Foundation

final class APIService {
    
    static let shared = APIService()
    private let baseURLString = "https://covid-19-statistics.p.rapidapi.com"
    private let headers = [
        "X-RapidAPI-Host": "covid-19-statistics.p.rapidapi.com",
        "X-RapidAPI-Key": "eda6f3892emsh2d191f757d6c379p1e4dd5jsn14e1a892529f" //This is free Key by NakadeShoya
    ]
    
    func fetchTotalData(completion: @escaping ( Result<TotalData, Error>) -> Void) {
        //MARK: - 1.API取得先URLの作成
        let totalURLString = baseURLString + "/reports/total"
        let url = URL(string:  totalURLString)
        
        guard let url = url else {
            completion(.failure(CovidError.incorrectURL))
            return
        }
        
        //MARK: - 2.URLリクエストの作成
        var request = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 100.0)
        request.httpMethod = "GET" // 初期値でもGETだが、明示のため。
        request.allHTTPHeaderFields = headers

        //MARK: - 3.TASKの作成
        let session = URLSession.shared
        let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
            
            if (error != nil) {
                completion(.failure(CovidError.noDataReceived))
            } else {
//                if let json = try? JSONSerialization.jsonObject(with: data!, options: []) as? [String: Any] {
//                    print(json)
//                }
                let decorder = JSONDecoder()
                
                do {
                    let totalDataObject = try decorder.decode(TotalDataObject.self, from: data!)
                    completion(.success(totalDataObject.data))
                } catch let error {
                    completion(.failure(error))
                }            
            }
        })
        //MARK: - 4.TASKの実行
        dataTask.resume() //
    }
}

APIserver.swiftの解説

上記ファイルでは大きく分けて3つのことをしています。

  1. APIを取得するためのヘッダー作成
  2. URLリクエストの作成
  3. データを取得するタスクの作成と実行

1. APIを取得するためのヘッダー作成

この部分では、使用するAPIを指定しています。HostがAPI取得先で、Keyはアクセスするための鍵です。 このAPIは無料で使用されているためKeyを公開してますが、料金などがかかるAPIが多いのでこのKeyは基本的に非公開にしましょう。なお、これらの値はrapidAPIのページに記載されています。

    private let headers = [
        "X-RapidAPI-Host": "covid-19-statistics.p.rapidapi.com",
        "X-RapidAPI-Key": "eda6f3892emsh2d191f757d6c379p1e4dd5jsn14e1a892529f" //This is free Key by NakadeShoya
    ]
rapidAPI

2. URLリクエストの作成

次に1.で作った情報をもとにAPIサーバーへの”URLリクエスト” を作成します。

//MARK: - 2.URLリクエストの作成
var request = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 100.0)
request.httpMethod = "GET" // 初期値でもGETだが、明示のため。
request.allHTTPHeaderFields = headers

3. データを取得するタスクの作成と実行

最後にTASKを作成します。
CovidErrorは自作した構造体で、エラー内容がわかりやすいようにしているだけです。
通信に成功したときにのみ、取ってきたデータ(data)をjsonデータとしてデコード(解読)し、totalDataObject というModelに入れています。それを .successでTotalData型を返すというわけです。
最後にタスクを実行するresume()をします。

func fetchTotalData(completion: @escaping ( Result<TotalData, Error>) -> Void) {
        //MARK: - 1.API取得先URLの作成
        let totalURLString = baseURLString + "/reports/total"
       ・
       ・
       ・
       ・
       ・

//MARK: - 3.TASKの作成
        let session = URLSession.shared
        let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
            
            if (error != nil) {
                completion(.failure(CovidError.noDataReceived))
            } else {
//                if let json = try? JSONSerialization.jsonObject(with: data!, options: []) as? [String: Any] {
//                    print(json)
//                }
                let decorder = JSONDecoder()
                
                do {
                    let totalDataObject = try decorder.decode(TotalDataObject.self, from: data!)
                    completion(.success(totalDataObject.data))
                } catch let error {
                    completion(.failure(error))
                }            
            }
        })
        //MARK: - 4.TASKの実行
        dataTask.resume() //
}

Model

主要ModelはTotalDataObjectです。こちらの構造はRapidApiのResultsを見てその構造体に合わせています。structのプロトコルにCodableをつけないと読み込みできませんので気をつけましょう。
Codableプロトコルによって,jsonデコードできるようになっています。

rapidAPI_Result
//
//  TotalData.swift
//  CovidStats
//
//  Created by 中出翔也 on 2022/03/22.
//

import Foundation

struct TotalDataObject: Codable {
    let data: TotalData // APIに合わせている
}

struct TotalData: Codable {
    let confirmed: Int
    let deaths: Int
    let confirmed_diff: Int
    let deaths_diff: Int
    let active: Int
    let fatality_rate: Double
    
    static let dummyData = TotalData(confirmed: 0, deaths: 0, confirmed_diff: 0,deaths_diff: 0, active: 0, fatality_rate: 0)
}

View

MainView() → TotalDataView() → DataCardViewという流れで表示されますが、コードの可視性・再利用性の点で分割しているだけです。
重要なのは @StateObject private var viewModel = MainViewModel()
これだけです。このViewでModelVIewをインスタンス化している。これより下層のviewにも伝番させているというわけですね。

※ちなみにこの場合はObservedObjectでも問題ありません。ライフサイクルが違いObservedObjectの場合は親VIewが再描写されるたびに更新されます。

//
//  MainView.swift
//  CovidStats

import SwiftUI

struct MainView: View {
    @StateObject private var viewModel = MainViewModel()
    let formatter = DateFormatter()
    let today: String
    
    init() {
        formatter.dateFormat = "y-MM-dd" // APIに合わせる
        self.today = formatter.string(from: Date())
    }
    
    var body: some View {         
        VStack(alignment: .leading) {
            Text("世界の総数:(\(today))")
                .font(.title2.bold())
                .foregroundColor(.primary)
                .padding(10)
            TotalDataView(totalData: viewModel.totalData)
        }
    }
}
import SwiftUI
import Foundation

struct TotalDataView: View {
    var totalData: TotalData
  
    var body: some View {
        
        VStack {
            HStack {
                DataCardView(number: totalData.confirmed.formatNumber, name: "総感染者数", color: .accentColor)
                DataCardView(number: totalData.confirmed_diff.formatNumber, name: "1日の感染者数", color: .accentColor)
            }
            HStack {
                DataCardView(number: totalData.deaths_diff.formatNumber, name: "死亡数", color: .red)
                DataCardView(number: String(format: "%.2f", totalData.fatality_rate)
                , name: "死亡率 %"
                , color: .red)
            }
        }
        .frame(height: 170)
        .padding(10)
    }
}

extension Int {
    var formatNumber: String {
        let formatter = NumberFormatter()
        formatter.groupingSeparator = ","
        formatter.numberStyle = .decimal
        return formatter.string(from: NSNumber(value: self))!
    }
}

ViewModel

ViewModelでは先ほどのModelとAPI,そして次項のVIewをつなぐ役割をしています。橋渡し役をしています。
.sharedシングルトンと呼ばれており、1つのインスタンスを共有している ぐらいの認識で大丈夫です。
APIService.shared.fetchTotalData { result in
この文でresultは通信に成功したときに(let totalData)を持っているので、ModelVIewのTotalDataにそれを渡しています。

//
//  MainViewModel.swift
//  CovidStats

import Foundation

final class MainViewModel: ObservableObject {
    
    @Published var totalData: TotalData = TotalData.dummyData
    
    init() {
        fetchTotalData()
    }
    
    func fetchTotalData() {
        APIService.shared.fetchTotalData { result in
            //MARK: - dispatchを使う理由はfetchに時間がかかるから、アプリの裏側で実行する
            DispatchQueue.main.async {
                switch result{
                case .success(let totalData):
                    self.totalData = totalData
                case .failure(_):
                    self.alertItem = AlertContext.unableToFetchTotalStats
                }
            }
        }
    }
}

まとめ

本記事では以下3点を初心者向けに解説しました。

APIの基本的な使い方
HTTPリクエストの作成
MVVMモデルの実装

質問や、意見などありましたらぜひ教えてください!自分のアウトプットとして書いてますが、少しでも初学者の方の手助けになれば幸いです。