スポンサーリンク

【SwiftUI】Moyaを利用したAPI通信処理を実装する

スポンサーリンク

前提

まず、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ファイル

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で実装していた時のファイル構成に寄せるような実装をしましたが、他の方はどのような実装をしているのか気になります。

コメント

タイトルとURLをコピーしました