SwiftUI框架詳細(xì)解析 (十三) —— 基于SwiftUI創(chuàng)建Mind-Map UI(二)

版本記錄

版本號 時(shí)間
V1.0 2020.03.30 星期一

前言

今天翻閱蘋果的API文檔,發(fā)現(xiàn)多了一個(gè)框架SwiftUI,這里我們就一起來看一下這個(gè)框架。感興趣的看下面幾篇文章。
1. SwiftUI框架詳細(xì)解析 (一) —— 基本概覽(一)
2. SwiftUI框架詳細(xì)解析 (二) —— 基于SwiftUI的閃屏頁的創(chuàng)建(一)
3. SwiftUI框架詳細(xì)解析 (三) —— 基于SwiftUI的閃屏頁的創(chuàng)建(二)
4. SwiftUI框架詳細(xì)解析 (四) —— 使用SwiftUI進(jìn)行蘋果登錄(一)
5. SwiftUI框架詳細(xì)解析 (五) —— 使用SwiftUI進(jìn)行蘋果登錄(二)
6. SwiftUI框架詳細(xì)解析 (六) —— 基于SwiftUI的導(dǎo)航的實(shí)現(xiàn)(一)
7. SwiftUI框架詳細(xì)解析 (七) —— 基于SwiftUI的導(dǎo)航的實(shí)現(xiàn)(二)
8. SwiftUI框架詳細(xì)解析 (八) —— 基于SwiftUI的動畫的實(shí)現(xiàn)(一)
9. SwiftUI框架詳細(xì)解析 (九) —— 基于SwiftUI的動畫的實(shí)現(xiàn)(二)
10. SwiftUI框架詳細(xì)解析 (十) —— 基于SwiftUI構(gòu)建各種自定義圖表(一)
11. SwiftUI框架詳細(xì)解析 (十一) —— 基于SwiftUI構(gòu)建各種自定義圖表(二)
12. SwiftUI框架詳細(xì)解析 (十二) —— 基于SwiftUI創(chuàng)建Mind-Map UI(一)

源碼

1. Swift

首先看下工程組織結(jié)構(gòu)

下面就是源碼了

1. AppDelegate.swift
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    return true
  }

  // MARK: UISceneSession Lifecycle
  func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
    return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
  }
}
2. SceneDelegate.swift
import UIKit
import SwiftUI

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
  var window: UIWindow?

  @Published var mesh = Mesh.sampleMesh()
  @Published var selection = SelectionHandler()

  func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    let contentView = SurfaceView(mesh: mesh, selection: selection)

    // Use a UIHostingController as window root view controller.
    if let windowScene = scene as? UIWindowScene {
      let window = UIWindow(windowScene: windowScene)
      window.rootViewController = UIHostingController(rootView: contentView)
      self.window = window
      window.makeKeyAndVisible()
    }
  }
}
3. CGPoint+Help.swift
import CoreGraphics

extension CGPoint {
  func translatedBy(x: CGFloat, y: CGFloat) -> CGPoint {
    return CGPoint(x: self.x + x, y: self.y + y)
  }
}

extension CGPoint {
  func alignCenterInParent(_ parent: CGSize) -> CGPoint {
    let x = parent.width/2 + self.x
    let y = parent.height/2 + self.y
    return CGPoint(x: x, y: y)
  }
  
  func scaledFrom(_ factor: CGFloat) -> CGPoint {
    return CGPoint(
      x: self.x * factor,
      y: self.y * factor)
  }
}

extension CGSize {
  func scaledDownTo(_ factor: CGFloat) -> CGSize {
    return CGSize(width: width/factor, height: height/factor)
  }
  
  var length: CGFloat {
    return sqrt(pow(width, 2) + pow(height, 2))
  }
  
  var inverted: CGSize {
    return CGSize(width: -width, height: -height)
  }
}
4. Edge.swift
import Foundation
import CoreGraphics

typealias EdgeID = UUID

