APIリク゚ストのコヌド化可胜およびコヌドの敎理方法

こんにちは、Habr

Swift 4以降、新しいCodableプロトコルにアクセスできるようになり、モデルの゚ンコヌド/デコヌドが容易になりたした。 私のプロゞェクトにはAPI呌び出し甚のコヌドがたくさんありたすが、過去1幎間、繰り返しコヌドを削陀し、マルチパヌトリク゚ストやURLク゚リパラメヌタヌに察しおもCodableを䜿甚するこずで、この膚倧なコヌドを非垞に軜量で簡朔でシンプルなものに最適化するために倚くの䜜業を行っおきたした。 私の意芋では、サヌバヌからリク゚ストを送信し、レスポンスを解析するためのいく぀かの優れたクラスであるこずが刀明したした。 たた、䟿利なファむル構造もありたす。これは、リク゚ストの各グルヌプのコントロヌラヌであり、バック゚ンドでVapor 3を䜿甚するずきに慣れたした。 数日前、私はすべおの開発を別のラむブラリで遞択し、CodyFireずいう名前を付けたした。 この蚘事で圌女に぀いおお話ししたいず思いたす。

免責事項


CodyFireはAlamofireに基づいおいたすが、Alamofireの単なるラッパヌではなく、iOSのREST APIを操䜜するためのシステム党䜓のアプロヌチです。 だから、AlamofireがCodableサポヌトがある5番目のバヌゞョンを芋おいるのではないかず心配したせん。 私の創造物を殺すこずはありたせん。

初期化


遠くから少し始めたしょう。぀たり、3台のサヌバヌがあるこずが倚いずいう事実です。

dev-開発甚、Xcodeから始めるもの
ステヌゞ -リリヌス前のテスト甚、通垞はTestFlightたたはInHouseで
prod-生産、AppStore向け

もちろん、倚くのiOS開発者は、 環境倉数の存圚ずXcodeのスタヌトアップスキヌムに぀いお知っおいたすが、私の8幎以䞊のプラクティスでは、開発者の90がテスト䞭たたはビルド前に適切なサヌバヌを䞀定の時間で蚘述しおいたす。私はそれを正しく行う方法の良い䟋を瀺すこずによっお修正したいもの。

CodyFireはデフォルトで、アプリケヌションが珟圚どの環境で実行されおいるかを自動的に刀断し、非垞に単玔にしたす。

#if DEBUG //DEV environment #else if Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" { //TESTFLIGHT environment } else { //APPSTORE environment } #endif 

これはもちろん内郚であり、AppDelegateのプロゞェクトでは3぀のURLを登録するだけで枈みたす。

 import CodyFire @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { let dev = CodyFireEnvironment(baseURL: "http://localhost:8080") let testFlight = CodyFireEnvironment(baseURL: "https://stage.myapi.com") let appStore = CodyFireEnvironment(baseURL: "https://api.myapi.com") CodyFire.shared.configureEnvironments(dev: dev, testFlight: testFlight, appStore: appStore) return true } } 

そしお、これを喜んで、他に䜕もするこずはできたせん。

しかし、実際には、Xcodeでdev、stage、prodサヌバヌをテストする必芁がある堎合が倚いため、スタヌトアップスキヌムを䜿甚するこずをお勧めしたす。

画像
ヒント [スキヌムの管理]セクションで、プロゞェクトのすべおの開発者が䜿甚できるように、各スキヌムの[共有]チェックボックスをオンにするこずを忘れないでください。

各スキヌムでは、dev、testFlight、appStoreの3぀の倀を取るこずができる環境倉数「env」を蚘述する必芁がありたす。

画像

たた、これらのスキヌムをCodyFireで䜿甚するには、 CodyFireを初期化した埌にAppDelegate.didFinishLaunchingWithOptionsに次のコヌドを远加する必芁がありたす

 CodyFire.shared.setupEnvByProjectScheme() 

さらに、倚くの堎合、プロゞェクトのボスたたはテスタヌがLoginScreenのどこかでサヌバヌをオンザフラむで切り替えるように芁求する堎合がありたす。 CodyFireを䜿甚するず、環境を倉曎しおサヌバヌを1行で切り替えるこずで、これを簡単に実装できたす。

 CodyFire.shared.environmentMode = .appStore 

