哈Ha!
从Swift 4开始,我们可以使用新的Codable协议,从而可以轻松地对模型进行编码/解码。 我的项目中有许多用于API调用的代码,并且在过去的一年里,我通过杀死重复的代码并甚至对多部分请求和url查询参数使用Codable,将大量的代码优化为非常轻巧,简洁和简单的东西。 在我看来,它是用于发送请求和解析服务器响应的几个出色的类。 以及方便的文件结构,它是每组请求的控制器,我在后端使用Vapor 3时扎根。 几天前,我将所有开发内容分配到一个单独的库中,并将其命名为CodyFire。 我想在这篇文章中谈论她。
免责声明
CodyFire基于Alamofire,但它不仅仅是Alamofire的包装,它是与iOS的REST API一起使用的整个系统方法。 这就是为什么我不用担心Alamofire会看到第五版将提供Codable支持的原因,因为 它不会杀死我的创造。
初始化
让我们从远处开始,即我们经常拥有三台服务器:
dev-对于开发,我们从Xcode开始
阶段 -在发布前进行测试,通常在TestFlight或InHouse中进行
产品 -生产,用于AppStore
当然,许多iOS开发人员都知道Xcode中存在
环境变量和启动方案,但是在我(超过8年的实践)中,有90%的开发人员在测试时或组装前以一定的常数手动编写了正确的服务器,这是我想通过展示一个正确的例子来解决这个问题。
默认情况下,CodyFire自动确定应用程序当前在哪个环境中运行,这非常简单:
#if DEBUG //DEV environment #else if Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" { //TESTFLIGHT environment } else { //APPSTORE environment } #endif
这当然是在幕后,在AppDelegate的项目中,您只需要注册三个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中测试开发,登台和生产服务器,为此,我敦促您使用启动方案。

提示:在“ 管理方案”部分中,请不要忘记选中每个方案的“共享”框,以便项目中的所有开发人员都可以使用它们。
在每种方案中,您都需要编写环境变量`env`,该变量可以采用三个值:dev,testFlight和appStore。

为了使这些方案与CodyFire一起使用,您需要在初始化CodyFire之后向
AppDelegate.didFinishLaunchingWithOptions添加以下代码
CodyFire.shared.setupEnvByProjectScheme()
而且,项目的老板或测试人员经常可能会要求您在
LoginScreen上的某个位置即时切换服务器。 使用CodyFire,您可以通过在一行中切换服务器,更改环境来轻松实现此目的:
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 OK200 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) } }
此代码将能够发送包含图像文件数组和一个视频的多部分表单。
让我们看看如何调用调度。 这是关于
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) } }
有两个有趣的观点。
-返回类型为APIRequest,它指定通用类型
Nothing ,它是一个空的
Codable模型。
-我们明确表示希望收到204个NO内容,在这种情况下CodyFire仅会调用
onSuccess 。
您已经知道如何从ViewController调用此终结点。
但是有两个选项,第一个带有
onSuccess ,第二个没有。 我们来看他
API.post.delete(id:).execute()
就是说,如果您对请求是否成功并不重要,则可以简单地对其调用
.execute() ,否则,它将在处理程序的
onSuccess声明之后开始。
可用功能
每个请求的授权
要使用任何http-header签名每个请求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.经典[String:String]
CodyFire.shared.fillHeaders = { guard let apiToken = LocalAuthStorage.savedToken else { return [:] } return ["Authorization": "Bearer \(apiToken)"] }
有选择地向请求添加一些http标头
这可以在创建APIRequest时完成,例如:
APIRequest("some/endpoint").headers(["someKey": "someValue"])
处理未授权的请求
您可以全局处理它们,例如在
AppDelegate中 CodyFire.shared.unauthorizedHandler = {
或在每个请求中本地
API.post.create(request).onNotAuthorized {
如果网络不可用
API.post.create(request). onNetworkUnavailable {
否则在
onError中,您将收到错误
._notConnectedToInternet在请求开始之前先开始
您可以设置
.onRequestStarted并开始显示例如加载程序。
这是一个方便的地方,因为在没有Internet的情况下不会调用它,例如,您不必徒劳地显示装载程序。
如何全局禁用/启用日志输出
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)
处理程序也可以挂在超时事件上
. onTimeout {
否则会
引发onError设置交互式额外超时
这是我最喜欢的功能。 一位来自美国的顾客曾经问过我关于她的事,因为 他不希望登录表单的生成速度太快,他认为登录表单看起来不自然,好像是假的,不是授权。
这个想法是他希望电子邮件/密码检查持续2秒或更长时间。 如果仅持续0.5秒,则需要再扔1.5秒,然后再调用
onSuccess 。 如果恰好需要2或2.5秒,请立即调用
onSuccess 。
.additionalTimeout(2)
自定义日期编码器/解码器
CodyFire有其自己的
DateCodingStrategy枚举,其中有三个值
-秒自1970年以来
自1970年以来
-格式化(_ customDateFormatter:DateFormatter)
可以通过三种方式分别设置
DateCodingStrategy ,以进行解码和编码
-在
AppDelegate中全局
CodyFire.shared.dateEncodingStrategy = .secondsSince1970 let customDateFormatter = DateFormatter() CodyFire.shared.dateDecodingStrategy = .formatted(customDateFormatter)
-一个请求
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'
用于ReactiveCoca的
APIRequest将具有
.signalProducer和用于RxSwift的
.observableUPD2:现在您可以运行多个请求如果获取每个查询的结果对您来说很重要,请使用
.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),这将允许同时执行三个请求,可以指定任意数量。
要跳过失败的查询错误,请添加
.avoidCancelOnError()要获得进度,请添加
.onProgressUPD3:现在您可以为每个请求设置单独的服务器有必要在某个地方创建必要的服务器地址,例如这样
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)