struct Edge: Identifiable {
  var id = EdgeID()
  var start: NodeID
  var end: NodeID
}

struct EdgeProxy: Identifiable {
  var id: EdgeID
  var start: CGPoint
  var end: CGPoint
}

extension Edge {
  static func == (lhs: Edge, rhs: Edge) -> Bool {
    return lhs.start == rhs.start && lhs.end == rhs.end
  }
}
5. Mesh.swift
import Foundation
import CoreGraphics

class Mesh: ObservableObject {
  let rootNodeID: NodeID
  @Published var nodes: [Node] = []
  @Published var editingText: String
  
  init() {
    self.editingText = ""
    let root = Node(text: "root")
    rootNodeID = root.id
    addNode(root)
  }
  
  var edges: [Edge] = [] {
    didSet {
      rebuildLinks()
    }
  }
  @Published var links: [EdgeProxy] = []
  
  func rebuildLinks() {
    links = edges.compactMap { edge in
      let snode = nodes.filter({ $0.id == edge.start }).first
      let enode = nodes.filter({ $0.id == edge.end }).first
      if let snode = snode, let enode = enode {
        return EdgeProxy(id: edge.id, start: snode.position, end: enode.position)
      }
      return nil
    }
  }
  
  func rootNode() -> Node {
    guard let root = nodes.filter({ $0.id == rootNodeID }).first else {
      fatalError("mesh is invalid - no root")
    }
    return root
  }
  
  func nodeWithID(_ nodeID: NodeID) -> Node? {
    return nodes.filter({ $0.id == nodeID }).first
  }
  
  func replace(_ node: Node, with replacement: Node) {
    var newSet = nodes.filter { $0.id != node.id }
    newSet.append(replacement)
    nodes = newSet
  }
}

extension Mesh {
  func updateNodeText(_ srcNode: Node, string: String) {
    var newNode = srcNode
    newNode.text = string
    replace(srcNode, with: newNode)
  }
  
  func positionNode(_ node: Node, position: CGPoint) {
    var movedNode = node
    movedNode.position = position
    replace(node, with: movedNode)
    rebuildLinks()
  }
  
  func processNodeTranslation(_ translation: CGSize, nodes: [DragInfo]) {
    nodes.forEach { draginfo in
      if let node = nodeWithID(draginfo.id) {
        let nextPosition = draginfo.originalPosition.translatedBy(x: translation.width, y: translation.height)
        self.positionNode(node, position: nextPosition)
      }
    }
  }
}

extension Mesh {
  func addNode(_ node: Node) {
    nodes.append(node)
  }
  
  func connect(_ parent: Node, to child: Node) {
    let newedge = Edge(start: parent.id, end: child.id)
    let exists = edges.contains(where: { edge in
      return newedge == edge
    })
    
    guard exists == false else {
      return
    }
    
    edges.append(newedge)
  }
}

extension Mesh {
  @discardableResult func addChild(_ parent: Node, at point: CGPoint? = nil) -> Node {
    let target = point ?? parent.position
    let child = Node(position: target, text: "child")
    addNode(child)
    connect(parent, to: child)
    rebuildLinks()
    return child
  }
  
  @discardableResult func addSibling(_ node: Node) -> Node? {
    guard node.id != rootNodeID else {
      return nil
    }
    
    let parentedges = edges.filter({ $0.end == node.id })
    if
      let parentedge = parentedges.first,
      let parentnode = nodeWithID(parentedge.start) {
      let sibling = addChild(parentnode)
      return sibling
    }
    return nil
  }
  
  func deleteNodes(_ nodesToDelete: [NodeID]) {
    for id in nodesToDelete where id != rootNodeID {
      if let delete = nodes.firstIndex(where: { $0.id == id }) {
        nodes.remove(at: delete)
        let remainingEdges = edges.filter({ $0.end != id && $0.start != id })
        edges = remainingEdges
      }
    }
    rebuildLinks()
  }
  
