版本记录
版本号 | 时间 |
---|---|
V1.0 | 2020.03.30 星期一 |
前言
今天翻阅苹果的API文档,发现多了一个框架SwiftUI,这里我们就一起来看一下这个框架。感兴趣的看下面几篇文章。
1. SwiftUI框架详细解析 (一) —— 基本概览(一)
2. SwiftUI框架详细解析 (二) —— 基于SwiftUI的闪屏页的创建(一)
3. SwiftUI框架详细解析 (三) —— 基于SwiftUI的闪屏页的创建(二)
4. SwiftUI框架详细解析 (四) —— 使用SwiftUI进行苹果登录(一)
5. SwiftUI框架详细解析 (五) —— 使用SwiftUI进行苹果登录(二)
6. SwiftUI框架详细解析 (六) —— 基于SwiftUI的导航的实现(一)
7. SwiftUI框架详细解析 (七) —— 基于SwiftUI的导航的实现(二)
8. SwiftUI框架详细解析 (八) —— 基于SwiftUI的动画的实现(一)
9. SwiftUI框架详细解析 (九) —— 基于SwiftUI的动画的实现(二)
10. SwiftUI框架详细解析 (十) —— 基于SwiftUI构建各种自定义图表(一)
11. SwiftUI框架详细解析 (十一) —— 基于SwiftUI构建各种自定义图表(二)
开始
首先看下主要内容
在本教程中,您将了解如何在支持平移和缩放交互的
SwiftUI
中创建动画空间UI。
接着看一下写作环境
Swift 5, iOS 13, Xcode 11
SwiftUI
是对iOS或Mac开发者工具箱的完美补充,它只会随着SDK的成熟而不断改进。SwiftUI坚实的原生支持非常适合垂直列表和排列在矩形网格中的项目。
但如果UI不是那么方呢?
想想像Sketch
或OmniGraffle
这样的应用程序。它们允许您在屏幕上的任意点上排列项目,并在它们之间建立连接。
在本教程中,您将学习如何使用SwiftUI
创建这种类型的思维导图空间UI。您将创建一个简单的思维导图应用程序,它允许您在屏幕上放置文本框,移动它们并在它们之间创建连接。
在继续之前,请确保安装了Xcode 11.3
或更高版本。
为了让您关注这个项目的SwiftUI
元素,您将从描述这个图的一些现有模型代码开始。在下一节中,您将学习更多关于图论的知识。
在starter
文件夹中打开RazeMind
项目。在项目导航器中,找到并展开名为Model
的文件夹。你会看到四个Swift
文件,它们为你要渲染的图形提供了一个数据源:
- 1) Mesh.swift:
mesh
是模型的顶层容器。网格有一组节点和一组边。有一些与操纵网格数据相关的逻辑。您将在本教程的后面使用该逻辑。 - 2) Node.swift:一个节点描述网格中的一个对象、节点的位置和节点包含的文本。
- 3) Edge.swift:边描述两个节点之间的连接,并包含对它们的引用。
- 4) SelectionHandler.swift:这个帮助类充当视图选择状态的持久内存。有一些与节点的选择和编辑相关的逻辑,稍后您将使用它们。
请随意浏览starter项目代码。简而言之,starter
代码提供了对某些对象集的托管访问。你现在不需要完全理解它。
Understanding Graph Theory
图Graphs
是对图中节点之间的成对关系进行建模的数学结构mathematical structures。两个节点之间的连接称为边。
图可以是有向的,也可以是无向的。有向图表示边的两个端点(A和B)之间的方向。A -> B != B -> A
。无向图对端点的方向没有任何意义,所以A -> B == B -> A
。
图表是一个连接网络。节点可以引用您选择的任何内容。
在示例项目中,您的节点是单个字符串的容器,但是您可以根据自己的需要考虑大小。
假设你是一位建筑师,正在规划一座建筑。您将从调色板中获取组件,并使用该信息生成材料清单。
Designing Your UI
在本教程中,您将构建一个无限的2D平面。您将能够拖放表面并放大或缩小以查看更多或更少的内容。
当你创建自己的应用程序时,你需要决定如何操作你的界面。您几乎可以做任何事情,但是要记住考虑常用模式common-use patterns和可访问性。如果你的界面太奇怪或太复杂,你的用户会发现很难使用。
下面是你要执行的规则:
- 通过拖动更改节点的位置。
- 点击一个节点来选择它。
- 拖动和放大屏幕,因为它就像一个无限的表面。
- 通过拖动表面来平移表面。
- 使用缩放手势放大和缩小。
现在,是时候实现这些特性了。首先构建一些简单的视图。
Building the View Primitives
您希望在表面上显示两个东西:节点和边缘。首先要做的是为这两种类型创建SwiftUI
视图。
1. Creating a Node View
首先创建一个新文件。在“项目导航器”中,选择View Stack
文件夹,然后按Command-N
键添加新文件。回到iOS ▸ Swift UI View
,点击Next
。将文件命名为NodeView.swift
并检查是否选择了目标RazeMind
。最后,单击Create
。
在NodeView
中,添加以下变量:
static let width = CGFloat(100)
// 1
@State var node: Node
//2
@ObservedObject var selection: SelectionHandler
//3
var isSelected: Bool {
return selection.isNodeSelected(node)
}
- 1) 传递要显示的节点。
- 2)
@ObservedObject
告诉你selection
是通过引用传递给NodeView
的,因为它对AnyObject
都有要求。 - 3) 计算属性
isSelected
保持视图主体内的整洁。
现在,找到NodeView_Previews
实现,并将previews
属性的主体替换为:
let selection1 = SelectionHandler()
let node1 = Node(text: "hello world")
let selection2 = SelectionHandler()
let node2 = Node(text: "I'm selected, look at me")
selection2.selectNode(node2)
return VStack {
NodeView(node: node1, selection: selection1)
NodeView(node: node2, selection: selection2)
}
在这里,使用SelectionHandler
的两个不同实例实例化两个节点。这为您提供了视图在您选择它时的预览。
返回到NodeView
,并使用以下实现替换body
属性:
Ellipse()
.fill(Color.green)
.overlay(Ellipse()
.stroke(isSelected ? Color.red : Color.black, lineWidth: isSelected ? 5 : 3))
.overlay(Text(node.text)
.multilineTextAlignment(.center)
.padding(EdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 8)))
.frame(width: NodeView.width, height: NodeView.width, alignment: .center)
这将产生一个带有边框和文本的绿色椭圆。没有什么特别的,但是可以开始工作了。
在目标选择器中选择模拟器iPhone 11 Pro
。这个选项控制了SwiftUI
画布显示预览效果的方式。
使用位于编辑器右上角的Adjust Editor Options - canvas
或按Option-Command-Return
打开SwiftUI canvas
。
在预览框中,您将看到两个可能的NodeView
版本。
2. Creating an Edge Shape
现在,您已经为下一个任务做好了准备,即创建edge
的视图。边是连接两个节点的线。
在“项目导航器”中,选择View Stack
。然后,创建一个新的SwiftUI
视图文件。将文件命名为EdgeView.swift
。
Xcode创建了一个名为EdgeView
的模板视图,但是您希望EdgeView
是一个Shape
。因此,替换类型的声明:
struct EdgeView: View {
为
struct EdgeView: Shape {
删除模板的body
。现在,您有了一个内部没有代码的结构体。
要定义形状,请将此代码添加到EdgeView
中。
var startx: CGFloat = 0
var starty: CGFloat = 0
var endx: CGFloat = 0
var endy: CGFloat = 0
// 1
init(edge: EdgeProxy) {
// 2
startx = edge.start.x
starty = edge.start.y
endx = edge.end.x
endy = edge.end.y
}
// 3
func path(in rect: CGRect) -> Path {
var linkPath = Path()
linkPath.move(to: CGPoint(x: startx, y: starty)
.alignCenterInParent(rect.size))
linkPath.addLine(to: CGPoint(x: endx, y:endy)
.alignCenterInParent(rect.size))
return linkPath
}
查看EdgeView
的代码:
- 1) 使用
EdgeProxy
实例初始化形状,而不是Edge
,因为Edge
不知道它引用的节点实例的任何信息。当模型改变时,Mesh
会重新构建EdgeProxy
对象的列表。 - 2) 您将两个端点
cgpoint
拆分为四个CGFloat
属性。这一点在本教程后面添加动画时将变得非常重要。 - 3) 在
path(in:)
中绘制是一条从开始到结束的简单直线。对助手alignCenterInParent(_:)
的调用将线条的原点从顶部前缘移动到视图矩形的中心。
在EdgeView
下面找到EdgeView_Previews
,并使用此代码替换previews
的默认实现。
let edge1 = EdgeProxy(
id: UUID(),
start: CGPoint(x: -100, y: -100),
end: CGPoint(x: 100, y: 100))
let edge2 = EdgeProxy(
id: UUID(),
start: CGPoint(x: 100, y: -100),
end: CGPoint(x: -100, y: 100))
return ZStack {
EdgeView(edge: edge1).stroke(lineWidth: 4)
EdgeView(edge: edge2).stroke(Color.blue, lineWidth: 2)
}
刷新预览。你会在模拟器窗口中看到一个以X
为中心的窗口。
现在可以开始创建网格视图了。
Making a Map View
在本节中,您将结合您的NodeView
和EdgeView
来显示Mesh
的可视化描述。
1. Creating the Nodes’ Layer
对于第一个任务,您将构建绘制节点的层。您可以将节点和边缘隐藏到一个视图中,但是构建层更整洁,并提供更好的模块化和数据隔离。
在“项目导航器”中,选择View Stack
文件夹。然后,创建一个新的SwiftUI
视图文件。将文件命名为NodeMapView.swift
。
找到NodeMapView
,并将这两个属性添加到它:
@ObservedObject var selection: SelectionHandler
@Binding var nodes: [Node]
在节点nodes
上使用@Binding
告诉SwiftUI
另一个对象将拥有节点集合并将其传递给NodeMapView
。
接下来,将body
的模板实现替换为:
ZStack {
ForEach(nodes, id: \.visualID) { node in
NodeView(node: node, selection: self.selection)
.offset(x: node.position.x, y: node.position.y)
.onTapGesture {
self.selection.selectNode(node)
}
}
}
检查NodeMapView
的body
。您正在创建一个节点ZStack
,并对每个节点应用一个offset
来定位节点在表面上的位置。当您点击它时,每个节点还将获得一个要执行的操作。
最后,找到NodeMapView_Previews
并将这些属性添加到它:
static let node1 = Node(position: CGPoint(x: -100, y: -30), text: "hello")
static let node2 = Node(position: CGPoint(x: 100, y: 30), text: "world")
@State static var nodes = [node1, node2]
并将previews
的实现替换为:
let selection = SelectionHandler()
return NodeMapView(selection: selection, nodes: $nodes)
请注意如何使用$nodes
语法来传递Binding
类型。将模拟节点数组放在previews
之外作为@State
允许您创建此绑定。
刷新画布,您将看到两个并排的节点。通过按下Play
按钮,将画布置于实时预览Live Preview
模式。选择逻辑现在是交互式的,触摸任何一个节点都会显示一个红色边框。
2. Creating the Edges’ Layer
现在,您将创建一个层来显示所有的边。
在“项目导航器”中,选择View Stack
文件夹。然后,创建一个新的SwiftUI
视图文件。将文件命名为EdgeMapView.swift
。
添加此属性到EdgeMapView
:
@Binding var edges: [EdgeProxy]
使用下面替换body
:
ZStack {
ForEach(edges) { edge in
EdgeView(edge: edge)
.stroke(Color.black, lineWidth: 3.0)
}
}
注意,数组中的每条边都有一个黑色边框。
将这些属性添加到EdgeMapView_Previews
:
static let proxy1 = EdgeProxy(
id: EdgeID(),
start: .zero,
end: CGPoint(x: -100, y: 30))
static let proxy2 = EdgeProxy(
id: EdgeID(),
start: .zero,
end: CGPoint(x: 100, y: 30))
@State static var edges = [proxy1, proxy2]
使用下面替换previews
这一行:
EdgeMapView(edges: $edges)
同样,您创建一个@State
属性来将模拟数据传递给EdgeMapView
的预览。你的预览会显示两条边:
好了,激动起来吧,因为你马上就要成功了!现在,您将合并这两个层来形成完成的视图。
3. Creating the MapView
你要把一层放在另一层的上面来创建完成的视图。
在项目导航器中,选择View Stack
并创建一个新的SwiftUI
视图文件。将文件命名为MapView.swift
。
添加这两个属性到MapView
:
@ObservedObject var selection: SelectionHandler
@ObservedObject var mesh: Mesh
同样,这里有对SelectionHandler
的引用。您第一次将Mesh
实例引入到视图系统中。
将body
实现替换为以下内容:
ZStack {
Rectangle().fill(Color.orange)
EdgeMapView(edges: $mesh.links)
NodeMapView(selection: selection, nodes: $mesh.nodes)
}
最后把所有不同的视图放在一起。首先是一个橙色的矩形,将边缘堆叠在上面,最后是节点堆叠。橙色的矩形帮助你看到你的视图发生了什么。
请注意,如何使用$
注释将mesh
的相关部分绑定到EdgeMapView
和NodeMapView
。
找到MapView_Previews
,并将previews
中的代码替换为以下实现:
let mesh = Mesh()
let child1 = Node(position: CGPoint(x: 100, y: 200), text: "child 1")
let child2 = Node(position: CGPoint(x: -100, y: 200), text: "child 2")
[child1, child2].forEach {
mesh.addNode($0)
mesh.connect(mesh.rootNode(), to: $0)
}
mesh.connect(child1, to: child2)
let selection = SelectionHandler()
return MapView(selection: selection, mesh: mesh)
创建两个节点并将它们添加到一个Mesh
中。然后,在节点之间创建边缘。单击preview
窗格中的Resume
,您的画布现在应该显示三个节点,它们之间有链接。
在RazeMind
的特殊情况下,Mesh
总是有一个根节点。
就是这样。您的核心地图视图已经完成。现在,您将开始添加一些拖动交互。
Dragging Nodes
在本节中,您将添加拖动手势,以便可以在屏幕上移动您的节点NodeView
。您还将添加平移MapView
的功能。
在“项目导航器”中,选择View Stack
。然后创建一个新的SwiftUI
视图文件,并将其命名为SurfaceView.swift
。
在SurfaceView
内部,添加以下属性:
@ObservedObject var mesh: Mesh
@ObservedObject var selection: SelectionHandler
//dragging
@State var portalPosition: CGPoint = .zero
@State var dragOffset: CGSize = .zero
@State var isDragging: Bool = false
@State var isDraggingMesh: Bool = false
//zooming
@State var zoomScale: CGFloat = 1.0
@State var initialZoomScale: CGFloat?
@State var initialPortalPosition: CGPoint?
找到SurfaceView_Previews
,将previews
的实现替换为:
let mesh = Mesh.sampleMesh()
let selection = SelectionHandler()
return SurfaceView(mesh: mesh, selection: selection)
添加到SurfaceView
的@State
变量会跟踪将要创建的拖动和放大手势。
注意,本节只处理拖动。在下一节中,您将处理缩放MapView
的问题。但是在使用DragGesture
设置拖动操作之前,需要添加一些基础设施。
1. Getting Ready to Drag
在SurfaceView_Previews
中,实例化一个预制网格并将其分配给SurfaceView
。
用以下代码替换SurfaceView
内部的body
实现:
VStack {
// 1
Text("drag offset = w:\(dragOffset.width), h:\(dragOffset.height)")
Text("portal offset = x:\(portalPosition.x), y:\(portalPosition.y)")
Text("zoom = \(zoomScale)")
//<-- insert TextField here
// 2
GeometryReader { geometry in
// 3
ZStack {
Rectangle().fill(Color.yellow)
MapView(selection: self.selection, mesh: self.mesh)
//<-- insert scale here later
// 4
.offset(
x: self.portalPosition.x + self.dragOffset.width,
y: self.portalPosition.y + self.dragOffset.height)
.animation(.easeIn)
}
//<-- add drag gesture later
}
}
在这里,您已经创建了一个包含四个视图的VStack
。
- 1) 您有三个文本
Text
元素,它们显示关于状态的一些信息。 - 2)
GeometryReader
提供有关包含的VStack
大小的信息。 - 3) 在
GeometryReader
中,您有一个ZStack
,它包含一个黄色背景和一个MapView
。 - 4)
MapView
通过dragOffset
和portalPosition
的组合从SurfaceView
的中心偏移。MapView
也有一个基本的动画,使变化看起来很漂亮,平滑如丝。
你的视图预览现在看起来是这样的:
2. Handling Changes to the Drag State
现在,您需要一些帮助来处理对拖动状态的更改。将此扩展添加到SurfaceView.swift
的末尾。
private extension SurfaceView {
// 1
func distance(from pointA: CGPoint, to pointB: CGPoint) -> CGFloat {
let xdelta = pow(pointA.x - pointB.x, 2)
let ydelta = pow(pointA.y - pointB.y, 2)
return sqrt(xdelta + ydelta)
}
// 2
func hitTest(point: CGPoint, parent: CGSize) -> Node? {
for node in mesh.nodes {
let endPoint = node.position
.scaledFrom(zoomScale)
.alignCenterInParent(parent)
.translatedBy(x: portalPosition.x, y: portalPosition.y)
let dist = distance(from: point, to: endPoint) / zoomScale
//3
if dist < NodeView.width / 2.0 {
return node
}
}
return nil
}
// 4
func processNodeTranslation(_ translation: CGSize) {
guard !selection.draggingNodes.isEmpty else { return }
let scaledTranslation = translation.scaledDownTo(zoomScale)
mesh.processNodeTranslation(
scaledTranslation,
nodes: selection.draggingNodes)
}
}
这个扩展提供了一些低级的帮助方法,用于询问有关拖动操作的问题。
- 1) 帮助方法中
distance(from:to:)
是毕达哥拉斯定理的一个实现。它计算两点之间的距离。 - 2) 在
hitTest(point:parent:)
中,您将SurfaceView
的引用系统中的一个点转换为MapView
的引用系统。转换使用当前zoomScale
,SurfaceView
的大小和MapView
的当前偏移量。 - 3) 如果节点位置与输入点之间的
position
小于节点NodeView
半径,则接触点在节点内。 - 4)
processNodeTranslation(_:)
使用当前zoomScale
对translation
进行缩放。然后,它要求Mesh
使用SelectionHandler
中的信息移动节点。
开始你的工作方式的处理堆栈,并将这些方法内相同的扩展:
func processDragChange(_ value: DragGesture.Value, containerSize: CGSize) {
// 1
if !isDragging {
isDragging = true
if let node = hitTest(
point: value.startLocation,
parent: containerSize
) {
isDraggingMesh = false
selection.selectNode(node)
// 2
selection.startDragging(mesh)
} else {
isDraggingMesh = true
}
}
// 3
if isDraggingMesh {
dragOffset = value.translation
} else {
processNodeTranslation(value.translation)
}
}
// 4
func processDragEnd(_ value: DragGesture.Value) {
isDragging = false
dragOffset = .zero
if isDraggingMesh {
portalPosition = CGPoint(
x: portalPosition.x + value.translation.width,
y: portalPosition.y + value.translation.height)
} else {
processNodeTranslation(value.translation)
selection.stopDragging(mesh)
}
}
这些方法完成了将拖动操作转换为网格Mesh
中的更改的工作。
- 1) 在
processDragChange(_:containerSize:)
中,您将确定这是否是收到的第一个更改通知。在这个视图中有两种可能的拖动操作:您可以拖动一个NodeView
,或者您可以拖动整个MapView
,改变MapView
的哪一部分将会显示。您可以使用hitTest(point:parent:)
来确定哪个操作是合适的。 - 2) 如果您拖动一个节点,您将要求
SelectionHandler
启动对所选节点的拖动操作。SelectionHandler
存储对节点的引用和节点的初始位置。 - 3) 如果平移
MapView
-或将translation
传递给processNodeTranslation(_:)
,则将拖动 -translation
值应用于dragOffset
。 - 4)
processDragEnd(_:)
获取最终的translation
值,并将该值应用于已拖动的节点或已平移的映射。然后为下一次重置跟踪属性。
添加拖动手势
现在可以向SurfaceView添加拖动手势。在body中,稍后查找添加拖动手势的注释行。删除行,并添加这个修改:
3. Adding a Drag Gesture
现在可以向SurfaceView
添加DragGesture
。在body
中,查找add drag gesture later
的注释行。删除行,并添加这个修改:
.gesture(DragGesture()
.onChanged { value in
self.processDragChange(value, containerSize: geometry.size)
}
.onEnded { value in
self.processDragEnd(value)
})
//<-- add magnification gesture later
在这里,您向包含MapView
的ZStack
添加了一个拖动手势DragGesture
。这个手势将onChanged
和onEnded
的状态更改传递给前面添加的方法。
这是一组很复杂的代码,所以现在是处理您所创建的代码的好时机。刷新画布,按下Play
按钮并进入预览模式。
4. Testing Your Code
通过启动你的拖动动作来拖动任何NodeView
,通过在其他任何地方启动拖动来拖动MapView
。查看橙色MapView
的原点是如何变化的。视图顶部的文本将为您提供所发生事情的数字值。
您将注意到节点之间的链接没有动画。NodeView
实例是有动画效果的,但是EdgeView
没有。你很快就会修好的。
Scaling the MapView
在前一节中,您为放大做了大量的基础工作。拖动帮助函数已经使用了zoomScale
的值。所以,剩下的就是添加一个MagnificationGesture
来操作zoomScale
并将缩放应用到MapView
。
首先,将以下方法添加到之前的私有SurfaceView
扩展中:
// 1
func scaledOffset(_ scale: CGFloat, initialValue: CGPoint) -> CGPoint {
let newx = initialValue.x*scale
let newy = initialValue.y*scale
return CGPoint(x: newx, y: newy)
}
func clampedScale(_ scale: CGFloat, initialValue: CGFloat?)
-> (scale: CGFloat, didClamp: Bool) {
let minScale: CGFloat = 0.1
let maxScale: CGFloat = 2.0
let raw = scale.magnitude * (initialValue ?? maxScale)
let value = max(minScale, min(maxScale, raw))
let didClamp = raw != value
return (value, didClamp)
}
func processScaleChange(_ value: CGFloat) {
let clamped = clampedScale(value, initialValue: initialZoomScale)
zoomScale = clamped.scale
if !clamped.didClamp,
let point = initialPortalPosition {
portalPosition = scaledOffset(value, initialValue: point)
}
}
- 1) 缩放
CGPoint
值。 - 2) 确保计算的刻度在
0.1
到2.0
之间。 - 3) 使用以下两种方法来调整
zoomScale
和portalPosition
。
您只希望在修改zoomScale
时修改portalPosition
。因此,您将didClamp
在clampedScale(_:initialValue:)
的返回元组值中传递回processScaleChange(_:)
。
现在,向包含MapView
的ZStack
添加一个MagnificationGesture
。找到标记注释行add magnification gesture later
。删除注释,并将其替换为:
.gesture(MagnificationGesture()
.onChanged { value in
// 1
if self.initialZoomScale == nil {
self.initialZoomScale = self.zoomScale
self.initialPortalPosition = self.portalPosition
}
self.processScaleChange(value)
}
.onEnded { value in
// 2
self.processScaleChange(value)
self.initialZoomScale = nil
self.initialPortalPosition = nil
})
这段代码是这样的:
- 1) 在第一个更改通知上存储初始的
zoomScale
和portalPosition
。然后,将更改传递给processScaleChange(_:)
。 - 2) 应用最后一次更改并将跟踪变量重置为
nil
。
最后要做的是使用MapView
上的zoomScale
。在SurfaceView
的body
属性中,找到注释行insert scale here later
。删除注释,并将其替换为:
.scaleEffect(self.zoomScale)
最后,将预览显示调到11
。
在SurfaceView_Previews
中,找到以下行:
let mesh = Mesh.sampleMesh()
并用下面进行替换
let mesh = Mesh.sampleProceduralMesh()
这个动作为预览创建了一个更大的随机生成的网格。
刷新画布并将其置于实时预览模式。
现在,当你在屏幕上使用缩放手势时,整个橙色的MapView
会在屏幕中央上下缩放。您还可以将节点拖出橙色边界。
Animating the Links
你已经看到当你拖动一个NodeView
时,EdgeView
不参与动画循环。为了解决这个问题,你需要给渲染系统关于如何使EdgeView
动起来的信息。
打开EdgeView.swift
EdgeView
是一个Shape
,Shape
符合Animatable
协议。动画的声明如下:
/// A type that can be animated
public protocol Animatable {
/// The type defining the data to be animated.
associatedtype AnimatableData : VectorArithmetic
/// The data to be animated.
var animatableData: Self.AnimatableData { get set }
}
您需要在属性animatableData
中提供一个符合vectoralgorithms
的值。
1. Using Animatable Pairs
有四个值需要动画。
怎么做呢?你需要一个可动画的梨。
实际上,你需要一个AnimatablePair
。因为有四个值,所以需要一对。你可以把它想象成一个没有同伴的系统。
在EdgeView.swift
中import SwiftUI
下添加以下类型声明:
typealias AnimatablePoint = AnimatablePair
typealias AnimatableCorners = AnimatablePair
这个声明将两个类型化对绑定到一个名称AnimatableCorners
中。AnimatablePair
中的类型必须符合VectorArithmetic
。CGPoint
不符合VectorArithmetic
,这就是为什么要将两个端点拆分为它们的CGFloat
组件。
在EdgeView
中,将这段代码添加到结构体的末尾:
var animatableData: AnimatableCorners {
get {
return AnimatablePair(
AnimatablePair(startx, starty),
AnimatablePair(endx, endy))
}
set {
startx = newValue.first.first
starty = newValue.first.second
endx = newValue.second.first
endy = newValue.second.second
}
}
在这里,将animatableData
定义为AnimatableCorners
的一个实例,并构造嵌套对。SwiftUI
现在知道如何动画EdgeView
了。
打开SurfaceView.swift
。快速刷新画布。现在尝试拖动节点,您将看到链接与节点同步活动。
你现在有一个工作的2D
无限表面渲染器,处理缩放和平移!
注意:
SwiftUI
是一个声明式API——你可以描述你想要绘制的内容,然后由框架在设备上绘制。不像UIKit
,你不需要担心视图和层的内存管理或者队列和退出队列视图。
Editing the View
到目前为止,您已经使用了预定义的模型,但是一个有效的UI应该允许用户编辑模型。您已经展示了可以编辑节点的位置,但是文本呢?
在本节中,您将向界面添加一个TextField
。
还在SurfaceView.swift
,在SurfaceView
的body
中找到insert TextField here
,并添加这段代码来定义一个编辑字段:
TextField("Breathe…", text: $selection.editingText, onCommit: {
if let node = self.selection.onlySelectedNode(in: self.mesh) {
self.mesh.updateNodeText(node, string: self.self.selection.editingText)
}
})
同样,SelectionHandler
充当视图的持久内存。将editingText
绑定到TextField
。
刷新画布并启动实时预览模式。编辑一个节点,点击它,然后编辑窗口顶部的TextField
。当您按下Return
时,您的编辑将显示在视图中。
祝贺您,您已经解决了创建可视UI的所有主要障碍。给自己一个掌声!
1. Building the application
最后一步是将您的工作放到一个正在运行的应用程序中。在项目导航器中找到文件夹Infrastructure
并打开文件SceneDelegate.swift
。
找到行:
let contentView = BoringListView(mesh: mesh, selection: selection)
用下面内容替换该行:
let contentView = SurfaceView(mesh: mesh, selection: selection)
构建并在实际设备上运行该app
后记
本篇主要讲述了
Mind-Map UI
,感兴趣的给个赞或者关注~~~