XMLMapper
XMLMapper是一个用Swift编写的框架,它使您能够轻松地将模型对象(类和结构)转换为XML,并从XML进行转换。
示例
要运行示例项目,请首先克隆仓库,然后从Example目录中运行pod install
要求
- iOS 8.0+ / macOS 10.9+ / tvOS 9.0+ / watchOS 2.0+
- Xcode 9.1+
- Swift 3.1+
协议定义
XMLBaseMappable
协议
var nodeName: String! { get set }
此属性是映射XML节点名称的位置
mutating func mapping(map: XMLMap)
所有映射定义应该放在此函数中。在解析XML时,会在此函数创建成功对象之后执行该函数。在生成XML时,该对象只有一个会被调用的函数。
注意:不应直接实现此协议。应使用 XMLMappable
或 XMLStaticMappable
XMLMappable
协议(XMLBaseMappable
的子协议)
init?(map: XMLMap)
这个可失败初始化器用于XMLMapper创建对象。开发人员可以使用它来在对象序列化之前验证XML。在函数中返回nil将防止发生映射。您可以通过检查XML
Map对象中的XMLMap
对象来执行验证。
required init?(map: XMLMap) {
// check if a required "id" element exists within the XML.
if map.XML["id"] == nil {
return nil
}
}
XMLStaticMappable
协议(XMLBaseMappable
的子协议)
XMLStaticMappable
是 XMLMappable
的替代方案。它为开发人员提供了一个静态函数,该函数由XMLMapper用于对象初始化,而不是使用 init?(map: XMLMap)
。
static func objectForMapping(map: XMLMap) -> XMLBaseMappable?
XMLMapper使用此函数来获取用于映射的对象。在函数中,开发人员应返回符合XMLBaseMappable
协议的对象实例。此函数还可以用于:
- 在对象序列化之前验证XML
- 提供一个现有的缓存对象来使用于映射
- 返回用于映射的另一种类型的对象(也符合
XMLBaseMappable
)。例如,您可能需要检查XML来推断用于映射的对象类型
如果您需要在扩展中实现XMLMapper,您将需要采用此协议而不是XMLMappable
。
如何使用
为了支持映射,一个类或结构体只需实现XMLMappable
协议即可
var nodeName: String! { get set }
init?(map: XMLMap)
mutating func mapping(map: XMLMap)
XMLMapper使用<-
运算符来定义每个属性映射到和从XML的方式
<food>
<name>Belgian Waffles</name>
<price>5.95</price>
<description>
Two of our famous Belgian Waffles with plenty of real maple syrup
</description>
<calories>650</calories>
</food>
class Food: XMLMappable {
var nodeName: String!
var name: String!
var price: Float!
var description: String?
var calories: Int?
required init?(map: XMLMap) {}
func mapping(map: XMLMap) {
name <- map["name"]
price <- map["price"]
description <- map["description"]
calories <- map["calories"]
}
}
XMLMapper可以映射下列类型的类或结构体:
Int
Bool
Double
Float
String
RawRepresentable
(枚举)Array<Any>
Dictionary<String, Any>
T: XMLBaseMappable
的Object
T: XMLBaseMappable
的Array
T: XMLBaseMappable
的Set
String, T: XMLBaseMappable
的Dictionary
String, T: XMLBaseMappable
的Array <T: XMLBaseMappable>
- 上述所有类型的可选和隐式解包可选
基本的XML映射
轻松地将XML字符串转换为XMLMappable
let food = Food(XMLString: xmlString)
或将XMLMappable
对象转换为XML字符串
let xmlString = food.toXMLString()
XMLMapper
类也可以提供相同的功能
let food = XMLMapper<Food>().map(XMLString: xmlString)
let xmlString = XMLMapper().toXMLString(food)
高级映射
设置类中的nodeName
属性以更改元素名称
food.nodeName = "myFood"
<myFood>
<name>Belgian Waffles</name>
<price>5.95</price>
<description>
Two of our famous Belgian Waffles with plenty of real maple syrup
</description>
<calories>650</calories>
</myFood>
利用XMLMap
的attributes
属性轻松映射XML属性
<food name="Belgian Waffles">
</food>
func mapping(map: XMLMap) {
name <- map.attributes["name"]
}
映射元素数组
<breakfast_menu>
<food>
<name>Belgian Waffles</name>
<price>5.95</price>
<description>
Two of our famous Belgian Waffles with plenty of real maple syrup
</description>
<calories>650</calories>
</food>
<food>
<name>Strawberry Belgian Waffles</name>
<price>7.95</price>
<description>
Light Belgian waffles covered with strawberries and whipped cream
</description>
<calories>900</calories>
</food>
</breakfast_menu>
func mapping(map: XMLMap) {
foods <- map["food"]
}
通过实现XMLTransformType
协议创建您自己的自定义变换类型
public protocol XMLTransformType {
associatedtype Object
associatedtype XML
func transformFromXML(_ value: Any?) -> Object?
func transformToXML(_ value: Object?) -> XML?
}
并在映射中使用它
func mapping(map: XMLMap) {
startTime <- (map["starttime"], XMLDateTransform())
}
通过使用点分隔名称来映射嵌套XML元素
<food>
<details>
<price>5.95</price>
</details>
</food>
func mapping(map: XMLMap) {
price <- map["details.price"]
}
注意:当前仅支持嵌套映射
- 仅适用于仅包含innerText的元素(如上面的示例)和属性
- 和
这意味着为了将以下XML中的实际食品价格映射进去
<food>
<details>
<price currency="euro">5.95</price>
</details>
</food>
您需要使用XMLMappable对象,而不是Float
class Price: XMLMappable {
var nodeName: String!
var currency: String!
var actualPrice: Float!
required init?(map: XMLMap) {}
func mapping(map: XMLMap) {
currency <- map.attributes["currency"]
actualPrice <- map.innerText
}
}
因为存在currency
属性。同样的情况适用于下面的XML
<food>
<details>
<price>
5.95
<currency>euro</currency>
</details>
</food>
您需要使用类似以下的XMLMappable对象
class Price: XMLMappable {
var nodeName: String!
var currency: String!
var actualPrice: Float!
required init?(map: XMLMap) {}
func mapping(map: XMLMap) {
currency <- map["currency"]
actualPrice <- map.innerText
}
}
因为存在currency
元素。
Swift 4.2和未排序的XML元素
从Swift 4.2开始,XML元素在每次运行应用时很可能有不同的顺序。(这是因为它们由Dictionary
表示)
因此,从XMLMapper版本1.5.2开始,您可以使用XMLMap
的nodesOrder
属性映射并更改另一节点内部的节点顺序
class TestOrderedNodes: XMLMappable {
var nodeName: String!
var id: String?
var name: String?
var nodesOrder: [String]?
init() {}
required init?(map: XMLMap) {}
func mapping(map: XMLMap) {
id <- map["id"]
name <- map["name"]
nodesOrder <- map.nodesOrder
}
}
let testOrderedNodes = TestOrderedNodes()
testOrderedNodes.id = "1"
testOrderedNodes.name = "the name"
testOrderedNodes.nodesOrder = ["id", "name"]
print(testOrderedNodes.toXMLString() ?? "nil")
注意:如果您想更改节点顺序,请确保在nodesOrder
数组中包含您希望在XML字符串中出现的所有节点名称
映射CDATA包装的值
自XMLMapper 2.0.0版本起,增加了对CDATA的支持。默认情况下,现在将CDATA包装的字符串映射为Array<Data>
,而不是上一版本中的String
。这导致了一个副作用,即不能序列化CDATA包装的值。
例如,使用以下代码
class Food: XMLMappable {
var nodeName: String!
var description: String?
init() {}
required init?(map: XMLMap) {}
func mapping(map: XMLMap) {
description <- map["description"]
}
}
let food = Food()
food.nodeName = "Food"
food.description = "Light Belgian waffles covered with strawberries & whipped cream"
print(food.toXMLString() ?? "nil")
您的结果总是
<Food>
<description>
Light Belgian waffles covered with strawberries & whipped cream
</description>
</Food>
在版本2.0.0中,我们引入了内置的XMLCDATATransform
类型,可以像这样使用
class Food: XMLMappable {
var nodeName: String!
var description: String?
init() {}
required init?(map: XMLMap) {}
func mapping(map: XMLMap) {
description <- (map["description"], XMLCDATATransform())
}
}
let food = Food()
food.nodeName = "Food"
food.description = "Light Belgian waffles covered with strawberries & whipped cream"
print(food.toXMLString() ?? "nil")
并且结果将是
<Food>
<description>
<![CDATA[
Light Belgian waffles covered with strawberries & whipped cream
]]>
</description>
</Food>
破坏性变更在于,除非您使用XMLCDATATransform
类型,否则无法进行CDATA包裹值的反序列化。例如,如果您尝试将上述XML映射到以下模型类
class Food: XMLMappable {
var nodeName: String!
var description: String?
required init?(map: XMLMap) {}
func mapping(map: XMLMap) {
description <- map["description"]
}
}
您最终的description
属性值将是nil
。
注意:如果您自己运行XMLSerialization
的xmlObject(withString:encoding:options:)
函数,并将包括cdataAsString
选项在内的default
集合作为options
传递,则可以更改默认行为。
例如,以下代码将有效:
class Food: XMLMappable {
var nodeName: String!
var description: String?
required init?(map: XMLMap) {}
func mapping(map: XMLMap) {
description <- map["description"]
}
}
let xmlString = """
<Food>
<description>
<![CDATA[
Light Belgian waffles covered with strawberries & whipped cream
]]>
</description>
</Food>
"""
let data = Data(xmlString.utf8) // Data for deserialization (from XML to object)
do {
let xml = try XMLSerialization.xmlObject(with: data, options: [.default, .cdataAsString])
let food = XMLMapper<Food>().map(XMLObject: xml)
} catch {
print(error)
}
XML映射示例
映射XML
<?xml version="1.0" encoding="UTF-8"?>
<root>
<TestElementXMLMappable testAttribute="enumValue">
<testString>Test string</testString>
<testList>
<element>
<testInt>1</testInt>
<testDouble>1.0</testDouble>
</element>
<element>
<testInt>2</testInt>
<testDouble>2.0</testDouble>
</element>
<element>
<testInt>3</testInt>
<testDouble>3.0</testDouble>
</element>
<element>
<testInt>4</testInt>
<testDouble>4.0</testDouble>
</element>
</testList>
<someTag>
<someOtherTag>
<nestedTag testNestedAttribute="nested attribute">
</nestedTag>
</someOtherTag>
</someTag>
</TestElementXMLMappable>
</root>
到类
class TestXMLMappable: XMLMappable {
var nodeName: String!
var testElement: TestElementXMLMappable!
var testNestedAttribute: String?
required init?(map: XMLMap) {}
func mapping(map: XMLMap) {
testElement <- map["TestElementXMLMappable"]
testNestedAttribute <- map.attributes["TestElementXMLMappable.someTag.someOtherTag.nestedTag.testNestedAttribute"]
}
}
enum EnumTest: String {
case theEnumValue = "enumValue"
}
class TestElementXMLMappable: XMLMappable {
var nodeName: String!
var testString: String?
var testAttribute: EnumTest?
var testList: [Element]?
var nodesOrder: [String]?
required init?(map: XMLMap) {}
func mapping(map: XMLMap) {
testString <- map["testString"]
testAttribute <- map.attributes["testAttribute"]
testList <- map["testList.element"]
nodesOrder <- map.nodesOrder
}
}
class Element: XMLMappable {
var nodeName: String!
var testInt: Int?
var testDouble: Float?
required init?(map: XMLMap) {}
func mapping(map: XMLMap) {
testInt <- map["testInt"]
testDouble <- map["testDouble"]
}
}
Requests子规格
注意:由于依赖于Alamofire
,Requests
子规格有不同的最低部署目标。目前为iOS 10.0+ / macOS 10.12+ / tvOS 10.0+ / watchOS 3.0+
。
使用Alamofire
轻松创建并发送带有XML主体的请求(已添加缺失的XMLEncoding
结构)
Alamofire.request(url, method: .post, parameters: xmlMappableObject.toXML(), encoding: XMLEncoding.default)
还使用Alamofire
扩展将XML响应映射到XMLMappable
对象。例如,一个URL返回以下CD目录
<CATALOG>
<CD>
<TITLE>Empire Burlesque</TITLE>
<ARTIST>Bob Dylan</ARTIST>
<COUNTRY>USA</COUNTRY>
<COMPANY>Columbia</COMPANY>
<PRICE>10.90</PRICE>
<YEAR>1985</YEAR>
</CD>
<CD>
<TITLE>Hide your heart</TITLE>
<ARTIST>Bonnie Tyler</ARTIST>
<COUNTRY>UK</COUNTRY>
<COMPANY>CBS Records</COMPANY>
<PRICE>9.90</PRICE>
<YEAR>1988</YEAR>
</CD>
</CATALOG>
按照以下方式映射响应
Alamofire.request(url).responseXMLObject { (response: DataResponse<CDCatalog>) in
let catalog = response.result.value
print(catalog?.cds?.first?.title ?? "nil")
}
CDCatalog
对象看起来可能像这样
class CDCatalog: XMLMappable {
var nodeName: String!
var cds: [CD]?
required init?(map: XMLMap) {}
func mapping(map: XMLMap) {
cds <- map["CD"]
}
}
class CD: XMLMappable {
var nodeName: String!
var title: String!
var artist: String?
var country: String?
var company: String?
var price: Double?
var year: Int?
required init?(map: XMLMap) {}
func mapping(map: XMLMap) {
title <- map["TITLE"]
artist <- map["ARTIST"]
country <- map["COUNTRY"]
company <- map["COMPANY"]
price <- map["PRICE"]
year <- map["YEAR"]
}
}
最后但同样重要的是,再次使用Alamofire
轻松创建并发送SOAP请求
let soapMessage = SOAPMessage(soapAction: "ActionName", nameSpace: "ActionNameSpace")
let soapEnvelope = SOAPEnvelope(soapMessage: soapMessage)
Alamofire.request(url, method: .post, parameters: soapEnvelope.toXML(), encoding: XMLEncoding.soap(withAction: "ActionNameSpace#ActionName"))
请求看起来可能像这样
POST / HTTP/1.1
Host: <The url>
Content-Type: text/xml; charset="utf-8"
Connection: keep-alive
SOAPAction: ActionNameSpace#ActionName
Accept: */*
User-Agent: XMLMapper_Example/1.0 (org.cocoapods.demo.XMLMapper-Example; build:1; iOS 11.0.0) Alamofire/4.5.1
Accept-Language: en;q=1.0
Content-Length: 251
Accept-Encoding: gzip;q=1.0, compress;q=0.5
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<soap:Body>
<m:ActionName xmlns:m="ActionNameSpace"/>
</soap:Body>
</soap:Envelope>
添加动作参数就像子类化SOAPMessage
类一样简单。
class MySOAPMessage: SOAPMessage {
// Custom properties
override func mapping(map: XMLMap) {
super.mapping(map: map)
// Map the custom properties
}
}
还要指定端点使用的SOAP版本,如下所示
let soapMessage = SOAPMessage(soapAction: "ActionName", nameSpace: "ActionNameSpace")
let soapEnvelope = SOAPEnvelope(soapMessage: soapMessage, soapVersion: .version1point2)
Alamofire.request(url, method: .post, parameters: soapEnvelope.toXML(), encoding: XMLEncoding.soap(withAction: "ActionNameSpace#ActionName", soapVersion: .version1point2))
然后请求将变为这样
POST / HTTP/1.1
Host: <The url>
Content-Type: application/soap+xml;charset=UTF-8;action="ActionNameSpace#ActionName"
Connection: keep-alive
Accept: */*
User-Agent: XMLMapper_Example/1.0 (org.cocoapods.demo.XMLMapper-Example; build:1; iOS 11.0.0) Alamofire/4.5.1
Accept-Language: en;q=1.0
Content-Length: 248
Accept-Encoding: gzip;q=1.0, compress;q=0.5
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope/" soap:encodingStyle="http://www.w3.org/2003/05/soap-encoding">
<soap:Body>
<m:ActionName xmlns:m="ActionNameSpace"/>
</soap:Body>
</soap:Envelope>
不幸的是,目前没有简单的方法可以映射SOAP响应,除了创建您自己的XMLMappable对象(至少不是现在)
通讯
- 如果您需要帮助,请使用Stack Overflow。(标签'xmlmapper')
- 如果您想问一个通用问题,请使用Stack Overflow。
- 如果您发现了一个错误,请提交一个问题。
- 如果您有一个功能请求,请提交一个问题。
安装
CocoaPods
XMLMapper 可以通过 CocoaPods 获取。要安装它,只需将以下行添加到您的 Podfile
pod 'XMLMapper'
要安装 Requests
子规格,请将以下行添加到您的 Podfile
pod 'XMLMapper/Requests'
Carthage
要使用 Carthage 将 XMLMapper 整合到您的 Xcode 项目中,请将以下行添加到您的 Cartfile
github "gcharita/XMLMapper" ~> 1.6
Swift Package Manager
要将 XMLMapper 添加到基于 Swift Package Manager 的项目,请添加以下内容
.package(url: "https://github.com/gcharita/XMLMapper.git", from: "1.6.0")
到您的 Package.swift
的 dependencies
值。
特别感谢
- 特别感谢 Tristan Himmelman。此项目主要基于 ObjectMapper,这是一个针对 JSON 映射的极佳解决方案。此外,Requests 子规格基于 AlamofireObjectMapper。
- 特别感谢 Nick Lockwood 和他关于 XMLDictionary 的想法
- 感谢 Alamofire 提供的子规格依赖项
许可证
XMLMapper可在MIT许可证下使用。更多信息,请参阅许可证文件。