XMLMapper

CI Status Version License Platform Swift Package Manager compatible Carthage compatible

XMLMapper 是一个用 Swift 编写的框架,它可以让你轻松地将模型对象(类和结构体)转换为 XML,以及从 XML 转换回来。

示例

要运行示例项目,请克隆代码仓库,然后首先从 Example 目录运行 pod install

要求

协议定义

XMLBaseMappable 协议

var nodeName: String! { get set }

此属性用于映射 XML 节点的名称

mutating func mapping(map: XMLMap)

此函数是所有映射定义应该存在的地方。 在解析 XML 时,此函数在成功创建对象后执行。 生成 XML 时,它是对象上调用的唯一函数。

注意:不应直接实现此协议。 应使用 XMLMappableXMLStaticMappable

XMLMappable 协议 (XMLBaseMappable 的子协议)

init?(map: XMLMap)

XMLMapper 使用此可失败的初始化程序来创建对象。 开发人员可以使用它来验证 XML,然后再进行对象序列化。 在函数中返回 nil 将阻止映射发生。 您可以检查存储在 XMLMap 对象中的 XML 来进行验证

required init?(map: XMLMap) {
    // check if a required "id" element exists within the XML.
    if map.XML["id"] == nil {
        return nil
    }
}

XMLStaticMappable 协议 (XMLBaseMappable 的子协议)

XMLStaticMappableXMLMappable 的替代方案。 它为开发人员提供了一个静态函数,XMLMapper 使用该函数进行对象初始化,而不是 init?(map: XMLMap)

static func objectForMapping(map: XMLMap) -> XMLBaseMappable?

XMLMapper 使用此函数来获取用于映射的对象。 开发人员应在此函数中返回符合 XMLBaseMappable 的对象的实例。 此函数还可以用于

如果您需要在扩展中实现 XMLMapper,则需要采用此协议,而不是 XMLMappable

如何使用

为了支持映射,类或结构只需要实现 XMLMappable 协议

var nodeName: String! { get set }
init?(map: XMLMap)
mutating func mapping(map: XMLMap)

XMLMapper 使用 <- 运算符来定义每个属性如何映射到 XML 以及从 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 可以映射由以下类型组成的类或结构

基本的 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>

使用 XMLMapattributes 属性轻松映射 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"]
}

注意:目前仅支持嵌套映射

这意味着为了映射以下 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 版本开始,你可以使用 XMLMapnodesOrder 属性来映射和更改出现在另一个节点内部的节点的顺序

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 &amp; 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"]
    }
}

你最终会将 nil 作为 description 属性的值。


注意:如果你自己运行 XMLSerializationxmlObject(withString:encoding:options:) 函数,并将 options 作为 default 设置,包括 cdataAsString 选项,则可以更改默认行为。

例如,以下代码将有效

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>

不幸的是,除了创建你自己的 XMLMappable 对象之外,没有简单的方法来映射 SOAP 响应(至少目前还没有)

沟通

安装

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.swiftdependencies 值中。

特别感谢

许可证

XMLMapper 在 MIT 许可证下可用。 有关更多信息,请参阅 LICENSE 文件。