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构建各种自定义图表(二)
12. SwiftUI框架详细解析 (十二) —— 基于SwiftUI创建Mind-Map UI(一)

源码

1. Swift

首先看下工程组织结构

下面就是源码了

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,感兴趣的给个赞或者关注~~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容