在接下来的几节课中,我将带你完成一个相当复杂的赢博体育程序的构建。GraphPad赢博体育程序是一个为平板电脑设计的赢博体育程序,它实现了图形绘制程序。
在这个例子中有很多内容,所以我把这个例子的讲义分成了两部分。在这第一节课中,我将带你完成一个初始原型赢博体育程序的构建,它只有最终赢博体育程序的一个子集的功能。
这两个赢博体育最重要的特性之一就是SwiftUI Canvas()元素的使用。这个元素使我们能够创建自定义组件,这些组件绘制自己的用户界面,并响赢博体育户事件(如点击和拖动)。这些示例中的图形绘制组件将是一个自定义的Canvas()组件。
下面是一些示例代码来演示如何使用Canvas组件来绘制一些简单的形状。
struct ContentView: View {var body: some View {Canvas {context, size in let d = CGFloat(5.0) let x1 = CGFloat(25.0) let x2 = CGFloat(75.0) let circle1 = Path(ellipseIn: CGRect(x: x1, y: x1, width: d, height: d)) let circle2 = Path(ellipseIn: CGRect(x: x2, y: x2, width: d, height: d)) let edge = Path() {Path in Path。移动(到:CGPoint(x:x1+d/2,y:x1+d/2))路径。addLine(to: CGPoint(x:x2+d/2,y:x2+d/2))} context.stroke(edge,with:.color(.green),lineWidth:2) context。填充(circle1,with: .color(.red))上下文。填充(circle2,with: .color(.red))} .frame(width:200,height:200) .padding()}}
Canvas元素主体中的闭包是我们放置绘图代码的地方,用于绘制我们想要绘制的任何东西。body闭包接受两个参数,一个是GraphicsContext对象context,另一个是CGSize对象size,它告诉我们画布绘制表面的大小。为了在Canvas中绘图,我们调用GraphicsContext提供的各种绘图方法。在这个例子中,我们画了两个用绿线连接的红色圆圈。要绘制这些形状,我们首先构造表示形状的Path对象,然后将它们传递给GraphicsContext中适当的绘制方法。有两个关键的绘图方法:stroke()和fill()。Stroke()在形状周围绘制轮廓,而fill()用颜色填充形状。
GraphicsContext类使用它自己的一组数据结构来描述形状。在这个例子中,我们使用CGFloat、CGPoint和CGRect类来描述各种几何数量。用于绘制几何图形的坐标系统使用原点位于画布左上角的坐标系统。在这个绘图系统中,正x轴指向右侧,正y轴指向原点下方。
关于GraphicsContext类的更多信息可以在官方文档中找到。
由于我们的最终目标是构建一个允许用户创建自己的图形的组件,因此我们希望在画布中摆脱固定的绘图代码。相反,我们将构建一个赢博体育程序,用户可以在其中创建自己的形状对象,并让Canvas绘制用户想要的任何形状。
这样做的第一步是定义我们自己的形状对象集:
导入基础导入SwiftUI类顶点{var中心:CGPoint让半径= CGFloat(5) var偏移:CGSize = . 0 init(中心:CGPoint){自我。函数getCenter() -> CGPoint{返回CGPoint(x:center)。X + offset.width,y:居中。{function drawIn(_ context: GraphicsContext) {let circle = Path(ellipseIn: CGRect(x: center. height))X + offset。宽度-半径,y:中心。Y +偏移量。高度-半径,宽度:2 *半径,高度:2 *半径))上下文。fill(circle,with: .color(.red))} function containsPoint(_ pt: CGPoint) -> Bool {let deltaX = (center. red)。(x - pt.x) let deltaY = (center. x)y - pt.y)让distSquared = deltaX*deltaX + delay * delay如果distSquared < radius*radius{返回真}否则{返回假}}函数setOffset(_ offset: CGSize){自我。offset = offset} func endDrag(){中心。X += offset。宽度中心。Y +=偏移量。}类边缘{var原点:顶点var dest:顶点初始化(原点:顶点,dest:顶点){self。origin = origin self.dest = dest} func drawIn(_ context: GraphicsContext){让edge = Path(){路径中的路径。移动(到:origin.getCenter())路径。addLine(to: dest.getCenter())} context.stroke(edge,with:.color(.green),lineWidth:2)}}
关于这些对象,需要注意的最重要的事情是,它们包含绘图方法,这些方法将使对象能够在给定GraphicsContext的情况下绘制自己。
我们创建的形状将被存储在一个模型类中:
导入基础导入SwiftUI类GraphModel: ObservableObject{@发布的var顶点:[顶点]@发布的var边缘:[边缘]@发布的var拖动:顶点?@Published var开始:Bool = false init(){顶点=[]让x1 = CGFloat(25.0)让x2 = CGFloat(75.0)让v1 =顶点(中心:CGPoint (x: x1, y: x1))让v2 =顶点(中心:CGPoint (x: x2, y: x2))顶点= = (v1、v2)让边缘边缘边缘(产地:v1,桌子:v2) =(边缘)}func makeVertex(_中心:CGPoint){让v =顶点(中心:中心)vertices.append (v)} func startDrag (_ pt: CGPoint){开始在顶点v = true{如果v.containsPoint (pt){拖= v}}} func continueDrag(_抵消:CGSize){如果let s =拖拽{s. setoffset (offset)}} func endDrag(){如果let s =拖拽{s.endDrag()}拖拽= nil开始= false} func endDraw(_ pt: CGPoint){对于v在顶点{如果v. containspoint (pt) && (v !==拖拽){let newEdge = Edge(原点:拖拽!,dest:v) edges.append(newEdge) break}}}}
注意两个发布的属性顶点和边缘:这些列表存储我们将要绘制的对象。
用户将通过在Canvas组件中点击和拖动来创建这些对象。您在shape类和GraphModel类中看到的大部分代码都支持这些用户交互。在下一节中,我将详细解释画布中的交互式绘图是如何工作的。
用户将使用我们的Canvas组件来绘制带有顶点和边的图形。下面是我们的Canvas需要支持的一些用户交互:
为了启用赢博体育这些交互,我们将在Canvas上附加一个修饰符,使Canvas能够响应拖动事件。下面是包含了拖动处理代码的组件的更新代码。
struct ContentView:视图{@StateObject var graph: GraphModel = GraphModel() @State var dragLine:路径?@State var drawingEdge: Bool = false var gesture: some gesture {DragGesture() . onchanged({值在if !开始{graph.startDrag(value.startLocation)} else {if !drawingEdge {graph.continueDrag(value.translation)} dragLine = Path(){路径中的路径。移动(到:value.startLocation)路径。addLine(to: value.location)}}}) . onended ({value in if drawingEdge {graph. enddraw (value.location)} else {graph. enddrag ()} dragLine = nil})} var body: some View {VStack {Toggle("Draw Edges", isOn: $drawingEdge) Canvas {context, size in for e in graph。edge {e.drawIn(context)} for v in graph。vertices {v. drawwin (context)} if let dl = dragLine {if drawingEdge {context.stroke(dl,with:.color(.black),lineWidth:1)}}} .gesture(gesture) .frame(width:200,height:200) .padding() Button("Make New Vertex") {graph.makeVertex(CGPoint(x:100,y:100))}}}}
拖动事件是SwiftUI组件可以响应的一种特殊类型的手势。我们通过给Canvas附加.gesture()修饰符来设置它来响应手势。这个修饰符的主体应该包含一个手势识别器。因为手势识别器通常是相当复杂的,我在这里使用的方法是将手势识别器定义为ContentView中的一个单独的属性,然后将它传递给。gesture()修饰符。
我们在这里要响应的特定手势是DragGesture,它会响应拖动事件。以下是关于拖动手势如何工作的一些附加信息:
在我们在这里设置的DragGesture代码中,您将看到我们设置回调函数以响应拖动更改和拖动结束事件的地方。这些回调函数接受DragGesture类型的单个参数value。包含有用信息的价值。value对象有三个重要的属性需要我们注意:
startLocation
是一个CGPoint对象,它告诉我们从哪里开始拖动。翻译
是一个CGSize对象,它告诉用户自上次更改事件以来移动了多远。位置
是一个CGPoint对象,它告诉我们用户现在在哪里。响应拖动事件的逻辑有点复杂。这有几个原因。首先,拖拽在不同的时间可能意味着不同的东西。在这个赢博体育中,Canvas可以处于两种模式:一种是用户可以拖动来移动顶点的模式,另一种是用户可以拖动来绘制边缘的模式。drawingEdge状态变量充当这两种模式之间的切换器。第二个问题是,不存在“拖拽启动”事件这样的东西供我们响应。相反,我们只获得拖动更改事件。如果有任何逻辑需要在拖动开始时运行,我们只能自己管理。为了帮助管理这个问题,我在模型类中添加了一个状态变量。当我们收到一个拖动变化事件时,我们首先检查该变量的状态:如果它为false,我们调用模型中的一个方法来启动拖动逻辑,然后将started变量设置为true。
在拖放事件中我们必须管理的另一件事是用户的视觉反馈。我们需要给用户一个视觉指示,表明他们的拖动请求实际上正在做一些事情。为了在拖动过程中提供这种反馈,我向组件添加了另一个状态变量,即dragLine变量。该变量存储了一个Path对象,该对象实现了从拖动的起始点到当前位置运行的一行。每次我们接收到一个拖动变化事件时,我们将用一个显示更新的线段的新路径替换dragLine。然后,当我们在Canvas主体中运行绘图代码时,我们将在那里添加代码来绘制dragLine(如果它存在)。使用dragLine状态变量还有另一个重要的功能:它为我们提供了一个将在拖动过程中更新的状态变量。对该状态变量的更改将触发组件的重组,这反过来将迫使Canvas重新绘制。在拖动过程中的画布重绘将确保用户在拖动过程中获得即时的视觉反馈。