Girders for Swift
Netcetera 开发的用于构建 iOS 应用的框架。
Girders 是什么?
如果您询问谷歌,桁架是一根用于桥梁建设和大型建筑框架的大型钢梁或复合结构。受到这一灵感的启发,Girders 是 Netcetera 开发的许多框架的标准名称。
GirdersSwift 是一个新框架,用 Swift 编写,包含多个您可能认为在应用中很有用的模块
- 网络
- 序列化
- 依赖注入
- 存储
- 配置
我们计划在将来添加更多新模块,所有这些模块都将开源。欢迎就扩展框架提出想法。
安装
请参见以下子部分了解有关不同安装方法的详细信息。
CocoaPods
为了使用 GirdersSwift 与 CocoaPods,你需要更新你的 Podfile,增加 GirdersSwift 作为 pod。
pod 'GirdersSwift', 'version'
Swift Package Manager
要使用 Swift Package Manager (SPM) 将 GirdersSwift 集成到你的 Xcode 项目中,在 Xcode 中打开你的 App,并选择 文件 > 添加包。然后在右上角的搜索字段中输入 git 仓库 URL:https://github.com/netceteragroup/GirdersSwift.git
通过选择 SDK 版本(或分支),你可以定义你的依赖规则,然后点击“添加包”按钮。如果你有一个使用 Package.swift 文件管理依赖的项目,你可以通过指定名称来指定目标
.package(name: "GirdersSwift", url: "https://github.com/netceteragroup/GirdersSwift", from: "VERSION_NUMBER"),
注意:Girders Swift 从版本 "0.6.0" 开始支持 SPM
核心理念
"一个好的架构师最大化不做的决策数量"——罗伯特·马丁。 ——罗伯特·马丁。
框架的核心理念是尽可能小,同时尽可能独立于其他框架。一方面,我们不想在 iOS 开发中有许多良好框架的情况下重造轮子;另一方面,我们也不想过度依赖第三方框架。这就是我们为何大量使用协议的原因。
本框架不会强迫你在应用架构或异步编程抽象上做出选择。你可以自由选择是否使用 completionHandlers、futures/promises 或 RX/Combine 扩展。这个决定由项目来负责。
本框架的一个目标是为其功能提供足够的扩展点,而不改变其底层实现。在本框架中使用了装饰器模式。
模块
网络请求
当你考虑一个网络框架应该做什么时,这个任务相当简单
- 创建一个请求
- 发送请求
- 处理响应
第一部分,创建请求看起来是最复杂的部分。存在许多不同的请求 - 具有不同的请求头、SSL凭据、基本认证、不同的HTTP方法(GET、POST 及其他6种)、不同的参数、请求体等等。一个好的网络库应该提供一个简单而稳健的方式来创建所有这些不同的请求类型。
第二部分可能是最简单的一部分 - 创建一个包装系统库的封装器,该封装器负责通过网络发送请求并接收响应。在这里,你会使用 NSURLSession 或 NSURLConnection。
请求完成后,其响应应被妥善处理 - 这里你需要检查请求是否成功。如果是成功的,根据 Content-Type 进行适当的处理 - 可能解析 json/xml 响应,并根据它提供一些已创建的模型对象/结构体返回给调用者。这里是附加适当响应序列化程序的地方。
我们有自己的请求、响应和错误类型。我们网络库的核心是我们的 HTTP 协议。我们使用 Apple 的 NSURLSession 提供其一个实现。
我们这里的核心方法是:
func executeRequest<T>(request: Request,
completionHandler: @escaping (Result<Response<T>, Error?>) -> Void)
自定义请求
我们的请求类型是不可变的 - 当请求创建并填写所有属性后,这些值不应再改变。我们在多线程环境中工作,因此引入可变性会带来更多的复杂性和错误。
存在一个可变的请求版本,由我们的 RequestGenerator 协议使用,该协议的任务是构建和自定义请求。请求的自定义是通过提供一系列纯函数完成的,这些函数接收一个请求,添加额外信息(例如头信息)并返回修改后的请求副本。
这种方法的优点是可以轻松组合提供的函数以创建不同的请求类型,而无需修改实现。你还可以定义自己的函数来装饰请求。你可以使用前向箭头操作符来构建请求,以提高清晰度。
在你的 RequestGenerator 中重写 generateRequest(withMethod:) 以自定义请求。例如,如果你想为请求添加 JSON 支持,你可以做以下操作:
public func generateRequest(withMethod method: HTTPMethod) -> MutableRequest {
return request(withMethod: method) |> withJsonSupport
}
如果你想添加 SSL 凭据和基本认证,稍后,你唯一需要做的更改是
public func generateRequest(withMethod method: HTTPMethod) -> MutableRequest {
return request(withMethod: method) |> withJsonSupport |> withSSLCredentials |> withBasicAuth
}
你可以创建尽可能多的请求生成器,并使用这些函数在应用中构建不同类型的请求。所有请求生成器都可以组合相同的函数来创建不同类型的请求,而不需要重复。
根据你想要的定制程度,有几种方便的初始化方法来创建请求。这里是最灵活的选项。
public init(URL: URL,
method: HTTPMethod,
parameters: [String: Any],
queryParameters: [String: Any] = [:],
requestGenerator: RequestGenerator)
端点
直接与URL打交道可能是一项烦琐且容易出错的工作。这就是为什么我们提供了一个额外的抽象层——ServiceEndpoint,灵感来源于Moya框架(https://github.com/Moya/Moya)。服务端点的目标是能够以类型安全的模式创建REST服务端点URL。
端点也是协议。实现可以是任何类型,例如枚举、结构体或类。端点在底层使用请求生成器,因此您可以为每个端点定义自定义请求生成器,甚至为每个URL或方法类型定义。
以下是一个端点的示例
enum PaymentEndpoint {
case RechargeCredit
case CheckCardStatus
}
struct SecureRequestGenerator : RequestGenerator {
func generateRequest(method: HTTPMethod) -> MutableRequest {
return request(withMethod: method) |> withBasicAuth
}
}
extension PaymentEndpoint : ServiceEndpoint {
var baseURL: NSURL {
get {
return NSURL(string: "paymentBaseUrl")!
}
}
var method: HTTPMethod {
get {
return .POST
}
}
var path: String {
if self == RechargeCredit {
return "/rechargeCredit/"
} else {
return "/checkCardStatus/"
}
}
var requestGenerator: RequestGenerator {
get {
return SecureRequestGenerator()
}
}
var parameters: [String : AnyObject] {
get {
return ["token" : "someToken"]
}
}
}
完成此设置后,使用网络代码的用户只需在创建请求时指定他们想要调用的端点即可。
let rechargeCredit = PaymentEndpoint.RechargeCredit
let request = Request(endpoint: rechargeCredit)
当使用枚举时,您可以使用关联值向端点提供参数。例如
enum AccountEndpoint {
case Login(String, String)
case CreateAccount(String, String, String)
}
let createAccount = AccountEndpoint.CreateAccount(name, email, password)
let request = Request(endpoint: createAccount)
处理响应
在我们的 executeRequest 方法完成处理程序中,我们使用了 Result
public enum Result<T, NSError> {
case Success(T)
case Failure(Error?)
}
如果请求成功,结果是 Response 类型。这是我们自定义的结构,其中包含从响应中预期的所有必要事物,如 statusCode、body、bodyObject、responseHeaders和url。如果请求失败,我们返回符合Error协议的错误。我们还提供了一个 ResponseError 枚举,实现常见的错误状态码。
要处理响应,您可以定义自己的响应处理程序,通过实现 ResponseHandler 协议。这允许您在不修改内部实现的情况下附加额外的逻辑到响应处理流程中。
已经有一个JSON处理程序,它可以返回包含解析数据的字典。您可以通过实现 ResponseHandler 协议来定义自己的解析逻辑和自定义对象。目前尚未使用Apple的 Codable,但计划在将来使用。
以下是处理程序操作的示例。
typealias ResultHandler = ((Result<Response<[String : Any]>, Error?>) -> Void)
typealias ErrorHandler = ((Error?) -> Void)
func booleanHandler(result: @escaping (Bool) -> Void,
error: @escaping (Error?) -> Void) -> ResultHandler {
let handler: ResultHandler = { requestResult in
switch requestResult {
case .Success(_):
result(true)
case .Failure(let requestError):
error(requestError)
}
}
return handler
}
func register(withRequest request: RegisterRequest,
result: @escaping (Bool) -> Void,
error: @escaping (Error?) -> Void) {
let registerRequest = self.request(forApiKey: .Users,
parameters: request.toParameters(),
readOnly: false)
httpClient.executeRequest(request: registerRequest,
completionHandler: booleanHandler(result: result,
error: error))
}
Combine扩展
该框架现在支持Combine扩展。您可以执行网络请求,它返回一个 Publisher 或 Future。使用 Decodable 协议,响应自动解析并作为模型返回给调用者。
func loadSensors() -> AnyPublisher<[Sensor], Error> {
let request = Request(endpoint: PulseEndpoint.sensors)
let publisher: AnyPublisher<[SensorResponse], Error> = httpClient.executeRequest(request: request)
return publisher.map { (response) -> [Sensor] in
self.convert(sensorsResponse: response)
}
.eraseToAnyPublisher()
}
private func convert(sensorsResponse: [SensorResponse]) -> [Sensor] {
let sensors = sensorsResponse.map { (sensorResponse) -> Sensor in
return convert(sensorResponse: sensorResponse)
}
return sensors
}
Combine扩展的使用示例可以在这里找到:https://github.com/martinmitrevski/GirdersCombineSample。
错误处理
如果您不使用Combine,则可以创建自己的错误处理器来自动处理常见的错误处理逻辑。例如,假设您的应用程序通过可以过期的令牌访问REST API。在这种情况下,我们希望通过静默登录用户来尝试刷新令牌。以下是使用错误处理器进行这种方式的方法。
func standardErrorHandler(_ error: @escaping ErrorHandler) -> ErrorHandler {
let handler: ErrorHandler = { anError in
guard let responseError = anError as? ResponseError else {
error(anError)
return
}
if responseError == .Unauthorized {
autoLogin()
} else {
error(anError)
}
}
return handler
}
第三方扩展
PromiseKit
如果您喜欢使用Promise,则可以使用我们对PromiseKit的HTTP实现的扩展。
func executeRequestAsync<T>(request: Request) -> Promise<Response<T>> {
return Promise { seal in
executeRequest(request: request,
completionHandler: { (result: Result<Response<T>, Error?>) in
switch result {
case .Failure(let error):
seal.reject(error!)
case .Success(let data):
seal.fulfill(data)
}
})
}
}
您可以为任何需要的异步编程抽象创建类似的扩展。即将添加RXSwift。
配置
通常,当开发与REST服务通信的应用程序时,我们需要支持多个环境,例如开发、测试和生产。使用我们的配置类,您可以通过提供不同的plist文件来支持这些环境。对于生产,文件应命名为Configuration.plist,而对于环境,则命名为Configuration-env.plist。
当应用程序在生产模式下运行时,仅使用Configuration文件。否则,这两个配置将合并,其中Configuration-env文件优先。
配置作为Singleton提供,从中获取值相当简单。
let apiURL = Configuration.sharedInstance[Constants.APIURLKey] as? String
安全存储
有一个SecureStorage协议,它定义了将数据保存到安全存储和从安全存储检索数据的方法。这个协议由KeychainStorage类实现,该类使用另一个开源框架KeychainAccess(《https://github.com/kishikawakakatsumi/KeychainAccess》)。
示例用法
KeychainStorage.shared.save(string: username, forKey: Constants.Username)
let username = KeychainStorage.shared.string(forKey: Constants.Username)
工具
日志记录
提供了 LogProtocol,允许您在不同的级别进行日志记录,例如
- 详细输出
- 调试
- 信息
- 警告
- 错误
- 致命错误
提供了一个与开源日志记录器 SwiftyBeaver(https://github.com/SwiftyBeaver/SwiftyBeaver)一起的示例实现。您可以通过在 Configuration.plist 文件中设置 logLevel 值来控制不同环境的日志记录级别。
翻译
如果您需要支持多种语言,但希望与 Android 应用共享文本,我们定义了自己的 XML 格式,我们称之为 trema。仓库中有一个名为 trema.rb 的脚本,它可以将 trema 文件转换为 Apple 的 .strings 格式。下面是我们的 trema 文件的样子
<trema masterLang="de"
noNamespaceSchemaLocation="http://software.group.nca/trema/schema/trema-1.0.xsd">
<text key="app_name">
<context/>
<value lang="de" status="translated">Your DE App Name</value>
<value lang="en" status="translated">Your EN App Name</value>
<value lang="fr" status="translated">Your FR App Name</value>
<value lang="it" status="translated">Your IT App Name</value>
</text>
</trema>
使用 translate 函数通过键引用翻译项。
struct Texts {
static let AppName = translate("app_name")
}
使用 textsCreator.rb 在 Swift 中安全且简单地使用 trema 进行本地化
向 GirdersSwift 添加了一个新的 Ruby 脚本 textsCreator.rb,它创建一个读取自 trema 文件的 Swift 结构体,具有常量属性。该脚本遍历 .trm 文件,并从键创建属性。
使用它非常简单,该脚本接受2个必选参数和1个可选参数。第1个参数是您项目的产品名称,可以使用XCode构建常量${PRODUCT_NAME}进行引用,此参数是必需的,因为在我们创建所需目录中的文件后,我们还需要将其添加到.xcodeproj中。第2个参数是您.trm文件的路径。第3个参数是您希望文件创建的路径。它是您项目根目录的相对路径。例如:src/main/swift/utils/Texts.swift将在您的utils目录中创建一个Texts.swift文件,结构名称将是Texts。如果您省略第3个参数,则脚本将在src/main/resources/trema/Texts.swift中创建您的文件,并带有适当的结构名称。
如果您的项目组织方式不同,并且您省略了第3个参数或使用绝对路径而不是相对路径到您的目录,脚本将失败。这是由于使用ruby中的xcodeproj工具导致的限制,该工具是添加文件到项目所必需的。以下是一个示例。
"${SRCROOT}"/src/main/resources/trema/textsCreator.rb "${PRODUCT_NAME}" "${SRCROOT}/src/main/resources/trema/texts.trm" "src/main/swift/util/TextUtil.swift"
运行项目一次后,您只需使用StructName.property就能在项目的任何地方使用所有trema键。此脚本还将每个trema条目的上下文添加到所有属性的注释上方,以帮助进行文档编制。
日期字符串工具
这些工具可以用于将日期转换为字符串以及相反的转换。支持转换为从RFC822和RFC3339解析日期。
依赖注入
Girders Swift包含一个控制反转容器,可以简化依赖注入的过程。使用依赖注入可以提高可测试性,使组件解耦更强,并且容易切换实现。
为了实现依赖注入,每个需要注入的“服务”应首先定义为一个协议(一个契约),其他类将使用这个协议。例如
protocol SomeServiceProtocol {
func someMethod() -> String
}
实现类看起来类似于这样
class SomeService: SomeServiceProtocol {
func someMethod() -> String {
return UUID().uuidString
}
}
SomeService的实例可以根据需要创建,也可以只创建一次(即使用单例模式)。
为了使用单例模式,协议和工厂方法需要像这样注册到< strong>Container
Container.addSingleton { () -> SomeServiceProtocol in
return SomeService()
}
如果每次需要时都应创建一个新的实例,请使用以下命令:
Container.addPerRequest { () -> SomeServiceProtocol in
return SomeService()
}
为了解析某个协议的实例,请使用强大的
let resolvedInstance: SomeServiceProtocol = Container.resolve()
在现实中,可以使用“服务”使用其他服务的方法。为了解析依赖关系,应使用惰性属性
class SomeOtherService {
lazy var someService: SomeServiceProtocol = Container.resolve()
func otherServiceMethod() {
...
let someValue = someService.someMethod()
...
}
}
现在可以随时切换 SomeServiceProtocol 的实现。开发者甚至可以在编写单元测试时注册模拟实现。通过这种方法,您可以创建互联的服务、服务级联等。您还可以将服务注入到您的视图或视图控制器中。
注意:虽然这个容器允许您创建循环引用,但这并不意味着您应该这样做。请注意避免创建循环引用并在您的应用程序中引入内存泄漏。
应将协议和工厂方法的注册放在应用程序的 AppDelegate 中完成。
改进区域
- 添加更多单元测试
- 改进文档
- 添加下载大文件的方法
- 扩展 Configuration 类
- 添加 RXSwift 支持
- 添加 XML 支持
- 添加持久层
- 等等。