Swift中利用XMLParser解析SVG文档时,如何检测元素结束以维护嵌套变换数组
解决SVG嵌套标签的Transform栈管理问题
我之前在做SVG路径转UIBezierPath的需求时,刚好碰到过一模一样的嵌套<g>变换处理问题——其实XMLParser完全能检测到</g>闭合标签,你只是没用到对应的代理回调而已!下面给你一步步拆解解决方案:
核心思路:利用XMLParser的结束元素回调维护Transform栈
XMLParser的代理协议里有两个关键方法,刚好对应标签的开始和结束:
parser(_:didStartElement:namespaceURI:qualifiedName:attributes:):遇到<g>、<path>等开始标签时触发parser(_:didEndElement:namespaceURI:qualifiedName:):遇到</g>、</path>等闭合标签时触发
我们可以用一个数组作为Transform栈,进入<g>时压入对应的变换,离开</g>时弹出栈顶的变换,这样就能完美处理嵌套层级的叠加变换。
具体实现步骤(Swift示例)
1. 定义Transform栈和存储处理后路径的数组
import UIKit import Foundation class SVGParser: NSObject, XMLParserDelegate { // 维护嵌套的Transform栈 private var transformStack: [CGAffineTransform] = [] // 存储转换后的UIBezierPath private(set) var processedPaths: [UIBezierPath] = [] // 解析入口方法 func parseSVG(from data: Data) { let parser = XMLParser(data: data) parser.delegate = self parser.parse() } }
2. 处理开始标签:压入Transform、解析路径
在开始元素回调中,遇到<g>就提取并转换其transform属性压入栈;遇到<path>就解析路径并叠加栈中所有Transform:
extension SVGParser { func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) { switch elementName.lowercased() { case "g": // 提取并转换SVG的transform字符串为CGAffineTransform if let transformStr = attributeDict["transform"], let transform = parseSVGTransform(transformStr) { transformStack.append(transform) } case "path": // 解析SVG路径字符串为UIBezierPath(这里假设你已有成熟的路径解析方法) guard let pathStr = attributeDict["d"], let bezierPath = UIBezierPath(svgPathString: pathStr) else { return } // 叠加栈中所有Transform(从父到子依次应用) let combinedTransform = transformStack.reduce(.identity) { result, nextTransform in result.concatenating(nextTransform) } bezierPath.apply(combinedTransform) // 保存处理后的路径 processedPaths.append(bezierPath) default: break } } // 解析SVG的transform字符串为CGAffineTransform(基础实现,可扩展更多变换类型) private func parseSVGTransform(_ transformStr: String) -> CGAffineTransform? { var transform = CGAffineTransform.identity // 移除无关字符,拆分指令 let cleanedStr = transformStr.replacingOccurrences(of: "[", with: "").replacingOccurrences(of: "]", with: "") let components = cleanedStr.components(separatedBy: CharacterSet(charactersIn: "() ,")).filter { !$0.isEmpty } var index = 0 while index < components.count { let command = components[index] index += 1 switch command.lowercased() { case "translate": let x = CGFloat(Double(components[index]) ?? 0) index += 1 let y = CGFloat(Double(components[index]) ?? 0) index += 1 transform = transform.concatenating(.init(translationX: x, y: y)) case "scale": let sx = CGFloat(Double(components[index]) ?? 1) index += 1 let sy = CGFloat(Double(components[index]) ?? 1) index += 1 transform = transform.concatenating(.init(scaleX: sx, y: sy)) case "rotate": let angle = CGFloat(Double(components[index]) ?? 0) * .pi / 180 index += 1 // SVG的rotate默认以原点为中心,若有额外参数则是旋转中心 if index < components.count { let cx = CGFloat(Double(components[index]) ?? 0) index += 1 let cy = CGFloat(Double(components[index]) ?? 0) index += 1 transform = transform.concatenating(.init(translationX: cx, y: cy)) .concatenating(.init(rotationAngle: angle)) .concatenating(.init(translationX: -cx, y: -cy)) } else { transform = transform.concatenating(.init(rotationAngle: angle)) } // 可扩展skewX、skewY等其他SVG变换类型 default: continue } } return transform } }
3. 处理结束标签:弹出Transform
在结束元素回调中,遇到</g>就弹出栈顶的Transform:
extension SVGParser { func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) { if elementName.lowercased() == "g" { // 只有栈不为空时才弹出,避免空栈错误 if !transformStack.isEmpty { transformStack.removeLast() } } } }
关键注意事项
- Transform的叠加顺序:SVG的嵌套变换是从外层到内层依次应用的,所以用
reduce从identity开始依次拼接栈中的Transform是正确的顺序。 - 命名空间处理:如果你的SVG使用了命名空间,可能需要用
qualifiedName代替elementName进行判断。 - 容错处理:解析transform字符串时要做足够的容错,避免因为格式异常导致解析失败。
内容的提问来源于stack exchange,提问作者Dan Donaldson
相关产品推荐
相关产品推荐