  func deleteNodes(_ nodesToDelete: [Node]) {
    deleteNodes(nodesToDelete.map({ $0.id }))
  }
}

extension Mesh {
  func locateParent(_ node: Node) -> Node? {
    let parentedges = edges.filter { $0.end == node.id }
    if let parentedge = parentedges.first,
      let parentnode = nodeWithID(parentedge.start) {
      return parentnode
    }
    return nil
  }
  
  func distanceFromRoot(_ node: Node, distance: Int = 0) -> Int? {
    if node.id == rootNodeID { return distance }
    
    if let ancestor = locateParent(node) {
      if ancestor.id == rootNodeID {
        return distance + 1
      } else {
        return distanceFromRoot(ancestor, distance: distance + 1)
      }
    }
    return nil
  }
}
6. Mesh+Demo.swift
import Foundation
import CoreGraphics

extension Mesh {
  static func sampleMesh() -> Mesh {
    let mesh = Mesh()
    mesh.updateNodeText(mesh.rootNode(), string: "every human has a right to")
    [(0, "shelter"),
     (120, "food"),
     (240, "education")].forEach { (angle, name) in
      let point = mesh.pointWithCenter(center: .zero, radius: 200, angle: angle.radians)
      let node = mesh.addChild(mesh.rootNode(), at: point)
      mesh.updateNodeText(node, string: name)
    }
    return mesh
  }

  static func sampleProceduralMesh() -> Mesh {
    let mesh = Mesh()
    //seed root node with 3 children
    [0, 1, 2, 3].forEach { index in
      let point = mesh.pointWithCenter(center: .zero, radius: 400, angle: (index * 90 + 30).radians)
      let node = mesh.addChild(mesh.rootNode(), at: point)
      mesh.updateNodeText(node, string: "A\(index + 1)")
      mesh.addChildrenRecursive(to: node, distance: 200, generation: 1)
    }
    return mesh
  }

  func addChildrenRecursive(to node: Node, distance: CGFloat, generation: Int) {
    let labels = ["A", "B", "C", "D", "E", "F"]
    guard generation < labels.count else {
      return
    }

    let childCount = Int.random(in: 1..<4)
    var count = 0
    while count < childCount {
      count += 1
      let position = positionForNewChild(node, length: distance)
      let child = addChild(node, at: position)
      updateNodeText(child, string: "\(labels[generation])\(count + 1)")
      addChildrenRecursive(to: child, distance: distance + 200.0, generation: generation + 1)
    }
  }
}

extension Int {
  var radians: CGFloat {
    CGFloat(self) * CGFloat.pi/180.0
  }
}
7. Mesh+MathHelp.swift
import Foundation
import CoreGraphics

extension Mesh {
  public func positionForNewChild(_ parent: Node, length: CGFloat) -> CGPoint {
    let childEdges = edges.filter { $0.start == parent.id }
    if let grandparentedge = edges.filter({ $0.end == parent.id }).first, let grandparent = nodeWithID(grandparentedge.start) {
      let baseAngle = angleFrom(start: grandparent.position, end: parent.position)
      let childBasedAngle = positionForChildAtIndex(childEdges.count, baseAngle: baseAngle)
      let newpoint = pointWithCenter(center: parent.position, radius: length, angle: childBasedAngle)
      return newpoint
    }
    return CGPoint(x: 200, y: 200)
  }
  
  /// get angle for n'th child in order delta * 0,1,-1,2,-2
  func positionForChildAtIndex(_ index: Int, baseAngle: CGFloat) -> CGFloat {
    let jitter = CGFloat.random(in: CGFloat(-1.0)...CGFloat(1.0)) * CGFloat.pi/32.0
    guard index > 0 else { return baseAngle + jitter }

    let level = (index + 1)/2
    let polarity: CGFloat = index % 2 == 0 ? -1.0:1.0

    let delta = CGFloat.pi/6.0 + jitter
    return baseAngle + polarity * delta * CGFloat(level)
  }

