ApiModel
使用realm.io来与REST API交互以表示对象。《ApiModel》的目标是易于设置、易于理解和有趣使用。应将样板代码保持在最小,并使其易于设置。
该项目非常受@idlefingers'优秀的api-model启发。
开始使用
将APIModel添加到Podfile,然后运行pod install
pod 'APIModel', '~> 0.13.0'
关键是实现 ApiModel
协议。
import RealmSwift
import ApiModel
class Post: Object, ApiModel {
// Standard Realm boilerplate
dynamic var id = ""
dynamic var title = ""
dynamic var contents = ""
dynamic lazy var createdAt = NSDate()
override class func primaryKey() -> String {
return "id"
}
// Define the standard namespace this class usually resides in JSON responses
// MUST BE singular ie `post` not `posts`
class func apiNamespace() -> String {
return "post"
}
// Define where and how to get these. Routes are assumed to use Rails style REST (index, show, update, destroy)
class func apiRoutes() -> ApiRoutes {
return ApiRoutes(
index: "/posts.json",
show: "/post/:id:.json"
)
}
// Define how it is converted from JSON responses into Realm objects. A host of transforms are available
// See section "Transforms" in README. They are super easy to create as well!
class func fromJSONMapping() -> JSONMapping {
return [
"id": ApiIdTransform(),
"title": StringTransform(),
"contents": StringTransform(),
"createdAt": NSDateTransform()
]
}
// Define how this object is to be serialized back into a server response format
func JSONDictionary() -> [String:Any] {
return [
"id": id,
"title": email,
"contents": contents,
"created_at": createdAt
]
}
}
目录
配置API
为了表示API自身,需要创建一个ApiManager
类的对象。这个对象包含一个ApiConfiguration
对象,用于定义所有请求的主机URL。创建之后,可以从func apiManager() -> ApiManager
单例函数中访问。
配置步骤
// Put this somewhere in your AppDelegate or together with other initialization code
var apiConfig = ApiConfig(host: "https://service.io/api/v1/")
ApiSingleton.setInstance(ApiManager(configuration: apiConfig))
如果您想禁用请求日志记录,可以设置requestLogging
为false
apiConfig.requestLogging = false
如果您想让ApiModel使用NSURLSessionConfiguration,可以像以下示例中那样设置
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
configuration.timeoutIntervalForRequest = 15 // shorten default timeout
configuration.timeoutIntervalForResource = 15 // shorten default timeout
ApiSingleton.setInstance(ApiManager(config: ApiConfig(host: "http://feed.myapi.com", urlSessionConfig:configuration)))
// or
//...
apiConfig.urlSessionConfig = configuration
全局和模型本地配置
大多数情况下,API在不同端点之间是一致的,但在实际应用中,常规通常差异很大。全局配置是通过调用ApiSingleton.setInstance(ApiManager(configuration: apiConfig))
设置的。
要有一个模型本地配置,模型需要实现ApiConfigurable
协议,该协议包含一个单一的方法
public protocol ApiConfigurable {
static func apiConfig(config: ApiConfig) -> ApiConfig
}
输入是根基本配置,输出是模型自己的配置对象。传入的对象是根配置的副本,因此您可以自由修改该对象而不产生任何副作用。
static func apiConfig(config: ApiConfig) -> ApiConfig {
config.encoding = ApiRequest.FormDataEncoding
return config
}
与API交互
ApiModel
的基类是Api
封装类。这个类封装了一个Object
类型,负责获取对象、保存对象和处理验证错误。
基本的REST操作
ApiModel
支持使用基本的HTTP动词查询API。
// GET call without parameters
Api<Post>.get("/v1/posts.json") { response in
println("response.isSuccessful: \(response.isSuccessful)")
println("Response as an array: \(response.array)")
println("Response as a dictionary: \(response.dictionary)")
println("Response errors?: \(response.errors)")
}
// Other supported methods:
Api<Post>.get(path, parameters: [String:AnyObject]) { response // ...
Api<Post>.post(path, parameters: [String:AnyObject]) { response // ...
Api<Post>.put(path, parameters: [String:AnyObject]) { response // ...
Api<Post>.delete(path, parameters: [String:AnyObject]) { response // ...
// no parameters
Api<Post>.get(path) { response // ...
Api<Post>.post(path) { response // ...
Api<Post>.put(path) { response // ...
Api<Post>.delete(path) { response // ...
// You can also pass in custom `ApiConfig` into each of the above mentioned methods:
Api<Post>.get(path, parameters: [String:AnyObject], apiConfig: ApiConfig) { response // ...
Api<Post>.post(path, parameters: [String:AnyObject], apiConfig: ApiConfig) { response // ...
Api<Post>.put(path, parameters: [String:AnyObject], apiConfig: ApiConfig) { response // ...
Api<Post>.delete(path, parameters: [String:AnyObject], apiConfig: ApiConfig) { response // ...
通常,您想要使用如下的ActiveRecord风格的动词index/show/create/update
来与REST API进行交互。
获取对象
使用REST资源的index
GET /posts.json
Api<Post>.findArray { posts, response in
for post in posts {
println("... \(post.title)")
}
}
使用REST资源中的show
GET /user.json
Api<User>.find { user, response in
if let user = user {
println("User is: \(user.email)")
} else {
println("Error loading user")
}
}
存储对象
var post = Post()
post.title = "Hello world - A prologue"
post.contents = "Hello!"
post.createdAt = NSDate()
var form = Api<Post>(model: post)
form.save { _ in
if form.hasErrors {
println("Could not save:")
for error in form.errorMessages {
println("... \(error)")
}
} else {
println("Saved! Post #\(post.id)")
}
}
Api
将知道该对象没有被持久存储,因为它没有设置id
(或者在任何定义了primaryKey
的字段中)。所以会发送以下POST
请求
POST /posts.json
{
"post": {
"title": "Hello world - A prologue",
"contents": "Hello!",
"created_at": "2015-03-08T14:19:31-01:00"
}
}
如果响应成功,服务器返回的属性将更新到模型上。
200 OK
{
"post": {
"id": 1
}
}
错误应该以以下格式提供
400 BAD REQUEST
{
"post": {
"errors": {
"contents": [
"must be longer than 140 characters"
]
}
}
}
以下是如何访问错误的方式
form.errors["contents"] // -> [String]
// or
form.errorMessages // -> [String]
转换
转换用于将JSON响应中的属性转换为富类型。最简单的方式是展示一个简单的转换。
ApiModel
提供了一组标准的转换。一个例子是IntTransform
import RealmSwift
class IntTransform: Transform {
func perform(value: AnyObject?, realm: Realm?) -> AnyObject {
if let asInt = value?.integerValue {
return asInt
} else {
return 0
}
}
}
它接受一个对象并尝试将其转换为整数。如果失败了,就返回默认值0。
转换可以是相当复杂的,甚至可以转换嵌套的模型。例如
class User: Object, ApiModel {
dynamic var id = ApiId()
dynamic var email = ""
let posts = List<Post>()
static func fromJSONMapping() -> JSONMapping {
return [
"posts": ArrayTransform<Post>()
]
}
}
Api<User>.find { user, response in
println("User: \(user.email)")
for post in user.posts {
println("\(post.title)")
}
}
默认转换包括
- StringTransform
- IntTransform
- FloatTransform
- DoubleTransform
- BoolTransform
- NSDateTransform
- ModelTransform
- ArrayTransform
- PercentageTransform
但是,定义自己的转换非常简单。尽情发挥吧!
NSDateTransform
日期和时间的解析通常是有些复杂的,并且有很多细微之处。NSDateTransform
接受一个字符串并尝试将其转换为NSDate
对象。如果失败了,就返回nil
。
日期可以以很多不同的格式出现。《ApiModel》和《NSDateTransform》默认使用的格式是ISO 8601
class Post: Object, APIModel {
class func fromJSONMapping() -> JSONMapping {
return [
"createdAt": NSDateTransform()
]
}
}
// Example of a valid string:
// "2015-12-30T12:12:33.000Z"
在现实世界中,你会遇到许多不同的日期格式,而且许多API可能对于如何表示日期有不同的看法。因此,你还可以为《NSDateTransform》传入自定义的格式字符串。
class Post: Object, APIModel {
class func fromJSONMapping() -> JSONMapping {
return [
"createdAt": NSDateTransform(dateFormat: "yyyy-MM-dd")
]
}
}
// Example of a valid string:
// "2015-12-30"
时区说明
内部,NSDate
将日期存储为从特定参考日期起的秒数。这意味着它不会存储有关时区的任何信息,即使日期字符串中提供了相关信息。例如,假设以下字符串由API返回:2015-12-30T18:12:33.000-05:00
。NSDate
只使用提供的偏移量来正确偏移结果时间,然后它将丢弃。
作为一名应用程序开发者,你需要确保在显示时间戳时始终传递正确的时区。通常使用NSDateFormatter
。默认情况下,NSDateFormatter
使用用户时区,因此你通常无需担心。以下是一个示例供参考。
let dateString = "2015-12-30T18:12:33.000-05:00"
// Parse as ISO 8601
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
let date = dateFormatter.dateFromString(dateString)!
let outputFormatter = NSDateFormatter()
outputFormatter.dateFormat = "yyyy-MM-dd HH:mm"
// prints "2015-12-30 23:12"
outputFormatter.timeZone = NSTimeZone(abbreviation: "GMT")
print(outputFormatter.stringFromDate(date))
// prints "2015-12-31 00:12"
outputFormatter.timeZone = NSTimeZone(abbreviation: "Europe/Stockholm") // +02:00
print(outputFormatter.stringFromDate(date))
// prints "2015-12-30 18:12"
outputFormatter.timeZone = NSTimeZone(abbreviation: "EST") // -05:00 SAME AS INPUT
print(outputFormatter.stringFromDate(date))
经验法则:你只在显示NSDate
时才需要考虑时区。
钩子
ApiModel
使用Alamofire进行请求的发送和接收。要连接到这一点,当前API
类具有before
和after
钩子,你可以使用它们来修改或记录请求。发送用户凭据的示例
// Put this somewhere in your AppDelegate or together with other initialization code
api().beforeRequest { request in
if let loginToken = User.loginToken() {
request.parameters["access_token"] = loginToken
}
}
还有一个afterRequest
,它传递一个ApiRequest
和一个ApiResponse
。
api().afterRequest { request, response in
println("... Got: \(response.status)")
println("... \(request)")
println("... \(response)")
}
URL
根据上面的Post
模型配置,如果你想要获取完整URL,以替换展示资源(如https://service.io/api/v1/posts/123.json
),你可以使用以下方法。
post.apiUrlForRoute(Post.apiRoutes().show)
// NOT IMPLEMENTED YET BECAUSE LIMITATIONS IN SWIFT: post.apiUrlForResource(.Show)
处理ID
作为API的消费者,你永远不会希望对自己的模型使用的ID结构做出假设。对于ID类型,不要使用Int
或任何类似的内容,应使用字符串。因此,ApiModel
定义了一个别名为String
的别名,称为ApiId。还有一个可用于ID的ApiIdTransform
。
命名空间和信封
一些API将所有响应封装在一个“信封”中,这是一个适用于所有响应的通用容器。例如,一个API可能会在根JSON的data
属性中封装所有响应数据。
{
"data": {
"user": { ... }
}
}
为了优雅地处理这种情况,在ApiConfig
类上有配置选项,名为rootNamespace
。这是一个点分隔的路由路径,每个响应都要遍历。要处理上述示例,你需要简单地
let config = ApiConfig()
config.rootNamespace = "data"
它也可以更复杂,例如如果信封看起来像这样
{
"JsonResponseEnvelope": {
"SuccessFullJsonResponse": {
"SoapResponseContainer": {
"EnterpriseBeanData": {
"user": { ... }
}
}
}
}
}
这将转换为rootNamespace
let config = ApiConfig()
config.rootNamespace = "JsonResponseEnvelope.SuccessFullJsonResponse.SoapResponseContainer.EnterpriseBeanData"
缓存和存储
缓存和存储任何调用结果取决于你自己。ApiModel为你执行此操作,并且不会这样做,因为策略根据需求有很大的不同。
文件上传
正如JSONDictionary
可以返回参数字典以发送到服务器一样,它还可以包含可以上传到服务器的NSData
值。例如,你可以将UIImage
转换为NSData
,并将它们上传为个人资料图片。
在Web上上传文件的标准方式是使用内容类型multipart/form-data
,这稍微不同于JSON。如果你有一个应该支持文件上传的模型,你可以配置模型将它的JSONDictionary
编码为multipart/form-data
。
以下示例说明了一个UserAvatar
模型
import RealmSwift
import ApiModel
import UIKit
class UserAvatar: Object, ApiModel, ApiConfigurable {
dynamic var userId = ApiId()
dynamic var url = String() // generated on the server
var imageData: NSData?
class func apiConfig(config: ApiConfig) -> ApiConfig {
// ApiRequest.FormDataEncoding is where the magic happens
// It tells ApiModel to encode everything with `multipart/form-data`
config.encoding = ApiRequest.FormDataEncoding
return config
}
// Important because the `imageData` property cannot be represented by Realm
override class func ignoredProperties() -> [String] {
return ["imageData"]
}
override class func primaryKey() -> String {
return "userId"
}
class func apiNamespace() -> String {
return "user_avatar"
}
class func apiRoutes() -> ApiRoutes {
return ApiRoutes.resource("/user/avatar.json")
}
class func fromJSONMapping() -> JSONMapping {
return [
"userId": ApiIdTransform(),
"url": StringTransform()
]
}
func JSONDictionary() -> [String:Any] {
return [
"image": FileUpload(fileName: "avatar.jpg", mimeType: "image/jpg", data: imageData!)
]
}
}
func upload() {
let image = UIImage(named: "me.jpg")!
let userAvatar = UserAvatar()
userAvatar.userId = "1"
userAvatar.imageData = UIImageJPEGRepresentation(image, 1)!
Api(model: userAvatar).save { form in
if form.hasErrors {
print("Could not upload file: " + form.errorMessages.joinWithSeparator("\n"))
} else {
print("File uploaded! URL: \(userAvatar.url)")
}
}
}
FileUpload
你可以使用这种方式上传任何文件,而不仅仅是图像。任何NSData都可以上传。上传文件的默认MIME类型为application/octet-stream
。如果你需要配置它,需要构造一个特殊的对象FileUpload
。
构造函数如图所示,它接受服务器接收的文件名、文件mime类型和数据。
FileUpload(fileName: "document.pdf", mimeType: "application/pdf", data: documentData)
这应该放入JSONDictionary
字典中。ApiModel将检测它并相应地进行编码。
感谢以下人士
许可证
MIT许可证(MIT)
版权所有 (c) 2015 Rootof Creations HB
以下条件下,免费允许任何获得此软件和相关文档文件(“软件”)副本的人在任何限制下处理软件,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或销售软件副本,并允许将软件提供给需要的人来执行这些操作:
上述版权声明和许可声明应包含在软件的所有副本或主要部分的副本中。
软件按“原样”提供,不提供任何保证,明示或暗示的,包括但不限于适销性、适用于特定目的和非侵权的担保。在任何情况下,作者或版权所有者均不对因合同、侵权或其他行为而导致的任何索赔、损害或其他责任承担责任,无论源于、涉及或与此软件或软件的使用或以其他方式相关。