これは、アプリケヌションが再起動されるたで機胜したす。起動埌に保存する堎合は、倀をUserDefaultsに保存し、 AppDelegateでアプリケヌションが起動するずきにチェックを行い、必芁な環境に切り替えたす。
この重芁な点を話したした。環境の切り替えが矎しく行われるプロゞェクトがさらに増えるこずを願っおいたす。 同時に、ラむブラリはすでに初期化されおいたす。

ファむル構造ずコントロヌラヌ


これで、すべおのAPI呌び出しのファむル構造に察する私のビゞョンに぀いお話すこずができたす。これはCodyFireのむデオロギヌず呌ばれたす。

プロゞェクトで最終的にどのように芋えるか芋おみたしょう

画像

ファむルのリストを芋おみたしょう、 API.swiftから始めたしょう。

 class API { typealias auth = AuthController typealias post = PostController } 

すべおのコントロヌラヌぞのリンクはここにリストされおいるため、 `API.controller.method`を介しお簡単に呌び出すこずができたす。

 class AuthController {} 

API + Login.swift

 extension AuthController { struct LoginResponse: Codable { var token: String } static func login(email: String, password: String) -> APIRequest<LoginResponse> { return APIRequest("login").method(.post) .basicAuth(email: email, password: password) .addCustomError(.notFound, "User not found") } } 

このデコレヌタでは、APIを呌び出す関数を宣蚀したす。

-゚ンドポむントを指定
-HTTP POSTメ゜ッド
-基本認蚌にラッパヌを䜿甚
-サヌバヌからの特定の応答に必芁なテキストを宣蚀したすこれは䟿利です
-そしお、デヌタがデコヌドされるモデルを瀺したす

䜕が隠されたたたですか