  /// angle in radians
  func pointWithCenter(center: CGPoint, radius: CGFloat, angle: CGFloat) -> CGPoint {
    let deltax = radius*cos(angle)
    let deltay = radius*sin(angle)
    let newpoint = CGPoint(x: center.x + deltax, y: center.y + deltay)
    return newpoint
  }

  func angleFrom(start: CGPoint, end: CGPoint) -> CGFloat {
    var deltax = end.x - start.x
    let deltay = end.y - start.y
    if abs(deltax) < 0.001 {
      deltax = 0.001
    }
    let  angle = atan(deltay/abs(deltax))
    return deltax > 0 ? angle: CGFloat.pi - angle
  }
}
8. Node.swift
import Foundation
import CoreGraphics

typealias NodeID = UUID

struct Node: Identifiable {
  var id: NodeID = NodeID()
  var position: CGPoint = .zero
  var text: String = ""

  var visualID: String {
    return id.uuidString
      + "\(text.hashValue)"
  }
}

extension Node {
  static func == (lhs: Node, rhs: Node) -> Bool {
    return lhs.id == rhs.id
  }
}
9. SelectionHandler.swift
import Foundation
import CoreGraphics

struct DragInfo {
  var id: NodeID
  var originalPosition: CGPoint
}

class SelectionHandler: ObservableObject {
  @Published var draggingNodes: [DragInfo] = []
  @Published private(set) var selectedNodeIDs: [NodeID] = []
  
  @Published var editingText: String = ""
  
  func selectNode(_ node: Node) {
    selectedNodeIDs = [node.id]
    editingText = node.text
  }
  
  func isNodeSelected(_ node: Node) -> Bool {
    return selectedNodeIDs.contains(node.id)
  }
  
  func selectedNodes(in mesh: Mesh) -> [Node] {
    return selectedNodeIDs.compactMap { mesh.nodeWithID($0) }
  }
  
  func onlySelectedNode(in mesh: Mesh) -> Node? {
    let selectedNodes = self.selectedNodes(in: mesh)
    if selectedNodes.count == 1 {
      return selectedNodes.first
    }
    return nil
  }
  
  func startDragging(_ mesh: Mesh) {
    draggingNodes = selectedNodes(in: mesh)
      .map { DragInfo(id: $0.id, originalPosition: $0.position) }
  }
  
  func stopDragging(_ mesh: Mesh) {
    draggingNodes = []
  }
}
10. BoringListView.swift
import SwiftUI

struct BoringListView: View {
  @ObservedObject var mesh: Mesh
  @ObservedObject var selection: SelectionHandler
  
  func indent(_ node: Node) -> CGFloat {
    let base = 20.0
    
    return CGFloat(mesh.distanceFromRoot(node) ?? 0) * CGFloat(base)
  }
  
  var body: some View {
    List(mesh.nodes, id: \.id) { node in
      Text(node.text)
        .padding(EdgeInsets(
          top: 0,
          leading: self.indent(node),
          bottom: 0,
          trailing: 0))
    }
  }
}

struct BoringListView_Previews: PreviewProvider {
  static var previews: some View {
    let mesh = Mesh.sampleMesh()
    let selection =  SelectionHandler()
    
    return BoringListView(mesh: mesh, selection: selection)
  }
}
11. NodeView.swift
import SwiftUI

struct NodeView: View {
  static let width = CGFloat(100)
  // 1
  @State var node: Node
  //2
  @ObservedObject var selection: SelectionHandler
  //3
  var isSelected: Bool {
    return selection.isNodeSelected(node)
  }
  
  var body: some View {
    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)
  }
}

struct NodeView_Previews: PreviewProvider {
  static var previews: some View {
    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)
    }
  }
}
12. EdgeView.swift
import SwiftUI

