前提
まず、Podflineに下記を入力しましょう。
バージョンは適宜stableもしくは最新のものを利用します。
pod 'Moya', '~> 15.0'
podsの使い方やinstall方法は省きます。
https://ios-docs.dev/cocoapods/
こちらの記事を参考にすれば導入可能です。
今回は、サーバーから下記のJson形式でデータが送信される前提です。
{
"data": {
"name": "通信 太郎",
"birth_day": "1990-01-01"
"age": 12
}
"error": {
"title": "エラータイトル",
"message": "エラー文",
}
}
ファイル構成
- プロジェクトフォルダ
- APIフォルダ
- ApiRequestファイル
- ApiResponseファイル
- ApiErrorファイル
- Requestフォルダ (APIの数だけここにリクエスト用ファイルを作成します。)
- ApiLoginファイル (ログインAPIを実装)
- Responseフォルダ (APIの数だけここにレスポンス用ファイルを作成します。)
- LoginDataファイル
- ErrorDataファイル
- Viewフォルダ
- MainView(メイン画面のViewファイル)
- Utilフォルダ
- Functionsファイル
- APIフォルダ
ApiRequest
まず、各APIの情報をこのファイルに入力します。
import Foundation
import Moya
// APIの結果を返す際に利用する形式を設定しています。
// successの場合はT(DataClass)を、エラーの場合はApiErrorを返却します。
typealias CompletionHandler<T> = (Result<T, ApiError>) -> Void
enum APIRequest {
// ログインAPI
// 名称は任意、引数はリクエストパラメータを指定します。
case getLogin(email: String, password: String)
// パスワード変更API
// このように、APIの数だけenumを作成します。
case postChangePass(currentPass: String, newPass: String)
}
extension APIRequest: TargetType {
// APIのBaseURL
var baseURL: URL {
//複数の接続先がある場合はここで分岐させる
switch self {
case .getLogin:
return URL(string: "https://xxx/xxx/")!
default: {
return URL(string: "http://yyy/yyy")!
}
}
// API毎のセグメントをここで設定しておく
var path: String {
switch self {
case .getLogin:
return "/xxx/login"
case .postChangePass:
return "/xxx/changePass"
}
}
// 各APIのHTTPメソッドをここに設定する
var method: Moya.Method {
switch self {
case .getLogin:
return .get
case .postChangePass:
return .post
}
}
// ここでパラメータのエンコーディングを行います。
// parametersには["key名": 送信する内容]と設定します。
var task: Task {
switch self {
case .getLogin(email: String, password: String):
return .requestParameters(parameters: ["email": email, "password": password], encoding: URLEncoding.queryString)
case .postChangePass(currentPass: String, newPass: String):
return .requestParameters(parameters: ["current_pass": currentPass, "new_pass": newPass], encoding: URLEncoding.queryString)
}
}
// ここではリクエストヘッダーの設定を行います。
var headers: [String: String]? {
// keyChainに保存しているアクセストークンを込めるように実装していますが、アプリによって変えてください。
// また、サーバー側の実装によってheaderに設定するものは変わります。
if let accessToken = KeychainManager().retrieveAccessToken() {
return ["Content-type": "application/json", "Authorization": "xxx \(accessToken)"]
} else {
// アクセストークンがない場合はAuthorizationを外す
return ["Content-type": "application/json"]
}
}
ApiResponse
ここで、APIのレスポンスをデコードするデータクラスを作成します。
import Foundation
struct ApiResponse<DataClass: Decodable>: Decodable {
let data: DataClass
let error: HeaderClass
struct HeaderClass: Decodable {
let title: String
let message: String
}
}
ここでは、レスポンスで得た返却値をJSON形式からデコードしてアプリで利用できるようにするためのデータクラスを書いています。
Decodableを継承していると、Jsonをクラスにデコードする機能を利用することができます。
今はDataClassを実装していないためエラーが発生すると思いますが、無視してください。
ApiError
API通信の結果、エラーが発生した際に返却するクラスです。
ダイアログでエラーのタイトルとエラーメッセージを表示させる前提で作成していますが、適宜変更してください。
import Foundation
struct ApiError: Error {
var errorCode: String = ""
var errorMessage: String = ""
}
ApiLogin
ログインAPIを送信する処理を書きます。
import Foundation
import Moya
// ログインAPI実行クラス
final class APILogin {
private let provider = MoyaProvider<APIRequest>(plugins: [NetworkLoggerPlugin()])
func getLogin(completion: @escaping CompletionHandler<LoginData>) {
provider.request(.getLogin) { result in
switch result {
// 通信が成功したら.successへ入ります。
case let .success(response):
completion(self.convertData(response: response))
case let .failure(moyaError):
completion(.failure(ApiError(errorCode: String(moyaError.errorCode), errorMessage: Functions.getMoyaErrorString(moyaError))))
}
}
}
private func convertData(response: Response) -> Result<LoginData, ApiError> {
do {
let decoder = JSONDecoder()
// デコードする際に、keyがスネークケースであれば、アッパーケースの同名の変数へデコードされます。
decoder.keyDecodingStrategy = .convertFromSnakeCase
// レスポンスのstatusCodeが200〜299でなければcatchに入ります。
let response = try response.filterSuccessfulStatusAndRedirectCodes()
// dataの中身が存在するか
if Function.isDataFieldNotEmpty(from: response) {
// レスポンスパラメータをApiResponse<LoginData>へ変換します。失敗時はcatchに入ります。
let loginData = try response.map(ApiResponse<LoginData>.self, using: decoder)
return .success(loginData.data)
} else {
// dataの中身がからの場合は受信したエラータイトルとエラーメッセージを返します。
let error = try response.map(ErrorData.self, using: decoder)
return .failure(ApiError(errorCode: "", errorMessage: "\(error.error.title)\n\(error.error.message)"))
}
} catch let error {
let moyaError = error as! MoyaError
return .failure(ApiError(errorCode: String(moyaError.errorCode), errorMessage: Function.getMoyaErrorString(moyaError)))
}
}
}
このクラスでログインAPIを呼び出す処理を書いています。
API毎にクラスを作成しますが、一つに纏めることも可能だと思います。
LoginData
ログインAPIのレスポンスをデコードするクラスです。
import Foundation
struct LoginData: Decodable {
// ユーザー名
var name: String
// 誕生日
var birthDay: String
// 年齢
var age: Int
}
ErrorData
dataが存在しない場合のデコードクラスです。
import Foundation
struct ErrorData: Decodable {
let error: HeaderClass
struct HeaderClass: Decodable {
let title: String
let message: String
}
}
Function
ここではアプリ全体で利用する関数をまとめています。
import Foundation
class Function {
// MoyaErrorからエラー文を取り出す
static func getMoyaErrorString(_ moyaError: MoyaError) -> String {
var errorText = ""
switch error {
case .underlying(let nsError as NSError, _):
errorText = nsError.localizedDescription
default:
errorText = "不明なエラーです。"
}
return errorText
}
// APIの返り値にDataが存在するか
func isDataFieldNotEmpty(from response: Response) -> Bool {
do {
let filteredResponse = try response.filterSuccessfulStatusAndRedirectCodes()
// JSONを解析
if let jsonObject = try? JSONSerialization.jsonObject(with: filterdResponse.data, option: []) as? [String: Any] {
// dataを取得する
if let dataValue = jsonObject["data"] {
// dataがArrayの場合
if let dataArray = dataValue as? [Any], !dataArray.isEmpty {
return true
}
// dataがDictionaryの場合
else if let dataDict = dataValue as? [String: Any], !dataDict.isEmpty {
return true
}
// 'data'がStringの場合
else if let dataString = dataValue as? String, !dataString.isEmpty {
return true
}
// その他の返却値の場合
else {
return false
}
} else {
// データkeyがない場合
return false
}
} else {
// JSONの解析が失敗した場合
return false
}
} catch {
// エラーが発生した場合
return false
}
}
MainView
ここまでで作成したAPIを呼び出してみましょう。
import SwiftUI
import Moya
struct MainView: View {
@State private var mail = ""
@State private var pass = ""
// ダイアログ表示フラグ
@State private var isShowDialog = false
// エラーメッセージ
@State private var errorTitle: String = ""
@State private var errorMessage: String = ""
var body: some View {
VStack {
TextField("メールアドレスを入力", text: $mail)
...
SecureField("パスワードを入力", text: $pass)
...
Button(action: {
login() { result in
switch result {
case .success(let data):
print(data.name)
print(data.birthDay)
print(data.age)
case .failure(let error):
// エラーの場合はエラー情報をアラートで表示します。
self.errorTitle = error.errorTitle
self.errorMessage = error.errorMessage
self.isShowDialog = true
}
}
}) {
Text("ログイン")
...
}
}
.alert(isPresented: $isShowDialog) {
return Alert(title: Text(errorTitle), message: Text(errorMessage), dismissButton: .default(Text("確認"))) {
self.isShowDialog = false
})
}
}
// ログインAPI処理
func login(completion: @escaping(Result<LoginData, ApiError>) -> Void) {
ApiLogin().getLogin(email: mail, password: pass) { result in
completion(result)
}
}
}
これで一連の処理は完成となります。
SwiftUIでの実装経験があまりないため、KotlinのOkHttpで実装していた時のファイル構成に寄せるような実装をしましたが、他の方はどのような実装をしているのか気になります。
コメント