import SwiftUI import XCTest extension XCUIElement { private static var _representation = [String:UITestsScreenshots]() // hack to add variable in extensions var representation: UITestsScreenshots { get { let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) if (XCUIElement._representation[tmpAddress] == nil) { XCUIElement._representation[tmpAddress] = UITestsScreenshots(element: self) } return XCUIElement._representation[tmpAddress]! } } } extension XCUIApplication { private static var _representationWithElement = UITestsAppRepresentation() var representationWithElements: UITestsAppRepresentation { get { return XCUIApplication._representationWithElement } } } class UITestsAppRepresentation : UITestsScreenshots { var mainView: XCUIElement var otherElement: XCUIElement? private let statusBar: XCUIElement private let tabBar: XCUIElement private let app = XCUIApplication() private(set) var allVariations = [[String](),[String](),[String](),[String]()] private static var backup: [String:(XCUIElement,XCUIElement?,[[String]])] = [:] private var elementsDescription = "" override var description: String { get { let description = super.description + " (definition = " + elementsDescription + ")" elementsDescription = "" return description } } init() { self.mainView = app.dialerView self.statusBar = app.statusBar self.tabBar = app.tabBar super.init(element: app) } func withElementVariations(mainView: [String], statusBar: [String], tabBar: [String], other: [String] = []) -> UITestsAppRepresentation { allVariations = [mainView, statusBar, tabBar, other] return self } func updateElementVariations(mainView: [String], statusBar: [String], tabBar: [String], other: [String] = []) -> UITestsAppRepresentation { allVariations[0] += mainView allVariations[1] += statusBar allVariations[2] += tabBar allVariations[3] += other return self } override func convertForComparaison(screenshot: UIImage) -> UIImage { let elements = getElements() if (svgManager.rects["mask"] == nil) {_=svgManager.parse()} _=elements.map{ if ($0 != nil) { if ($0!.representation.svgManager.rects["mask"] == nil) {_=$0!.representation.svgManager.parse()} svgManager.rects["mask"]! += $0!.representation.svgManager.rects["mask"]! } } return super.convertForComparaison(screenshot: screenshot) } override func getReference() -> UIImage? { UIGraphicsBeginImageContextWithOptions(UITestsScreenshots.screenSize, false, 1) guard (super.getReference()?.draw(at: CGPoint(x: 0, y: 0)) != nil) else {return nil} let elements = getElements() for i in 0.. [XCUIElement?]{ return [mainView,statusBar,tabBar,otherElement].map {($0 != nil && self.element.frame.contains($0!.frame)) ? $0! : nil} } func makeBackup(named: String) { UITestsAppRepresentation.backup[named] = (mainView,otherElement,allVariations) } func reloadBackup(named: String) -> UITestsAppRepresentation { if let backup = UITestsAppRepresentation.backup[named] { mainView = backup.0 allVariations = backup.2 otherElement = backup.1 UITestsAppRepresentation.backup.removeValue(forKey: named) } else { XCTFail("unable to find an app representation backup named \"\(named)\"") } return self } } class UITestsScreenshots { static let screenshotDelay: TimeInterval = 0.5 static let pixelTreshold: Int = 3 static let colorTreshold: Int = 3 static let screenSize: CGSize = { var size = XCUIApplication().frame.size let scaleFactor = 3.0 size.width.scale(by: scaleFactor) size.height.scale(by: scaleFactor) return size }() static let defaultPath: String = { let path = #filePath return String(path.prefix(path.distance(from: path.startIndex, to: path.range(of: "UITests")!.lowerBound) + "UITests/".count)) + "Screenshots/" }() internal var description: String { get { return viewName + (variations.isEmpty ? "" : "||\(variations.joined(separator: ","))") } } private(set) var debugHistory: String = "" internal let element: XCUIElement private var _svgManager: SVGManager? internal var svgManager: SVGManager { get{ if (_svgManager == nil) {_svgManager = SVGManager(path: "\(UITestsScreenshots.defaultPath + viewName).svg")} return _svgManager! } } internal var _viewName: String? var viewName: String { get { if (_viewName == nil) { _viewName = element.identifier if (_viewName!.isEmpty) {_viewName = element.label} //for elements wich don't have identifier debugHistory = "UITestsScreenshots : \(_viewName!) : " } return _viewName! } } private(set) var variations = [String]() init(element: XCUIElement) { self.element = element } // public functions func withVariations(named: [String]) -> UITestsScreenshots { variations = named debugHistory += " with varitions named \"\(named.joined(separator: "\", \""))\" -> " return self } func make(after time: TimeInterval = screenshotDelay) { XCTContext.runActivity(named: "Make \"\(viewName)\" reference screenshot") { context in debugHistory += "make reference -> " guard checkVariationNonDefinition(), referenceExist(expectedValue: false), var screenshot = takeScreenshot(after: time) else {return} saveImage(image: screenshot, path: getPath(name: viewName)) svgManager.createFile(referenceName: viewName, referenceArea: getElementArea(), svgSize: UITestsScreenshots.screenSize) screenshot = UITestsScreenshots.imageInScreenAcrea(image: screenshot, area: getElementArea()) let preview = UITestsScreenshots.createPreview(title: "Reference", image: screenshot) context.add(UITestsScreenshots.createAttachement(image: preview, name: description)) debugHistory += "done." } } func reMake(after time: TimeInterval = screenshotDelay) { XCTContext.runActivity(named: "Remake \"\(viewName)\" reference screenshot") { context in debugHistory += "re make reference -> " guard referenceImagesExist(names: [viewName]+variations, expectedValue: true), var screenshot = takeScreenshot(after: time) else {return} _ = (variations.isEmpty ? [viewName] : variations).map{ saveImage(image: screenshot, path: getPath(name: $0)) _ = svgManager.updateImage(name: $0, area: getElementArea()) } screenshot = UITestsScreenshots.imageInScreenAcrea(image: screenshot, area: getElementArea()) let preview = UITestsScreenshots.createPreview(title: "Reference", image: screenshot) context.add(UITestsScreenshots.createAttachement(image: preview, name: description)) debugHistory += "done." XCTFail("\"\(#function)\" is a temporary function, you can't succeed a test with it\nafter remaking a reference you have to use \"check()\" if you want to compare") } } func check(after time: TimeInterval = screenshotDelay) { XCTContext.runActivity(named: "Check \"\(viewName)\" screenshot with his reference") { context in debugHistory += "compare screenshot to reference -> " guard var screenshot = takeScreenshot(after: time), let reference = getReference() else {return} screenshot = convertForComparaison(screenshot: screenshot) guard let variances = UITestsScreenshots.getVarianceAreas(reference, screenshot) else {return} if (!variances.areas.isEmpty) { let errorMsg = "variances found with the reference view in \(variances.areas.count) areas." debugHistory += errorMsg XCTFail(errorMsg) } else { debugHistory += "done." } let preview = UITestsScreenshots.comparativePreview(reference: reference, screenshot: screenshot, difference: variances.image, areas: variances.areas) context.add(UITestsScreenshots.createAttachement(image: preview, name: description)) } } func addNewVariation(named name: String, after time: TimeInterval = screenshotDelay) { XCTContext.runActivity(named: "Add \"\(viewName)\" reference screenshot new varation named \(name)") { context in debugHistory += "add variances to a new variation named \(name) -> " guard referenceImagesExist(names: [name], expectedValue: false), let screenshot = takeScreenshot(after: time), let reference = getReference() else {return} guard let variances = UITestsScreenshots.getVarianceAreas(reference, convertForComparaison(screenshot: screenshot)) else {return} guard !variances.areas.isEmpty else { XCTFail(debugHistory + "error! : no variances found with the reference view") return } saveImage(image: screenshot, path: getPath(name: name)) _ = svgManager.addVariation(referenceName: viewName, name: name, area: getElementArea(), rects: variances.areas) let preview = UITestsScreenshots.variationPreview(reference: reference, areas: variances.areas) context.add(UITestsScreenshots.createAttachement(image: preview, name: description)) debugHistory += "done." } } func addNewFilterVariation(named name: String, color: UIColor, areas: [CGRect]) { XCTContext.runActivity(named: "Add \"\(viewName)\" reference screenshot new filter varation named \(name)") { context in let size = UITestsScreenshots.screenSize UIGraphicsBeginImageContextWithOptions(size, false, 1) color.setFill() UIRectFillUsingBlendMode(CGRect(x: 0, y: 0, width: size.width, height: size.height), .normal) let image = UIGraphicsGetImageFromCurrentImageContext()! UIGraphicsEndImageContext() saveImage(image: image, path: getPath(name: name)) _ = svgManager.addVariation(referenceName: viewName, name: name, area: getElementArea(), rects: areas) context.add(UITestsScreenshots.createAttachement(image: image, name: description)) debugHistory += "done." } } func addToMask(after time: TimeInterval = screenshotDelay) { XCTContext.runActivity(named: "Add new areas to \"\(viewName)\" mask") { context in debugHistory += "add variances to mask -> " guard let screenshot = takeScreenshot(after: time), let reference = getReference() else {return} guard let variances = UITestsScreenshots.getVarianceAreas(reference, convertForComparaison(screenshot: screenshot)) else {return} guard !variances.areas.isEmpty else { XCTFail(debugHistory + "error! : no variances found with the reference view") return } _ = svgManager.addToMask(rects: variances.areas) let preview = UITestsScreenshots.variationPreview(reference: reference, areas: variances.areas) context.add(UITestsScreenshots.createAttachement(image: preview, name: description)) debugHistory += "done." } } func isAnimated(timeInterval: TimeInterval) { XCTContext.runActivity(named: "Check \"\(viewName)\" animation") { context in debugHistory += "check if element is animated -> " if (takeScreenshot(after: UITestsScreenshots.screenshotDelay)?.pngData() == takeScreenshot(after: timeInterval)?.pngData()) { XCTFail("no animation detected for \"\(viewName)\"") } debugHistory += "done." } } func takeScreenshot(after time: TimeInterval) -> UIImage? { XCTContext.runActivity(named: "take screenshot") { context in debugHistory += "take screenshot -> " _=XCTWaiter.wait(for: [XCTestExpectation()], timeout: time) return UIImage(data: element.screenshot().pngRepresentation) } } func getReference() -> UIImage? { debugHistory += "get reference -> " guard svgManager.parse(withVariations: variations), referenceImagesExist(names: [viewName]+variations, expectedValue: true) == true else {return nil} let filePaths = [getPath(name: viewName)]+variations.map{getPath(name: $0)} let imagesName = [viewName] + variations.map{viewName+"_"+$0} var images = [UIImage]() for i in 0...imagesName.count-1 { let image = getImage(path: filePaths[i])! let area = svgManager.images[imagesName[i]]!.area var clip = [CGRect]() if (i>0) {clip = svgManager.rects[variations[i-1]]!} images.append(UITestsScreenshots.imageInScreenAcrea(image: image, area: area, clip: clip)) } UIGraphicsBeginImageContextWithOptions(UITestsScreenshots.screenSize, false, 1) _ = images.map{$0.draw(at: CGPoint(x: 0,y: 0))} let reference = UIGraphicsGetImageFromCurrentImageContext()! UIGraphicsEndImageContext() return imageWithMask(image: reference, mask: svgManager.rects["mask"]!) } //create attachement to return when calling public functions static func createAttachement(image: UIImage, name: String, lifetime: XCTAttachment.Lifetime = .deleteOnSuccess) -> XCTAttachment { let attachment = XCTAttachment(image: image) attachment.lifetime = lifetime attachment.name = name return attachment } //image operations to prepare comparison static func imageInScreenAcrea(image: UIImage, area: CGRect, clip: [CGRect] = []) -> UIImage { UIGraphicsBeginImageContextWithOptions(UITestsScreenshots.screenSize, false, 1) if (!clip.isEmpty) {UIGraphicsGetCurrentContext()!.clip(to: clip)} image.draw(at: CGPoint(x: area.minX, y: area.minY)) let screenImage = UIGraphicsGetImageFromCurrentImageContext()! UIGraphicsEndImageContext() return screenImage } static func setBackground(image: UIImage, color: UIColor) -> UIImage { let size = UITestsScreenshots.screenSize UIGraphicsBeginImageContextWithOptions(size, false, 1) color.setFill() UIRectFill(CGRect(x: 0, y: 0, width: size.width, height: size.height)) image.draw(at: CGPoint(x: 0,y: 0)) let newImage = UIGraphicsGetImageFromCurrentImageContext()! UIGraphicsEndImageContext() return newImage } internal func imageWithMask(image: UIImage, mask: [CGRect]) -> UIImage { UIGraphicsBeginImageContextWithOptions(UITestsScreenshots.screenSize, false, 1) image.draw(at: CGPoint(x: 0, y: 0)) _ = mask.map{UIRectFill($0)} let maskedImage = UIGraphicsGetImageFromCurrentImageContext()! UIGraphicsEndImageContext() return maskedImage } func convertForComparaison(screenshot: UIImage) -> UIImage { var area = getElementArea() if (svgManager.rects["mask"] == nil) {_=svgManager.parse()} var image = UITestsScreenshots.imageInScreenAcrea(image: screenshot, area: area) image = imageWithMask(image: image, mask: svgManager.rects["mask"]!) return image } func getElementArea() -> CGRect { XCTContext.runActivity(named: "get element coordinates") { _ in let area = element.frame let rect = CGRect(x: area.minX*3, y: area.minY*3, width: area.width*3, height: area.height*3) return rect } } //comparison functions static func getVarianceAreas(_ image1: UIImage, _ image2: UIImage) -> (image: UIImage, areas: [CGRect])? { let margin: CGFloat = 20 let replacementColor: UInt8 = 255 var areas = [CGRect]() //compare images UIGraphicsBeginImageContextWithOptions(image1.size, false, 1) setBackground(image: image1, color: UIColor.black).draw(at: CGPoint(x: 0, y: 0)) setBackground(image: image2, color: UIColor.black).draw(at: CGPoint(x: 0, y: 0), blendMode: .difference, alpha: 1) let image = UIGraphicsGetImageFromCurrentImageContext()! UIGraphicsEndImageContext() //findRects let exeptMsg = "error! : unexpected error during image conversion for comparison" guard let inputCGImage = image.cgImage else { XCTFail(exeptMsg) return nil } let colorSpace = CGColorSpaceCreateDeviceGray() let width = inputCGImage.width let height = inputCGImage.height let bytesPerPixel = 1 let bitsPerComponent = 8 let bytesPerRow = bytesPerPixel * width let bitmapInfo = CGImageAlphaInfo.none.rawValue guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) else { XCTFail(exeptMsg) return nil } context.draw(inputCGImage, in: CGRect(x: 0, y: 0, width: width, height: height)) guard let buffer = context.data else { XCTFail(exeptMsg) return nil } let pixelBuffer = buffer.bindMemory(to: UInt8.self, capacity: width * height) var rects: [Int:[CGRect]] = [:] for row in 0 ..< Int(height) { for column in 0 ..< Int(width) { let offset = row * width + column if (pixelBuffer[offset] > UInt8(colorTreshold)) { pixelBuffer[offset] = replacementColor let point = CGPoint(x: column, y: row) var rect = CGRect(x: point.x, y: point.y, width: 1, height: 1) for i in 0...1 { if let prevRects = rects[row-i] { for j in 0..= CGFloat(pixelTreshold) && rect.height >= CGFloat(pixelTreshold)) { areas.append(rect) } mergeCloseAreas(&areas, withMargin: margin) } } } let diff = UIImage(cgImage: (CGContext(data: pixelBuffer, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo)?.makeImage())!) return (diff,areas) } private static func mergeCloseAreas(_ areas: inout [CGRect], withMargin margin: CGFloat) { guard (areas.count >= 1) else {return} let areaSize = areas.count for i in 1...areaSize { if (i != 1 && areas.last!.intersects(areas[areaSize-i].insetBy(dx: -margin, dy: -margin))) { areas[areas.count-1] = areas.last!.union(areas[areaSize-i]) areas.remove(at: areaSize-i) mergeCloseAreas(&areas, withMargin: margin) break } } } //preview functions static func comparativePreview(reference: UIImage, screenshot: UIImage, difference: UIImage, areas: [CGRect]) -> UIImage { let realPreview = createPreview(title: "Real", image: screenshot, areas: areas, strokeColor: UIColor.red) let refPreview = createPreview(title: "Reference", image: reference, areas: areas, strokeColor: UIColor.red) let difPreview = createPreview(title: "Difference", image: difference) return createPreviewTable(images: refPreview,realPreview,difPreview) } static func variationPreview(reference: UIImage, areas: [CGRect]) -> UIImage { let variationPreview = createPreview(title: "Reference", image: reference, areas: areas, fillColor: UIColor.blue.withAlphaComponent(0.3),strokeColor: UIColor.blue) return variationPreview } static func createPreviewTable(images: UIImage...) -> UIImage { let sideMargin: CGFloat = 6 let imageSize = images.first!.size let globalSize = CGSize(width: (imageSize.width+sideMargin)*CGFloat(images.count), height: imageSize.height+sideMargin) UIGraphicsBeginImageContextWithOptions(globalSize, false, 1) for i in 0...images.count-1 { images[i].draw(at: CGPoint(x: CGFloat(i)*(imageSize.width+sideMargin), y: sideMargin)) } let tableImage = UIGraphicsGetImageFromCurrentImageContext()! UIGraphicsEndImageContext() return tableImage } static func createPreview(title: String, image: UIImage, areas: [CGRect] = [], fillColor: UIColor? = nil, strokeColor: UIColor? = nil) -> UIImage { let lineWidth: CGFloat = 3 let bottomMargin = image.size.height*0.05 let allSize = CGSize(width: image.size.width + lineWidth*2, height: image.size.height + bottomMargin + lineWidth*2) UIGraphicsBeginImageContextWithOptions(allSize, false, 1) //image image.draw(at: CGPoint(x: lineWidth, y: lineWidth), blendMode: .normal, alpha: 1) //rects UIGraphicsGetCurrentContext()?.setLineWidth(lineWidth) for area in areas { let newArea = area.offsetBy(dx: lineWidth, dy: lineWidth) if (fillColor != nil) { fillColor?.setFill() UIRectFillUsingBlendMode(newArea,.normal) } if (strokeColor != nil) { strokeColor?.setStroke() UIRectFrameUsingBlendMode(newArea.insetBy(dx: -3, dy: -3),.normal) } } //title let textFont = UIFont(name: "Helvetica Bold", size: bottomMargin/2) ?? UIFont() let textStyle=NSMutableParagraphStyle() textStyle.alignment=NSTextAlignment.center let textColor = UIColor.black let textAttributes = [NSAttributedString.Key.font: textFont, NSAttributedString.Key.paragraphStyle: textStyle, NSAttributedString.Key.foregroundColor: textColor] as [NSAttributedString.Key : Any] let text_h = textFont.lineHeight let text_y = allSize.height-bottomMargin + (bottomMargin-text_h)/2 let text_rect = CGRect(x: 0, y: text_y, width: allSize.width, height: bottomMargin) title.draw(in: text_rect, withAttributes: textAttributes) let preview = UIGraphicsGetImageFromCurrentImageContext()! UIGraphicsEndImageContext() return preview } //util functions private func referenceExist(expectedValue: Bool) -> Bool { guard (FileManager.default.fileExists(atPath: svgManager.path) == expectedValue) else { XCTFail(debugHistory + "error! : \(svgManager.path+(expectedValue ? " does not" : " already")) exist") return false } return true } private func referenceImagesExist(names: [String], expectedValue: Bool) -> Bool { guard svgManager.parse() else {return false} for name in names { let realName = viewName + (name != viewName ? "_"+name : "") if ((svgManager.images[realName] == nil) == expectedValue) { XCTFail(debugHistory + "error! : \(realName) image \(expectedValue ? "does not" : "already") exist") return false } } return true } private func checkVariationNonDefinition(caller: String = #function) -> Bool { guard variations.isEmpty else { XCTFail(debugHistory + "error : \"\(caller)\" function works only for views, not for views variations") return false } return true } //disk operation functions private func getPath(name: String) -> String{ return "\(UITestsScreenshots.defaultPath)images/\(viewName+((name != viewName) ? "_"+name : "")).png" } private func saveImage(image: UIImage, path: String) { guard let data = image.pngData() else { XCTFail("error != unable to save image at \(path)") return } do { try data.write(to: URL(fileURLWithPath: path)) } catch { NSLog(error.localizedDescription) XCTFail(debugHistory + "error != unable to save image at \(path)") } } private func getImage(path: String) -> UIImage? { guard let image = UIImage(contentsOfFile: path) else { XCTFail(debugHistory + "error != unable to get image at \(path)") return nil } return image } } class SVGManager : NSObject { var path: String var parentPath: String var rects: [String : [CGRect]] = [:] var images: [String : (area: CGRect, line: Int)] = [:] private var parser: XMLParser? private var current: String? private var defaultMask = [CGRect(x: 376, y: 2492, width: 418, height: 16)] private var referenceStart: Int? private var maskStart: Int? private var clipPathStart: Int? private var variationsStart: Int? private var selectedVariationStarts: [String : Int]? init(path: String) { self.path = path let index = path.lastIndex(of: "/") self.parentPath = (index != nil) ? String(path.prefix(path.distance(from: path.startIndex, to: index!)+1)) : "" } func svgRect(_ rect: CGRect, lock: Bool) -> String { return "" } func svgImage(name: String, area: CGRect, lock: Bool) -> String { return "" } func createFile(referenceName name: String, referenceArea area: CGRect, svgSize: CGSize) { let width = svgSize.width let height = svgSize.height var content = [String]() content.append("") content.append("") content.append(" \(svgImage(name: name, area: area, lock: true))") content.append("") content.append("\n \n") content.append("") content.append("") _ = defaultMask.map{content.append(" \(svgRect($0, lock: true))")} content.append("") content.append("") let svg = content.joined(separator: "\n") try! svg.write(toFile: path, atomically: true, encoding: String.Encoding.utf8) } func updateImage(name: String, area: CGRect) -> Bool { guard parse(), images[name] != nil else {return false} var svgData = try! String(contentsOf: URL(fileURLWithPath: path)).split(separator: "\n") var lineIndex = images[name]!.line-1 var line = svgData[lineIndex] while line.firstIndex(of: "<") == nil { svgData.remove(at: lineIndex) lineIndex += -1 line = svgData[lineIndex] } svgData[lineIndex] = line.prefix(line.distance(from: line.startIndex, to: line.firstIndex(of: "<")!)) + Substring(svgImage(name: name, area: area, lock: true)) try! String(svgData.joined(separator: "\n")).write(toFile: path, atomically: true, encoding: String.Encoding.utf8) return true } func addToMask(rects: [CGRect]) -> Bool { guard parse() else {return false} var content = "" for rect in rects { content += "\(svgRect(rect, lock: false))\n" } svgInsert(content, at: maskStart!) return true } func addVariation(referenceName: String, name: String, area: CGRect, rects: [CGRect]) -> Bool { guard parse() else {return false} var content = "\n" content += " \(svgImage(name: "\(referenceName)_\(name)", area: area, lock: true))\n" for rect in rects { content += " \(svgRect(rect, lock: false))\n" } content += "\n" svgInsert(content, at: variationsStart!) content = "" svgInsert(content, at: clipPathStart!, parentElement: "clipPath") return true } //if some variations are delete with a svg editor like Inkscape, this variations are copied in the clipPath wich break some functionalities. //this function is called at eeach parse to clean the clipPath if needed. func removeClipPathAnomalies() { guard (clipPathStart ?? 0 < (variationsStart ?? 0 )-2) else {return} var svgData = try! String(contentsOf: URL(fileURLWithPath: path)).split(separator: "\n") var index = clipPathStart! var use = false var line = svgData[index] while !line.contains("") && !line.contains("") { if (use == true) { if (line.contains("/>")) {use = false} } else { if (line.range(of: " Bool { rects["mask"] = [] _ = headers.map{rects[$0] = []} parser = XMLParser(contentsOf: URL(fileURLWithPath: path)) guard (parser != nil) else {return false} parser!.delegate = self parser!.parse() removeClipPathAnomalies() return true } func svgInsert(_ content: String, at index: Int, parentElement: String = "g") { var svgData = try! String(contentsOf: URL(fileURLWithPath: path)).split(separator: "\n") let line = svgData[index-1] let prefix = line.prefix(line.distance(from: line.startIndex, to: line.firstIndex(of: "<") ?? line.startIndex)) if (line.suffix(2) == "/>") { svgData[index-1] = line.prefix(line.count-2)+">" svgData.insert(prefix+"", at: index) } svgData.insert(prefix+Substring(" "+content), at: index) try! String(svgData.joined(separator: "\n")).write(toFile: path, atomically: true, encoding: String.Encoding.utf8) } } extension SVGManager : XMLParserDelegate { func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) { if (elementName == "g") { if (rects[attributeDict["id"] ?? ""] != nil) { current = attributeDict["id"] } if (attributeDict["id"] == "reference") { referenceStart = parser.lineNumber } else if (attributeDict["id"] == "mask") { maskStart = parser.lineNumber } else if (attributeDict["id"] == "variations") { variationsStart = parser.lineNumber } } if (elementName == "clipPath" && attributeDict["id"] == "clipPath") { clipPathStart = parser.lineNumber } if (elementName == "rect" && current != nil) { if current != nil { rects[current!]?.append(getRect(attributes: attributeDict))} } if (elementName == "image") { guard var imageName = attributeDict["xlink:href"] else {return} guard FileManager.default.fileExists(atPath: imageName) || FileManager.default.fileExists(atPath: parentPath+imageName) else { return } imageName = String(imageName.suffix(imageName.distance(from: imageName.lastIndex(of: "/") ?? imageName.startIndex, to: imageName.endIndex)-1)) imageName = String(imageName.prefix(imageName.distance(from: imageName.startIndex, to: imageName.lastIndex(of: ".") ?? imageName.endIndex))) images[imageName] = (getRect(attributes: attributeDict),parser.lineNumber) } } func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) { if (elementName == "g") { current = nil } } private func getRect(attributes: [String:String]) -> CGRect { var values = [Int]() for header in ["x","y","width","height"] { if let val = NumberFormatter().number(from: attributes[header] ?? "0") { values.append(Int(truncating: val)) } } return CGRect(x: values[0], y: values[1], width: values[2], height: values[3]) } }