本文探讨使用 SwiftUI 开发所见即所得 (WYSIWYG) 矢量绘图应用程序。所见即所得的矢量绘图应用程序可以被认为是在画布上呈现不同对象(例如矩形、椭圆形、文本或其他形状)的应用程序;通过拖动在画布周围移动对象;并通过点击对象来更改对象属性。这在下面的 iOS 应用程序的屏幕截图中进行了说明。具体来说,本文探讨了将 SwiftUI 视图用作“绘图画布”,而不是使用 Core Graphics 画布或 SwiftUI 画布。

[图像描述](https://res.cloudinary.com/practicaldev/image/fetch/s--UPvtSJi9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to- uploads.s3.amazonaws.com/uploads/articles/jwf55x8587hz1lq9k7le.png)

为什么要用 SwiftUI View 作为绘图画布?

在 Apple 生态系统中开发矢量绘图应用程序时,首先想到的是 Core Graphics 或 SwiftUI Canvas。两者都非常快速,易于使用,并为我们提供了画布。当然,作为绘图应用程序的画布,两者都是不错的选择。然而,当绘图应用程序需要 WYSIWYG 行为时,Swift 开发人员意识到处理对象交互(如拖动、移动和调整大小)需要使用 Apple 的手势和事件。

这使得 Swift 开发人员很难忽略将 SwiftUI 视图用作“绘图画布”的情况。这是因为 SwiftUI 视图直接支持所有 Apple 的手势和事件。退一步想一想,SwiftUI View 是 Apple 为用户界面 (UI) 开发而设计的,这样的 View 已经自然支持 WYSIWYG 应用程序所需的所有行为:渲染视图和对象,以及支持手势和事件。将 SwiftUI 视图用作“绘图画布”也不会阻止我们使用 Core Graphics、SwiftUI 画布甚至 Metal 来渲染需要特殊处理的底层对象,因为这三者都可以轻松地表示为 SwiftUI 视图。

勘探成果

开源

SwiftUI-WYSIWYG-Draw- GitHub(MIT 许可证)

应用程序

Barcode & Label- Apple App Store 上提供的标签设计 (WYSIWYG)、邮寄地址、信封和条形码打印应用程序

以下部分重点介绍了使用 SwiftUI 的优点、缺点和挑战,将 SwiftUI 视图作为“绘图画布”,用于开发所见即所得的应用程序。这可能不是适合每个人的设计,但强调的一些要点可能对所见即所得绘图应用程序开发人员有用。

设计

如上所述,我们将使用 SwiftUI 视图作为“绘图画布”来绘制形状、文本或图像。但是我们如何去做呢?以下是尽可能简单的。

         var body: some View {        
            ZStack {
                ForEach(shapes.shapeList){
                    shape in
                    ShapeViewX()
                    }            
            }
        } 

进入全屏模式 退出全屏模式

基本概念是仅在 SwiftUI 视图内的 ZStack 上绘制形状列表(表示为 SwiftUI 视图)。形状和形状类的列表定义如下:

         class ShapesX{

            @Published var shapeList = [ShapeX]()

        }

        class ShapeX {

            @Published var location: CGPoint = CGPoint(x: 50, y: 50)
            @Published var size: CGSize = CGSize(width: 100, height: 100)
            @Published var canvasSize: CGSize = CGSize(width: 500, height: 500)

        }

进入全屏模式 退出全屏模式

注意 - 上面的代码被简化以帮助更容易地传达概念。例如,删除了“ObservableObject”属性。

ShapesX 和 ShapeX 可以看作是我们矢量图的模型对象。我们通过使用 SwiftUI 视图来表示每个形状,从而在 SwiftUI 视图中呈现它们。下面的 ShapeViewX 用于此目的。有趣的是,ShapeViewX 也可以使用 SwiftUI Canvas、Core Image Canvas 或 Metal 来实现。对于我们的场景,我们将使用 SwiftUI 形状(例如 Rectangle、Ellipse)或将 SwiftUI View 包裹在 SwiftUI Text 周围以表示画布上的对象。

         struct ShapeViewX: View {

            @ObservedObject var shape: ShapeX

            //Handle the interactions such as Move and Resize
            //Draw a Bounding Box   

            //Draw the Shape        
            var body: some View {
              self.shape.view() 
              //depending of the type shape is, we can draw a rectangle or ellipse etc. 
            }
        }

进入全屏模式 退出全屏模式

ShapeViewX 用于渲染不同类型的形状,例如矩形或椭圆。绘制矩形的视图函数如下图所示。

class RectangleX: ShapeX {

            @Published var strokeWidth:Double = 1
            @Published var strokeColor = Color.black
            @Published var fillColor = Color.white

            override func view() -> AnyView {
                AnyView(
                    Rectangle()
                        .stroke(strokeColor, lineWidth: strokeWidth)
                        .background(Rectangle().fill(fillColor))
                        .frame(width: self.size.width, height: self.size.height)
                        .position(self.location)
                        )

                }
        }

进入全屏模式 退出全屏模式

除了绘制形状外,ShapeViewX 还可用于在一个地方轻松管理所有手势和事件。如下图所示。

手势

ShapeViewX 的平移(拖动移动)手势可以很简单,如下所示:

struct ShapeViewX: View {

            @ObservedObject var shape: ShapeX
            @State private var location: CGPoint = CGPoint(x: 50, y: 50)

            var drag: some Gesture {
              DragGesture()
                 .onChanged { value in
                     self.shape.location = value.location
                 }
            }

            var body: some View {
                ZStack
                {      
                       self.shape.view()

                   Rectangle()
                    .fill(Color.white)
                    .opacity(0.001)
                    .frame(width: self.shape.size.width, height: self.shape.size.height)
                    .position(self.shape.location)
                    .gesture(
                            drag
                    )
           }  
            }
        }

进入全屏模式 退出全屏模式

如果您测试上面的代码,当我们开始拖动矩形时,您会看到一个小小的跳跃。下面的文章很好地解释了这个问题并提供了解决方案。

https://sarunw.com/posts/move-view-around-with-drag-gesture-in-swiftui

打印问题

将 SwiftUI 视图用作 WYSIWYG 非常适合处理事件和手势。但对于许多矢量绘图应用程序而言,导出、打印和/或生成 PDF 输出的需求也很重要。如果我们需要生成 PDF,我们可以实现如下所示:

A手动解决方案

         let pageRect = CGRect(x:0, y:0, width: 8.5*72, height: 11.0*72)

         let renderer = UIGraphicsPDFRenderer(bounds:pageRect , format: format)    

         let pdf = renderer.pdfData { (context) in
            context.beginPage()
            let attributes = [
              NSFontAttributeName : UIFont.boldSystemFont(ofSize: 150)
            ]
            let text = "Hello!" as NSString
            text.draw(in: CGRect(x: 0, y: 0, width: 500, height: 200), withAttributes: attributes)
          }

进入全屏模式 退出全屏模式

在上面,我们在 PDF 中绘制了一个文本(“Hello”)。我们可以想象,我们需要将我们的每一个形状和文本翻译成PDF,代码与上面类似。如果需要对每个渲染细节进行细粒度控制,这将很有用。但是,它可能很乏味。

将 SwiftUI 视图转换为 PDF

或者,我们可以将 SwiftUI 视图转换为 PDF。如果我们不想手动将每个形状转换为 PDF,这是一种快速简便的方法。

有关详细信息,请参阅以下内容:

https://stackoverflow.com/questions/60753436/convert-swiftui-view-to-pdf-on-ios?msclkidu003dead66797becb11ec9c47d91ad8634470

下面的代码说明了如何使用 UIGraphicsPDFRenderer 将 SwiftUI 视图及其子对象转换为 PDF。

func exportToPDF() {

            let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
            let outputFileURL = documentDirectory.appendingPathComponent("SwiftUI.pdf")

            let width: CGFloat = 8.5 * 72.0
            let height: CGFloat = 11.0 * 72.0
            let printout = SwiftUICanvasView()

            let pdfVC = UIHostingController(rootView: printout)
            pdfVC.view.frame = CGRect(x: 0, y: 0, width: width, height: height)

            //Render the view behind all other views
            let rootVC = UIApplication.shared.windows.first?.rootViewController
            rootVC?.addChild(pdfVC)
            rootVC?.view.insertSubview(pdfVC.view, at: 0)

            //Render the PDF
            let pdfRenderer = UIGraphicsPDFRenderer(bounds: CGRect(x: 0, y: 0, width: 8.5 * 72.0, height: height))

            do {
                try pdfRenderer.writePDF(to: outputFileURL, withActions: { (context) in
                    context.beginPage()
                    pdfVC.view.layer.render(in: context.cgContext)
                })

                self.exportURL = outputFileURL
                self.showExportSheet = true

            }catch {
                self.showError = true
                print("Could not create PDF file: \(error)")
            }

            pdfVC.removeFromParent()
            pdfVC.view.removeFromSuperview()
        }               

进入全屏模式 退出全屏模式

将 SwiftUI 视图转换为 PDF 的问题

如果您更仔细地查看对话线程,您会注意到以下内容:

“当我尝试使用其他答案中的解决方案生成 PDF 文件时,我只得到了一个模糊的 PDF,而且质量远非好。”帕韦洛2222

当 SwiftUI 视图转换为 PDF 时,它会以每英寸 72 点 (DPI) 进行转换。如果您的绘图包含高分辨率对象(例如图像),它们在 PDF 中将变得模糊。所见即所得图形应用程序的低分辨率输出并不理想。如果我们不能接受较低分辨率的输出,是否意味着我们又回到了将 SwiftUI 视图转换为 PDF 的手动方法?

“我最终在更大的框架中生成了 SwiftUI 视图,并将上下文缩小到适当的大小。”帕韦洛2222

可能的解决方案

一种解决方案可能是以所需的 DPI(例如 300 DPI)渲染“Canvas”(SwiftUI 视图),然后使用“scaleEffect”将其缩小以在应用程序上查看。例如,我们可以以 300 DPI 渲染 8.5 x 11.0(英寸)的画布。要在应用程序上查看它,我们可以将其缩小 72.0/300.0 倍(72 DPI 是 iOS 应用程序使用的 DPI),如下所示。

            SwiftUICanvasView() //at 300 DPI
            .frame(width: 8.5*300, height: 11.0*300, alignment: .center)
            .scaleEffect(72.0/300.0)

进入全屏模式 退出全屏模式

当以 300 DPI 将 SwiftUICanvasView 渲染为 PDF 时,我们的 PDF 及其对象将不再模糊。

额外的复杂性

以较高的 DPI 渲染画布然后缩小查看的设计无疑会增加应用程序的资源需求。还会有额外的代码开发复杂性。例如,以前以 16 字体大小(72 DPI)呈现的文本,在 300 DPI 画布上呈现时,将需要 16 * 300.0/72.0 的字体大小。这种字体大小的转换需要进行管理。下面的代码显示了如何:

class TextX: ShapeX {

    //@Published var fontSize:CGFloat = 16.0
    @Published var fontSize:CGFloat = 16.0*300.0/72.0

}        

进入全屏模式 退出全屏模式

我们做了什么?

总之,我们详细介绍了使用 SwiftUI View 作为绘图画布来呈现形状和文本以及处理手势和事件。我们以 300 DPI 绘制了 SwiftUI 视图,并使用“scaleEffect”将其缩小以在应用程序上查看。然后我们使用 SwiftUI View(300 DPI)并将其转换为 72 PDF。我们还强调了这样做的额外复杂性。

上面的设计对每个开发所见即所得应用程序的人都有好处吗?当然不!但我希望,至少有些部分对考虑使用 SwiftUI 开发 WYSIWYG 应用程序的人有用。对我们来说,我们已经使用它开发了标签设计所见即所得和条码打印应用程序。

是事物的眼泪,触动凡人的心灵——青铜

Logo

ModelScope旨在打造下一代开源的模型即服务共享平台,为泛AI开发者提供灵活、易用、低成本的一站式模型服务产品,让模型应用更简单!

更多推荐