typealias AnimatablePoint = AnimatablePair<CGFloat, CGFloat>
typealias AnimatableCorners = AnimatablePair<AnimatablePoint, AnimatablePoint>

struct EdgeView: Shape {
  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
  }
  
  var animatableData: AnimatableCorners {
    get { AnimatablePair(
      AnimatablePair(startx, starty),
      AnimatablePair(endx, endy))
    }
    set {
      startx = newValue.first.first
      starty = newValue.first.second
      endx = newValue.second.first
      endy = newValue.second.second
    }
  }
}

struct EdgeView_Previews: PreviewProvider {
  static var previews: some View {
    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)
    }
  }

13. NodeMapView.swift
import SwiftUI

struct NodeMapView: View {
  @ObservedObject var selection: SelectionHandler
  @Binding var nodes: [Node]
  
  var body: some View {
    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)
          }
      }
    }
  }
}

struct NodeMapView_Previews: PreviewProvider {
  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]

  static var previews: some View {
    let selection = SelectionHandler()
    return NodeMapView(selection: selection, nodes: $nodes)
  }
}
14. EdgeMapView.swift
import SwiftUI

struct EdgeMapView: View {
  @Binding var edges: [EdgeProxy]
  
  var body: some View {
    ZStack {
      ForEach(edges) { edge in
        EdgeView(edge: edge)
          .stroke(Color.black, lineWidth: 3.0)
      }
    }
  }
}

struct EdgeMapView_Previews: PreviewProvider {
  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]
  
  static var previews: some View {
    EdgeMapView(edges: $edges)
  }
}
15. MapView.swift
import SwiftUI

struct MapView: View {
  @ObservedObject var selection: SelectionHandler
  @ObservedObject var mesh: Mesh
  
  var body: some View {
    ZStack {
      Rectangle().fill(Color.orange)
      EdgeMapView(edges: $mesh.links)
      NodeMapView(selection: selection, nodes: $mesh.nodes)
    }
  }
}

struct MapView_Previews: PreviewProvider {
  static var previews: some View {
    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)
  }
}
16. SurfaceView.swift
import SwiftUI

struct SurfaceView: View {
  @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?
  
  var body: some View {
    VStack {
      // 1
      Text("drag offset = w:\(dragOffset.width), h:\(dragOffset.height)")
      Text("portal offset = x:\(portalPosition.x), y:\(portalPosition.y)")
      Text("zoom = \(zoomScale)")
      TextField("Breathe…", text: $selection.editingText, onCommit: {
        if let node = self.selection.onlySelectedNode(in: self.mesh) {
          self.mesh.updateNodeText(node, string: self.self.selection.editingText)
        }
      })
      // 2
      GeometryReader { geometry in
        // 3
        ZStack {
          Rectangle().fill(Color.yellow)
          MapView(selection: self.selection, mesh: self.mesh)
            .scaleEffect(self.zoomScale)
            // 4
            .offset(
              x: self.portalPosition.x + self.dragOffset.width,
              y: self.portalPosition.y + self.dragOffset.height)
            .animation(.easeIn)
        }
        .gesture(DragGesture()
        .onChanged { value in
          self.processDragChange(value, containerSize: geometry.size)
        }
        .onEnded { value in
          self.processDragEnd(value)
        })
          .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
          })
      }
    }
  }
}

struct SurfaceView_Previews: PreviewProvider {
  static var previews: some View {
    let mesh = Mesh.sampleProceduralMesh()
    let selection = SelectionHandler()
    return SurfaceView(mesh: mesh, selection: selection)
  }
}

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)
  }
  
  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)
    }
  }
  
  // 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)
    }
  }
}

后記

本篇主要講述了Mind-Map UI,感興趣的給個(gè)贊或者關(guān)注~~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,622評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,716評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,746評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,991評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,706評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,036評論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,029評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,203評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,725評論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,451評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,677評論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,161評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,857評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,266評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,606評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,407評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,643評論 2 380

推薦閱讀更多精彩內(nèi)容