SwiftUI框架详细解析 (十二) —— 基于SwiftUI创建Mind-Map UI(一)

版本记录

版本号 时间
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不是那么方呢?

想想像SketchOmniGraffle这样的应用程序。它们允许您在屏幕上的任意点上排列项目,并在它们之间建立连接。

在本教程中,您将学习如何使用SwiftUI创建这种类型的思维导图空间UI。您将创建一个简单的思维导图应用程序,它允许您在屏幕上放置文本框,移动它们并在它们之间创建连接。

在继续之前,请确保安装了Xcode 11.3或更高版本。

为了让您关注这个项目的SwiftUI元素,您将从描述这个图的一些现有模型代码开始。在下一节中,您将学习更多关于图论的知识。

starter文件夹中打开RazeMind项目。在项目导航器中,找到并展开名为Model的文件夹。你会看到四个Swift文件,它们为你要渲染的图形提供了一个数据源:

  • 1) Mesh.swiftmesh是模型的顶层容器。网格有一组节点和一组边。有一些与操纵网格数据相关的逻辑。您将在本教程的后面使用该逻辑。
  • 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

SwiftUI框架详细解析 (十二) —— 基于SwiftUI创建Mind-Map UI(一)_第1张图片

图表是一个连接网络。节点可以引用您选择的任何内容。

在示例项目中,您的节点是单个字符串的容器,但是您可以根据自己的需要考虑大小。

假设你是一位建筑师,正在规划一座建筑。您将从调色板中获取组件,并使用该信息生成材料清单。

SwiftUI框架详细解析 (十二) —— 基于SwiftUI创建Mind-Map UI(一)_第2张图片

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

SwiftUI框架详细解析 (十二) —— 基于SwiftUI创建Mind-Map UI(一)_第3张图片

在预览框中,您将看到两个可能的NodeView版本。

SwiftUI框架详细解析 (十二) —— 基于SwiftUI创建Mind-Map UI(一)_第4张图片

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为中心的窗口。

SwiftUI框架详细解析 (十二) —— 基于SwiftUI创建Mind-Map UI(一)_第5张图片

现在可以开始创建网格视图了。


Making a Map View

在本节中,您将结合您的NodeViewEdgeView来显示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)
      }
  }
}

检查NodeMapViewbody。您正在创建一个节点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模式。选择逻辑现在是交互式的,触摸任何一个节点都会显示一个红色边框。

SwiftUI框架详细解析 (十二) —— 基于SwiftUI创建Mind-Map UI(一)_第6张图片

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的预览。你的预览会显示两条边:

SwiftUI框架详细解析 (十二) —— 基于SwiftUI创建Mind-Map UI(一)_第7张图片

好了,激动起来吧,因为你马上就要成功了!现在,您将合并这两个层来形成完成的视图。

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的相关部分绑定到EdgeMapViewNodeMapView

找到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总是有一个根节点。

SwiftUI框架详细解析 (十二) —— 基于SwiftUI创建Mind-Map UI(一)_第8张图片

就是这样。您的核心地图视图已经完成。现在,您将开始添加一些拖动交互。


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通过dragOffsetportalPosition的组合从SurfaceView的中心偏移。MapView也有一个基本的动画,使变化看起来很漂亮,平滑如丝。

你的视图预览现在看起来是这样的:

SwiftUI框架详细解析 (十二) —— 基于SwiftUI创建Mind-Map UI(一)_第9张图片

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(_:)使用当前zoomScaletranslation进行缩放。然后,它要求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

在这里,您向包含MapViewZStack添加了一个拖动手势DragGesture。这个手势将onChangedonEnded的状态更改传递给前面添加的方法。

这是一组很复杂的代码,所以现在是处理您所创建的代码的好时机。刷新画布,按下Play按钮并进入预览模式。

4. Testing Your Code

通过启动你的拖动动作来拖动任何NodeView,通过在其他任何地方启动拖动来拖动MapView。查看橙色MapView的原点是如何变化的。视图顶部的文本将为您提供所发生事情的数字值。

SwiftUI框架详细解析 (十二) —— 基于SwiftUI创建Mind-Map UI(一)_第10张图片

您将注意到节点之间的链接没有动画。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.12.0之间。
  • 3) 使用以下两种方法来调整zoomScaleportalPosition

您只希望在修改zoomScale时修改portalPosition。因此,您将didClampclampedScale(_:initialValue:)的返回元组值中传递回processScaleChange(_:)

现在,向包含MapViewZStack添加一个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) 在第一个更改通知上存储初始的zoomScaleportalPosition。然后,将更改传递给processScaleChange(_:)
  • 2) 应用最后一次更改并将跟踪变量重置为nil

最后要做的是使用MapView上的zoomScale。在SurfaceViewbody属性中,找到注释行insert scale here later。删除注释,并将其替换为:

.scaleEffect(self.zoomScale)

最后,将预览显示调到11

SurfaceView_Previews中,找到以下行:

let mesh = Mesh.sampleMesh()

并用下面进行替换

let mesh = Mesh.sampleProceduralMesh()

这个动作为预览创建了一个更大的随机生成的网格。

刷新画布并将其置于实时预览模式。

现在,当你在屏幕上使用缩放手势时,整个橙色的MapView会在屏幕中央上下缩放。您还可以将节点拖出橙色边界。

SwiftUI框架详细解析 (十二) —— 基于SwiftUI创建Mind-Map UI(一)_第11张图片

Animating the Links

你已经看到当你拖动一个NodeView时,EdgeView不参与动画循环。为了解决这个问题,你需要给渲染系统关于如何使EdgeView动起来的信息。

打开EdgeView.swift

EdgeView是一个ShapeShape符合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

有四个值需要动画。

怎么做呢?你需要一个可动画的梨。

SwiftUI框架详细解析 (十二) —— 基于SwiftUI创建Mind-Map UI(一)_第12张图片

实际上,你需要一个AnimatablePair。因为有四个值,所以需要一对。你可以把它想象成一个没有同伴的系统。

EdgeView.swiftimport SwiftUI下添加以下类型声明:

typealias AnimatablePoint = AnimatablePair
typealias AnimatableCorners = AnimatablePair

这个声明将两个类型化对绑定到一个名称AnimatableCorners中。AnimatablePair中的类型必须符合VectorArithmeticCGPoint不符合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,在SurfaceViewbody中找到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时,您的编辑将显示在视图中。

SwiftUI框架详细解析 (十二) —— 基于SwiftUI创建Mind-Map UI(一)_第13张图片

祝贺您,您已经解决了创建可视UI的所有主要障碍。给自己一个掌声!

1. Building the application

最后一步是将您的工作放到一个正在运行的应用程序中。在项目导航器中找到文件夹Infrastructure并打开文件SceneDelegate.swift

找到行:

let contentView = BoringListView(mesh: mesh, selection: selection)

用下面内容替换该行:

let contentView = SurfaceView(mesh: mesh, selection: selection)

构建并在实际设备上运行该app

SwiftUI框架详细解析 (十二) —— 基于SwiftUI创建Mind-Map UI(一)_第14张图片

后记

本篇主要讲述了Mind-Map UI,感兴趣的给个赞或者关注~~~

SwiftUI框架详细解析 (十二) —— 基于SwiftUI创建Mind-Map UI(一)_第15张图片

你可能感兴趣的:(SwiftUI框架详细解析 (十二) —— 基于SwiftUI创建Mind-Map UI(一))