diff --git a/CommandLine/CommandLine.swift b/CommandLine/CommandLine.swift index a6b478b..1ed57be 100644 --- a/CommandLine/CommandLine.swift +++ b/CommandLine/CommandLine.swift @@ -87,7 +87,7 @@ Options: --hide-unsupported-filters hide elements with unsupported filters. Available keys for --format swift: - --api api of generated code: appkit | uikit + --api api of generated code: swiftui | appkit | uikit Available keys for --format sfsymbol: --insets alignment of regular variant: top,left,bottom,right | auto diff --git a/README.md b/README.md index 3c518a4..fd1a5ca 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ Options: --hide-unsupported-filters hide elements with unsupported filters. Available keys for --format swift: - --api api of generated code: appkit | uikit + --api api of generated code: swiftui | appkit | uikit Available keys for --format sfsymbol: --insets alignment of regular variant: top,left,bottom,right | auto @@ -196,47 +196,66 @@ $ swiftdraw simple.svg --format swift ``` ```swift -extension UIImage { - static func svgSimple(size: CGSize = CGSize(width: 160.0, height: 160.0)) -> UIImage { - let f = UIGraphicsImageRendererFormat.preferred() - f.opaque = false - let scale = CGSize(width: size.width / 160.0, height: size.height / 160.0) - return UIGraphicsImageRenderer(size: size, format: f).image { - drawSimple(in: $0.cgContext, scale: scale) +struct SimpleView: View { + + var body: some View { + if isResizable { + canvas + .frame(idealWidth: 160.0, idealHeight: 160.0) + } else { + canvas + .frame(width: 160.0, height: 160.0) } } - private static func drawSimple(in ctx: CGContext, scale: CGSize) { - ctx.scaleBy(x: scale.width, y: scale.height) - let rgb = CGColorSpaceCreateDeviceRGB() - let color1 = CGColor(colorSpace: rgb, components: [1, 0.98, 0.98, 1])! - ctx.setFillColor(color1) - ctx.fill(CGRect(x: 0, y: 0, width: 160, height: 160)) - let color2 = CGColor(colorSpace: rgb, components: [1, 0.753, 0.796, 1])! - ctx.setFillColor(color2) - let path = CGMutablePath() - path.move(to: CGPoint(x: 80, y: 30)) - path.addCurve(to: CGPoint(x: 30, y: 80), - control1: CGPoint(x: 52.39, y: 30), - control2: CGPoint(x: 30, y: 52.39)) - path.addCurve(to: CGPoint(x: 80, y: 130), - control1: CGPoint(x: 30, y: 107.61), - control2: CGPoint(x: 52.39, y: 130)) - path.addCurve(to: CGPoint(x: 130, y: 80), - control1: CGPoint(x: 107.61, y: 130), - control2: CGPoint(x: 130, y: 107.61)) - path.addLine(to: CGPoint(x: 80, y: 80)) - path.closeSubpath() - ctx.addPath(path) - ctx.fillPath() - ctx.setLineCap(.butt) - ctx.setLineJoin(.miter) - ctx.setLineWidth(2) - ctx.setMiterLimit(4) - let color3 = CGColor(colorSpace: rgb, components: [0, 0, 0, 1])! - ctx.setStrokeColor(color3) - ctx.addPath(path) - ctx.strokePath() + private var isResizable = false + + func resizable() -> Self { + var copy = self + copy.isResizable = true + return copy + } + + var canvas: some View { + Canvas( + opaque: false, + colorMode: .linear, + rendersAsynchronously: false + ) { context, size in + let scale = CGSize(width: size.width / 160.0, height: size.height / 160.0) + context.withCGContext { ctx in + ctx.scaleBy(x: scale.width, y: scale.height) + let rgb = CGColorSpaceCreateDeviceRGB() + let color1 = CGColor(colorSpace: rgb, components: [1, 0.98, 0.98, 1])! + ctx.setFillColor(color1) + ctx.fill(CGRect(x: 0, y: 0, width: 160, height: 160)) + let color2 = CGColor(colorSpace: rgb, components: [1, 0.753, 0.796, 1])! + ctx.setFillColor(color2) + let path = CGMutablePath() + path.move(to: CGPoint(x: 80, y: 30)) + path.addCurve(to: CGPoint(x: 30, y: 80), + control1: CGPoint(x: 52.39, y: 30), + control2: CGPoint(x: 30, y: 52.39)) + path.addCurve(to: CGPoint(x: 80, y: 130), + control1: CGPoint(x: 30, y: 107.61), + control2: CGPoint(x: 52.39, y: 130)) + path.addCurve(to: CGPoint(x: 130, y: 80), + control1: CGPoint(x: 107.61, y: 130), + control2: CGPoint(x: 130, y: 107.61)) + path.addLine(to: CGPoint(x: 80, y: 80)) + path.closeSubpath() + ctx.addPath(path) + ctx.fillPath() + ctx.setLineCap(.butt) + ctx.setLineJoin(.miter) + ctx.setLineWidth(2) + ctx.setMiterLimit(4) + let color3 = CGColor(colorSpace: rgb, components: [0, 0, 0, 1])! + ctx.setStrokeColor(color3) + ctx.addPath(path) + ctx.strokePath() + } + } } } ``` diff --git a/SwiftDraw/Sources/CommandLine/CommandLine+Process.swift b/SwiftDraw/Sources/CommandLine/CommandLine+Process.swift index 2a470e2..7dd05b9 100644 --- a/SwiftDraw/Sources/CommandLine/CommandLine+Process.swift +++ b/SwiftDraw/Sources/CommandLine/CommandLine+Process.swift @@ -90,10 +90,11 @@ public extension CommandLine { } static func makeTextAPI(for api: CommandLine.API?) -> CGTextRenderer.API { - guard let api = api else { return .uiKit } + guard let api = api else { return .swiftUI} switch api { case .appkit: return .appKit case .uikit: return .uiKit + case .swiftui: return .swiftUI } } diff --git a/SwiftDraw/Sources/CommandLine/CommandLine.Configuration.swift b/SwiftDraw/Sources/CommandLine/CommandLine.Configuration.swift index df27913..e93d961 100644 --- a/SwiftDraw/Sources/CommandLine/CommandLine.Configuration.swift +++ b/SwiftDraw/Sources/CommandLine/CommandLine.Configuration.swift @@ -63,6 +63,7 @@ extension CommandLine { public enum API: String { case appkit case uikit + case swiftui } public enum Size: Equatable { diff --git a/SwiftDraw/Sources/Renderer/Renderer.CGText.swift b/SwiftDraw/Sources/Renderer/Renderer.CGText.swift index cfbeaca..c15995c 100644 --- a/SwiftDraw/Sources/Renderer/Renderer.CGText.swift +++ b/SwiftDraw/Sources/Renderer/Renderer.CGText.swift @@ -236,6 +236,7 @@ public final class CGTextRenderer: Renderer { public enum API { case uiKit case appKit + case swiftUI } init(api: API, @@ -591,6 +592,42 @@ public final class CGTextRenderer: Renderer { """) } + func makeSwiftUI() -> String { + """ + import SwiftUI + + struct \(name)View: View { + + var body: some View { + if isResizable { + canvas + .frame(idealWidth: \(commandSize.width), idealHeight: \(commandSize.width)) + } else { + canvas + .frame(width: \(commandSize.width), height: \(commandSize.width)) + } + } + + private var isResizable = false + + func resizable() -> Self { + var copy = self + copy.isResizable = true + return copy + } + + var canvas: some View { + Canvas( + opaque: false, + colorMode: .linear, + rendersAsynchronously: false + ) { context, size in + let scale = CGSize(width: size.width / \(commandSize.width), height: size.height / \(commandSize.height)) + context.withCGContext { ctx in + + """ + } + func makeUIKit() -> String { """ import CoreGraphics @@ -632,27 +669,53 @@ public final class CGTextRenderer: Renderer { """ } - func makeTemplate() -> String { + func makeTemplateStart() -> String { switch api { case .appKit: return makeAppKit() case .uiKit: return makeUIKit() + case .swiftUI: + return makeSwiftUI() } } + func makeTemplateEnd() -> String { + switch api { + case .appKit, .uiKit: + return "\n }\n}" + case .swiftUI: + return """ + + } + } + } + } + """ + } + } + + func makeIndent() -> String { + switch api { + case .appKit, .uiKit: + return String(repeating: " ", count: 4) + case .swiftUI: + return String(repeating: " ", count: 8) + } + } + func makeText() -> String { - var template = makeTemplate() + var template = makeTemplateStart() lines.insert("ctx.scaleBy(x: scale.width, y: scale.height)", at: 0) if !patterns.isEmpty { lines.insert("let baseCTM = ctx.ctm", at: 0) } - let indent = String(repeating: " ", count: 4) + let indent = makeIndent() let lines = self.lines.map { "\(indent)\($0)" } template.append(lines.joined(separator: "\n")) - template.append("\n }\n}") + template.append(makeTemplateEnd()) return template } } diff --git a/SwiftDraw/Tests/CommandLine/CommandLine.ConfigurationTests.swift b/SwiftDraw/Tests/CommandLine/CommandLine.ConfigurationTests.swift index b269844..519b452 100644 --- a/SwiftDraw/Tests/CommandLine/CommandLine.ConfigurationTests.swift +++ b/SwiftDraw/Tests/CommandLine/CommandLine.ConfigurationTests.swift @@ -140,7 +140,7 @@ final class CommandLineConfigurationTests: XCTestCase { } func testAPIConversion() { - XCTAssertEqual(CommandLine.makeTextAPI(for: nil), .uiKit) + XCTAssertEqual(CommandLine.makeTextAPI(for: nil), .swiftUI) XCTAssertEqual(CommandLine.makeTextAPI(for: .appkit), .appKit) XCTAssertEqual(CommandLine.makeTextAPI(for: .uikit), .uiKit) } diff --git a/SwiftDraw/Tests/Renderer/Renderer.CGTextTests.swift b/SwiftDraw/Tests/Renderer/Renderer.CGTextTests.swift index c3fe9ab..e0d5716 100644 --- a/SwiftDraw/Tests/Renderer/Renderer.CGTextTests.swift +++ b/SwiftDraw/Tests/Renderer/Renderer.CGTextTests.swift @@ -84,6 +84,76 @@ final class RendererCGTextTests: XCTestCase { ) } + func testSwiftUICode() throws { + let code = try CGTextRenderer.render(svgNamed: "lines.svg", api: .swiftUI) + XCTAssertEqual( + code, + """ + import SwiftUI + + struct ImageView: View { + + var body: some View { + if isResizable { + canvas + .frame(idealWidth: 100.0, idealHeight: 100.0) + } else { + canvas + .frame(width: 100.0, height: 100.0) + } + } + + private var isResizable = false + + func resizable() -> Self { + var copy = self + copy.isResizable = true + return copy + } + + var canvas: some View { + Canvas( + opaque: false, + colorMode: .linear, + rendersAsynchronously: false + ) { context, size in + let scale = CGSize(width: size.width / 100.0, height: size.height / 100.0) + context.withCGContext { ctx in + ctx.scaleBy(x: scale.width, y: scale.height) + let rgb = CGColorSpaceCreateDeviceRGB() + let color1 = CGColor(colorSpace: rgb, components: [0, 0, 0, 1])! + ctx.setFillColor(color1) + let path = CGMutablePath() + path.addLines(between: [ + CGPoint(x: 0, y: 0), + CGPoint(x: 100, y: 100) + ]) + ctx.addPath(path) + ctx.fillPath() + ctx.setLineCap(.butt) + ctx.setLineJoin(.miter) + ctx.setLineWidth(1) + ctx.setMiterLimit(4) + ctx.setStrokeColor(color1) + ctx.addPath(path) + ctx.strokePath() + let path1 = CGMutablePath() + path1.addLines(between: [ + CGPoint(x: 100, y: 0), + CGPoint(x: 0, y: 100) + ]) + ctx.addPath(path1) + ctx.fillPath() + ctx.addPath(path1) + ctx.strokePath() + } + } + } + } + """ + ) + } + func testGradientAppleCode() throws { let code = try CGTextRenderer.render(svgNamed: "gradient-apple.svg") XCTAssertEqual( @@ -479,10 +549,10 @@ final class RendererCGTextTests: XCTestCase { private extension CGTextRenderer { - static func render(svgNamed name: String, in bundle: Bundle = .test, precision: Int = 2) throws -> String { + static func render(svgNamed name: String, in bundle: Bundle = .test, api: API = .uiKit, precision: Int = 2) throws -> String { let url = try bundle.url(forResource: name) let data = try Data(contentsOf: url) - return try render(data: data, options: .default, api: .uiKit, precision: precision) + return try render(data: data, options: .default, api: api, precision: precision) } }