You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 21:58:12