在 iOS 开发中,将装备类信息与代码混合在一起是很常见的做法,虽然这种办法在开发过程中很便利,但也会带来一些潜在的问题。虽然 iOS 社区提供了 Alamofire 等各种超卓的网络东西包,但这种问题在网络层中尤为杰出。不过,有多种办法能够在更高层次上有用办理 URL 和 Endpoint,然后减轻其间的一些应战。
AF.request("https://bff.cyou/")
AF.request("https://bff.cyou/post", method: .post)
AF.request("https://bff.cyou/put", method: .put)
AF.request("https://bff.cyou/delete", method: .delete)
例如,经过 extensions 进行办理。那有没有愈加高雅的方式呢?
extension URL {
static var recommendations: URL {
URL(string: "https://ilove.pet/recommendations")!
}
static func article(withID id: Article.ID) -> URL {
URL(string: "https://ilove.pet/articles/(id)")!
}
}
操控回转(IoC)是一种设计形式,在这种形式下,计算机程序中自定义编写的部分从通用框架接收操控流。Spring 框架在 Java 编程语言中引进这一概念以及依赖注入(Dependency Injection)方面发挥了关键作用。这种标准化大大改善了 Java 中大型项目的办理。在 iOS 开发领域,运用 Xcode 经过 plist 装备文件办理 URL 的做法也符合操控回转 (IoC) 的核心原则。
1. 运用 yaml 描述装备
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>baseURL</key>
<string>https://api.bff.cyou</string>
<key>services</key>
<array>
<dict>
<key>name</key>
<string>login</string>
<key>path</key>
<string>/login</string>
<key>description</key>
<string>login</string>
<key>bypass</key>
<true/>
<key>method</key>
<string>POST</string>
</dict>
<dict>
<key>name</key>
<string>logout</string>
<key>path</key>
<string>/logout</string>
<key>description</key>
<string>logout</string>
<key>method</key>
<string>POST</string>
</dict>
</array>
</dict>
</plist>
以上是一个经过 Property List 定义的 endpoint,能够看到适当的繁琐,我们测验把它变成 yaml 试试。
baseURL: https://api.bff.cyou
services:
- { name: login, path: /login, description: login, bypass: true }
- { name: logout, path: /logout, description: logout }
能够看到,经过 yaml 描述的网络装备文件适当的简洁,便于保护。在 Swift 中能够经过 Yams 这个第三方 framework 解析 yaml 文件。
enum ConfigError: Error {
case NotFound(String)
}
// Use for parse config contents with yaml format.
struct Config: Codable, Equatable {
struct Environment: Codable, Equatable {
var env: String
var url: String
}
enum Method: String, Codable, Equatable {
case GET = "GET"
case POST = "POST"
case PUT = "PUT"
case DELETE = "DELETE"
}
struct Endpoint: Codable, Equatable {
var name: String
var path: String
var description: String?
var method: Method?
var baseAPI: String?
var bypass: Bool?
}
}
class ConfigManager {
static let shared = ConfigManager()
fileprivate let configName = "config.yml"
var config: Config!
init() {
do {
self.config = try loadConfig()
} catch {
fatalError("Can not load config file.")
}
}
private func loadConfig() throws -> Config {
if let path = Bundle.main.path(forResource: configName, ofType: nil) {
let contents = try String(contentsOfFile: path, encoding: .utf8)
let config = try YAMLDecoder().decode(Config.self, from: contents)
return config
} else {
throw ConfigError.NotFound("Can not find config file.")
}
}
}
2. 网络核心逻辑的处理
经过装备的信息,我们能够写一个简单的网络恳求逻辑,只需求传入 endpoint 的途径名称即可,不需求再额定保护。别的,默认状况下的恳求 host 也能够直接从装备读取。
// Some basic definition has been omitted, complete codes can be found at ...
public class NetworkCore {
public static let shared = NetworkCore()
var baseURL = ""
private(set) var taskQueue = NSMutableArray()
lazy var sessionManager: Alamofire.Session = Alamofire.Session(configuration: URLSessionConfiguration.default, startRequestsImmediately: false)
private init() {
let configManager = ConfigManager.shared
self.baseURL = configManager.config.baseURL
}
/**
Send request.
Usage exapmle: NetworkCore.shared.request(api: ServiceName.Login, complete: nil)
*/
@discardableResult
func request<T, K>(api: T, parameters: K = Alamofire.Empty?.none, headers: HTTPHeaders = HTTPHeaders(), complete: @escaping (DataResponse<Any, Error>) -> Void) -> DataRequest where T: APICovertable, K: Encodable {
let apiItem: APIItem = api.convert() as! APIItem
let url: URLConvertible = self.baseURL + apiItem.path
// Set http headers, token, etc.
let request = self.sessionManager.request(url, method: apiItem.method, parameters: parameters, headers: headers)
request.validate().responseData { dataResponse in
// deal response
}.resume()
return request
}
}
直接运转这个代码会报错,因为我们还没定义ServiceName
呢,假如手工创建这个数据结构,看起来会是这样的。
/// ServiceName is a enum for all services in this project.
public enum ServiceName: String, Codable, Equatable {
/// Login
case Login = "login"
/// Logout
case Logout = "logout"
}
能够看到这个 ServiceName
的枚举和yaml
中装备彻底一致!假如能够自动化的生成这个数据结构就好了。
3. 完结这个拼图!
假如想要自动化的完结这个过程,我们需求运用 Golang 先写一个命令行东西,以完成以下的工作流。
flowchart LR
start{start} --> step1[1. 经过装备生成 ServiceName] --> step2[2. 编译项目] --> ed{end}
ioc-script
首要的代码片段:
func renderTemplate(config *ConfigModule, out string) error {
tc := res.Template
timeSlot := "#CREATE_TIME#"
contentSlot := "#CONTENT#"
now := time.Now().Format("2006/01/02 15:04:05")
tc = strings.Replace(tc, timeSlot, now, -1)
var content string
for _, serv := range config.Services {
if serv.Description != "" {
content += "t/// " + serv.Description + "n"
} else {
content += "n"
}
content += fmt.Sprintf("tcase %s = "%s"n", capitalize(serv.Name), serv.Name)
}
tc = strings.Replace(tc, contentSlot, content, -1)
if err := os.WriteFile(out, []byte(tc), 0755); err != nil {
return fmt.Errorf("write file %s error. %s", out, err)
}
return nil
}
这个 golang 脚本的功用是读取项目中的 config.yml,运用文件内的装备替换预制的脚本,并将包含 ServiceName
的 swift 文件写入指定的途径。
将脚本编译为二进制文件 ioc-script
,放到项目目录中。因为 Swift 编译器要求在编译代码时,Swift 文件不能发生变更,所以有2个机遇能够履行这个脚本。
在 pod install
的时分履行,假如你运用了Cocoapods
作为包办理器。如以下 Podfile 的一个示例。
post_install do |installer|
puts "依据装备生成Service.swift"
system("./scripts/ioc-script -c './ioc-demo/config.yml' -o './ioc-demo/network/Services.swift'")
puts "生成Service.swift完结!"
end
**新建一个 Aggregate
的 Target 履行,假如你运用了 SPM 、 Carthage 或许其他状况。**这个 Target 必须早于项目的编译阶段。在新建的 Target 中,增加一个 Run Script
阶段,履行以下的命令行,其间途径需求依据你项目的具体状况进行修正。
./ioc-script/dist/ioc-script -c "./$PROJECT_NAME/config.yml" -o "./$PROJECT_NAME/network/Services.swift"
你需求单独运转一次 ioc-script
这个 Target 以生成一次 ServiceName,便利编码过程。之后,当你构建项目时,这个脚本会保证每次都依据 config.yml 中的内容更新这个类。
在示例的项目中,首要的截图如下。
4. 一些问题
Intel 和 Apple silicon CPU问题
默认状况下,golang 总会依据当前的 CPU 架构编译二进制文件,假如你的团队中同时有 Intel 和 Apple silicon 的设备,这个脚本可能在不同的 CPU 架构下不能正常工作。
解决计划其实很简单,就想在 iOS SDK 开发过程中,我们运用 lipo 将不同架构的 framework 进行合并,现在也能够运用这个命令将 golang 编译发生的二进制文件进行合并操作,只需经过以下的 Makefile 脚本对 golang 代码进行编译。编译发生的 fat binary 即可同时支撑 Intel 和 Apple silicon 两种 CPU 架构。
GO ?= $(shell command -v go 2> /dev/null)
GOPATH ?= $(shell go env GOPATH)
GO_BUILD_FLAGS ?=
BINARY_NAME ?= "ioc-script"
LIPO ?= $(shell command -v lipo 2> /dev/null)
.PHONY: dist
dist:
rm -rf dist
mkdir -p dist
@echo Build binary for darwin-amd64
GOOS=darwin GOARCH=amd64 $(GO) build -o dist/$(BINARY_NAME)-darwin-amd64
@echo Build binary for darwin-arm64
GOOS=darwin GOARCH=arm64 $(GO) build -o dist/$(BINARY_NAME)-darwin-arm64
@echo Combine binaries into universal binary
# 经过 lipo 命令将 2 个 CPU 架构的二进制包合成为一个 fat binary
$(LIPO) -create dist/$(BINARY_NAME)-darwin-amd64 dist/$(BINARY_NAME)-darwin-arm64 -output dist/$(BINARY_NAME)
@echo Clen jobs
rm -rf dist/$(BINARY_NAME)-darwin-amd64 dist/$(BINARY_NAME)-darwin-arm64
ENABLE_USER_SCRIPT_SANDBOXING 拜访权限问题
在 Xcode 中履行脚本报错,检查日志发现 Sandbox: ioc-script(5543) deny(1) file-read-data xxx/ioc-demo/ioc-script/dist/ioc-script 相关信息。
顺次履行以下步骤:
- 在 macOS 系统设置,隐私与安全性中,打开关于 Xcode 的
彻底磁盘拜访权限
。 - 在 Xcode 中,在 Project 的 Build Setting 中搜索
ENABLE_USER_SCRIPT_SANDBOXING
,修正为NO
。
5. 总结
经过以上的代码和装备,我们完成了一个根据 yaml 装备的 iOS 网络层 IoC。本文只是提供了基础的思路和演示代码,实际上在运用的时分,能够根据此计划做更多的延伸,比如能够经过愈加复杂的装备办理系统生成装备文件、增加更多的网络层定义与约束等等。
本文选用 golang 用于命令行东西的构建的考虑是,
- 直接选用 Shell 语句其实也能够完结文中的逻辑,可是缺乏扩展性,Shell 代码在规模扩大时难以办理。
- golang 选用内嵌运转环境的二进制文件布置,不需求额定的运转环境装备,适合团队共享。
本文选用的源码能够在 这里 找到。