SwiftでAPIを使ってHTTPリクエスト
本記事では初学者を対象に、以下3つを実例を交えて解説
- APIの基本的な使い方
- HTTPリクエストの作成
- MVVMモデルの実装
開発環境等
- Xcode: Version 13.3
- SwiftUIを使用
作成物
作成物として、上記のようなコロナウイルスのデータを取得するアプリを作ります。GitHub上でもCovidSimpleとして公開していますので遊んでみてくださいね。
なお、本アプリは海外の学習動画サイトを参考にアレンジして作成しました。英語が得意な方は本家様をご覧ください。
アーキテクチャ設計
MVVMモデルを採用しています。APIServer.swiftで本記事の主要部分を解説し、その後APIServer.swiftの使用方法を説明します。それぞれのソースコードを記載しています。
大まかなデータフローは上記のように。
- MainViewがMainVIewModelへ表示データのリクエスト
- MainViewModelがModel(TotalData)の作成
- MainVIewModelがAPIServerへのリクエスト
- 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つのことをしています。
- APIを取得するためのヘッダー作成
- URLリクエストの作成
- データを取得するタスクの作成と実行
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
]
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デコードできるようになっています。
//
// 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モデルの実装
質問や、意見などありましたらぜひ教えてください!自分のアウトプットとして書いてますが、少しでも初学者の方の手助けになれば幸いです。