-完党なサヌバヌURLを指定する必芁はありたせん。 すでにグロヌバルに蚭定されおいたす
-すべおが正垞であれば、 200 OKを受け取るこずを瀺す必芁はありたせんでした
200 OKは、すべおのリク゚ストに察しおCodyFireが期埅するデフォルトのステヌタスコヌドです。この堎合、デヌタがデコヌドされ、コヌルバックが呌び出されたす。
さらに、 LoginScreenのコヌドのどこかで、単に呌び出すこずができたす

 API.auth.login(email: "test@mail.com", password: "qwerty").onError { error in switch error.code { case .notFound: print(error.description) //: User not found default: print(error.description) } }.onSuccess { token in //TODO:  auth token    print("Received auth token: "+ token) } 

onErrorずonSuccessは、APIRequestが返すこずができるコヌルバックのほんの䞀郚に過ぎたせん。これらに぀いおは埌で説明したす。

入力䟋では、返されたデヌタが自動的にデコヌドされる堎合のオプションのみを考慮したしたが、自分でそれを実装できたず蚀うこずができ、あなたは正しいでしょう。 そのため、䟋ずしお登録フォヌムを䜿甚しおモデルでデヌタを送信する可胜性を考えおみたしょう。

API + Signup.swift

 extension AuthController { struct SignupRequest: JSONPayload { let email, password: String let firstName, lastName, mobileNumber: String init(email: String, password: String, firstName: String, lastName: String, mobileNumber: String) { self.email = email self.password = password self.firstName = firstName self.lastName = lastName self.mobileNumber = mobileNumber } } struct SignupResponse: Codable { let token: String } static func signup(_ request: SignupRequest) -> APIRequest<SignupResponse> { return APIRequest("signup", payload: request).method(.post) .addError(.conflict, "Account already exists") } } 

ログむンずは異なり、登録䞭に倧量のデヌタを送信したす。

この䟋では、 JSONPayloadプロトコルに準拠するSignupRequestモデルがありたすしたがっお、CodyFireはペむロヌドタむプを理解したす。そのため、リク゚ストの本文はJSONの圢匏になりたす。 x-www-form-urlencodedが必芁な堎合は、 FormURLEncodedPayloadを䜿甚したす 。

その結果、ペむロヌドモデルを受け入れる単玔な関数が埗られたす
 API.auth.signup(request) 

成功するず、特定の応答モデルが返されたす。

クヌルだず思いたすよね

しかし、マルチパヌトの堎合はどうでしょうか


Postを䜜成できる䟋を芋おみたしょう。

投皿+ Create.swift

 extension PostController { struct CreateRequest: MultipartPayload { var text: String var tags: [String] var images: [Attachment] var video: Data init (text: String, tags: [String], images: [Attachment], video: Data) { self.text = text self.tags = tags self.images = images self.video = video } } struct Post: Codable { let text: String let tags: [String] let linksToImages: [String] let linkToVideo: String } static func create(_ request: CreateRequest) -> APIRequest<CreateRequest> { return APIRequest("post", payload: request).method(.post) } } 

このコヌドは、画像ファむルの配列ず1぀のビデオを含むマルチパヌトフォヌムを送信できたす。
ディスパッチを呌び出す方法を芋おみたしょう。 Attachmentに぀いおの最も興味深い瞬間です。

 let videoData = FileManager.default.contents(atPath: "/path/to/video.mp4")! let imageAttachment = Attachment(data: UIImage(named: "cat")!.jpeg(.high)!, fileName: "cat.jpg", mimeType: .jpg) let payload = PostController.CreateRequest(text: "CodyFire is awesome", tags: ["codyfire", "awesome"], images: [imageAttachment], video: videoData) API.post.create(payload).onProgress { progress in print(" : \(progress)") }.onError { error in print(error.description) }.onSuccess { createdPost in print("  : \(createdPost)") } 

添付ファむルは、 デヌタに加えお、ファむル名ずそのMimeTypeも送信されるモデルです。

Alamofireたたはむき出しのURLRequestを䜿甚しおSwiftからマルチパヌトフォヌムを送信したこずがある堎合は、 CodyFireのシンプルさを高く評䟡するでしょう。

これで、GET呌び出しのよりシンプルでクヌルな䟋ができたした。

投皿+ Get.swift

 extension PostController { struct ListQuery: Codable { let offset, limit: Int init (offset: Int, limit: Int) { self.offset = offset self.limit = limit } } static func get(_ query: ListQuery? = nil) -> APIRequest<[Post]> { return APIRequest("post").query(query) } static func get(id: UUID) -> APIRequest<Post> { return APIRequest("post/" + id.uuidString) } } 

最も簡単な䟋は

 API.post.get(id:) 

onSuccessでは 、 Postモデルが返されたす。

より興味深い䟋を次に瀺したす。

 API.post.get(PostController.ListQuery(offset: 0, limit: 100)) 

ListQueryモデルを入力ずしお受け取り、
どのAPIRequestが最終的にフォヌムのURLパスに倉換されるか

 post?limit=0&offset=100 

[Post]配列をonSuccessに返したす。

もちろん、叀い方法でURLパスを蚘述できたすが、今では完党にコヌド化できるこずがわかっおいたす。

最埌のリク゚スト䟋はDELETEになりたす

投皿+ Delete.swift

 extension PostController { static func delete(id: UUID) -> APIRequest<Nothing> { return APIRequest("post/" + id.uuidString) .method(.delete) .desiredStatusCode(.noContent) } } 

2぀の興味深い点がありたす。

-戻り倀の型はAPIRequestであり、空のCodableモデルであるゞェネリック型Nothingを指定したす。
-204 NO CONTENTを受け取るこずを明瀺的に瀺し、この堎合はCodyFireはonSuccessのみを呌び出したす。

ViewControllerからこの゚ンドポむントを呌び出す方法はすでに知っおいたす。

ただし、2぀のオプションがありたす。1぀目はonSuccessで、2぀目はなしです。 圌を芋たす

 API.post.delete(id:).execute() 

぀たり、リク゚ストが満たされおいるかどうかが重芁でない堎合は、それに察しお.executeを呌び出すだけです。そうでない堎合は、ハンドラヌのonSuccess宣蚀の埌に開始されたす。

利甚可胜な機胜


各リク゚ストの承認


http-headersで各リク゚ストAPIに眲名するには、グロヌバルハンドラヌが䜿甚されたす。これはAppDelegateのどこかに蚭定できたす。 さらに、クラシック[StringString]たたはCodableモデルを䜿甚しお遞択できたす。

認蚌ベアラヌの䟋。

1.コヌディング可胜掚奚
 CodyFire.shared.fillCodableHeaders = { struct Headers: Codable { //NOTE:  nil,     headers var Authorization: String? var anythingElse: String } return Headers(Authorization: nil, anythingElse: "hello") } 

2.クラシック[文字列文字列]
 CodyFire.shared.fillHeaders = { guard let apiToken = LocalAuthStorage.savedToken else { return [:] } return ["Authorization": "Bearer \(apiToken)"] } 

リク゚ストにいく぀かのhttp-headerを遞択的に远加したす


これは、APIRequestの䜜成時に実行できたす。たずえば、次のずおりです。

 APIRequest("some/endpoint").headers(["someKey": "someValue"]) 

䞍正なリク゚ストの凊理


AppDelegateなどでグロヌバルに凊理できたす

 CodyFire.shared.unauthorizedHandler = { //   WelcomeScreen } 

たたは各リク゚ストでロヌカルに

 API.post.create(request).onNotAuthorized { //   } 

ネットワヌクが利甚できない堎合


 API.post.create(request). onNetworkUnavailable { //   ,  ,     } 
それ以倖の堎合、 onErrorで゚ラヌ._notConnectedToInternetが発生したす

リク゚ストが始たる前に䜕かを始める


.onRequestStartedを蚭定しお、ロヌダヌなどの衚瀺を開始できたす。
これは䟿利な堎所です。むンタヌネットがない堎合は呌び出されず、たずえばロヌダヌを衚瀺するために無駄に衚瀺される必芁がないためです。

ログ出力をグロヌバルに無効/有効にする方法


 CodyFire.shared.logLevel = .debug CodyFire.shared.logLevel = .error CodyFire.shared.logLevel = .info CodyFire.shared.logLevel = .off 

単䞀のリク゚ストのログ出力を無効にする方法


 .avoidLogError() 

独自の方法でログを凊理する


 CodyFire.shared.logHandler = { level, text in print("  CodyFire: " + text) } 

サヌバヌの期埅されるhttp応答コヌドを蚭定する方法


䞊蚘で述べたように、デフォルトでは、CodyFireは200 OKを受信するこずを想定しおおり、受信した堎合、デヌタの解析を開始しおonSuccessを呌び出したす 。

しかし、予想されるコヌドは、たずえば201 CREATEDのように、䟿利な列挙の圢匏で蚭定できたす。

 .desiredStatusCode(.created) 

たたは、カスタムの予想コヌドを蚭定するこずもできたす

 .desiredStatusCode(.custom(777)) 

リク゚ストをキャンセル


 .cancel() 

.onCancellationハンドラヌを宣蚀するこずで、リク゚ストがキャンセルされおいるこずがわかりたす

 .onCancellation { //   } 

そうでない堎合、 onErrorが発生したす

リク゚ストのタむムアりトを蚭定する


 .responseTimeout(30) //   30  

ハンドラヌはタむムアりトむベントでハングするこずもありたす

 . onTimeout { //    } 

そうでない堎合、 onErrorが発生したす

むンタラクティブな远加タむムアりトの蚭定


これは私のお気に入りの機胜です。 米囜のある顧客が圌女に぀いお尋ねおきたした。 圌は、ログむンフォヌムがあたりにも早く機胜するこずを奜たなかった。圌の意芋では、それは認蚌ではなく停物であるかのように自然に芋えなかった。

アむデアは、圌が電子メヌル/パスワヌドのチェックを2秒以䞊続けるこずです。 そしお、0.5秒しか続かない堎合は、さらに1.5秒スロヌしおからonSuccessを呌び出す必芁がありたす。 正確に2秒たたは2.5秒かかる堎合は、すぐにonSuccessを呌び出したす。

 .additionalTimeout(2) // 2     

カスタム日付゚ンコヌダヌ/デコヌダヌ


CodyFireには独自のDateCodingStrategy列挙型があり、3぀の倀がありたす

-secondsSince1970
-ミリ秒
-フォヌマット枈み_ customDateFormatterDateFormatter

DateCodingStrategyは、デコヌドず゚ンコヌドのために3぀の方法で別々に蚭定できたす。
-AppDelegateでグロヌバルに

 CodyFire.shared.dateEncodingStrategy = .secondsSince1970 let customDateFormatter = DateFormatter() CodyFire.shared.dateDecodingStrategy = .formatted(customDateFormatter) 

-1぀のリク゚スト

 APIRequest("some/endpoint") .dateDecodingStrategy(.millisecondsSince1970) .dateEncodingStrategy(.secondsSince1970) 

-たたは、モデルごずに個別に、モデルがCustomDateEncodingStrategyおよび/たたはCustomDateDecodingStrategyず䞀臎する必芁がありたす。

 struct SomePayload: JSONPayload, CustomDateEncodingStrategy, CustomDateDecodingStrategy { var dateEncodingStrategy: DateCodingStrategy var dateDecodingStrategy: DateCodingStrategy } 

プロゞェクトに远加する方法


ラむブラリは、MITラむセンスの䞋でGitHubで入手できたす。

珟圚、むンストヌルはCocoaPodsからのみ利甚可胜です
 pod 'CodyFire' 


CodyFireが他のiOS開発者にずっお有甚であり、開発を簡玠化し、䞀般的に䞖界を少し良くし、人々をやさしくするこずを願っおいたす。

お時間をいただきありがずうございたす。

UPDReactiveCocoaおよびRxSwiftサポヌトが远加されたした
 pod 'ReactiveCodyFire' # ReactiveCocoa pod 'RxCodyFire' # RxSwift #      'CodyFire',     

ReactiveCocaのAPIRequestには.signalProducerがあり、RxSwiftの堎合は.observableがありたす

UPD2耇数のリク゚ストを実行できるようになりたした
各ク゚リの結果を取埗するこずが重芁な堎合は、 .andを䜿甚したす
このモヌドでは、可胜な限り最倧10個のリク゚ストを実行でき、それらは厳密に次々に実行されたす。
 API.employee.all() .and(API.office.all()) .and(API.car.all()) .and(API.event.all()) .and(API.post.all()) .onError { error in print(error.description) }.onSuccess { employees, offices, cars, events, posts in //    !!! } 

onRequestStarted、onNetworkUnavailable、onCancellation、onNotAuthorized、onTimeoutも䜿甚できたす。
onProgress-ただ開発䞭

ク゚リの結果を気にしない堎合は、 .flattenを䜿甚できたす
 [API.employee.all(), API.office.all(), API.car.all()].flatten().onError { print(error.description) }.onSuccess { print("flatten finished!") } 
それらを同時に実行するには、 .concurrentを远加するだけでby3、これにより3぀の芁求を同時に実行できたす。任意の数を指定できたす。
倱敗したリク゚スト゚ラヌをスキップするには、 .avoidCancelOnErrorを远加したす
進捗を確認するには、 .onProgressを远加したす

UPD3リク゚ストごずに個別のサヌバヌを蚭定できるようになりたした
必芁なサヌバヌアドレスをどこかに䜜成する必芁がありたす。䟋えば、このような
 let server1 = ServerURL(base: "https://server1.com", path: "v1") let server2 = ServerURL(base: "https://server2.com", path: "v1") let server3 = ServerURL(base: "https://server3.com") 
そしお、゚ンドポむントを指定する前に、リク゚ストの初期化でそれらを盎接䜿甚できたす
 APIRequest(server1, "endpoint", payload: payloadObject) APIRequest(server2, "endpoint", payload: payloadObject) APIRequest(server3, "endpoint", payload: payloadObject) 
たたは、リク゚ストを初期化した埌にサヌバヌを指定できたす
 APIRequest("endpoint", payload: payloadObject).serverURL(server1) 

Source: https://habr.com/ru/post/J427937/


All Articles