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

版本記錄

版本號 時間
V1.0 2020.03.30 星期一

前言

今天翻閱蘋果的API文檔傲绣,發(fā)現(xiàn)多了一個框架SwiftUI,這里我們就一起來看一下這個框架。感興趣的看下面幾篇文章脆丁。
1. SwiftUI框架詳細解析 (一) —— 基本概覽(一)
2. SwiftUI框架詳細解析 (二) —— 基于SwiftUI的閃屏頁的創(chuàng)建(一)
3. SwiftUI框架詳細解析 (三) —— 基于SwiftUI的閃屏頁的創(chuàng)建(二)
4. SwiftUI框架詳細解析 (四) —— 使用SwiftUI進行蘋果登錄(一)
5. SwiftUI框架詳細解析 (五) —— 使用SwiftUI進行蘋果登錄(二)
6. SwiftUI框架詳細解析 (六) —— 基于SwiftUI的導航的實現(xiàn)(一)
7. SwiftUI框架詳細解析 (七) —— 基于SwiftUI的導航的實現(xiàn)(二)
8. SwiftUI框架詳細解析 (八) —— 基于SwiftUI的動畫的實現(xiàn)(一)
9. SwiftUI框架詳細解析 (九) —— 基于SwiftUI的動畫的實現(xiàn)(二)
10. SwiftUI框架詳細解析 (十) —— 基于SwiftUI構建各種自定義圖表(一)
11. SwiftUI框架詳細解析 (十一) —— 基于SwiftUI構建各種自定義圖表(二)
12. SwiftUI框架詳細解析 (十二) —— 基于SwiftUI創(chuàng)建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,感興趣的給個贊或者關注~~~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末动雹,一起剝皮案震驚了整個濱河市槽卫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌胰蝠,老刑警劉巖歼培,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異茸塞,居然都是意外死亡躲庄,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門钾虐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來噪窘,“玉大人效扫,你說我怎么就攤上這事倔监。” “怎么了丐枉?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我籍嘹,道長闪盔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任辱士,我火速辦了婚禮泪掀,結果婚禮上颂碘,老公的妹妹穿的比我還像新娘异赫。我一直安慰自己头岔,他們只是感情好塔拳,可當我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著峡竣,像睡著了一般靠抑。 火紅的嫁衣襯著肌膚如雪适掰。 梳的紋絲不亂的頭發(fā)上颂碧,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天,我揣著相機與錄音载城,去河邊找鬼肌似。 笑死诉瓦,一個胖子當著我的面吹牛川队,可吹牛的內容都是我干的垦搬。 我是一名探鬼主播呼寸,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼猴贰,長吁一口氣:“原來是場噩夢啊……” “哼对雪!你這毒婦竟也來了米绕?” 一聲冷哼從身側響起瑟捣,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤栅干,失蹤者是張志新(化名)和其女友劉穎迈套,沒想到半個月后碱鳞,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體桑李,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡窿给,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年贵白,在試婚紗的時候發(fā)現(xiàn)自己被綠了崩泡。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片禁荒。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡角撞,死狀恐怖呛伴,靈堂內的尸體忽然破棺而出谒所,到底是詐尸還是另有隱情热康,我是刑警寧澤百炬,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布褐隆,位于F島的核電站剖踊,受9級特大地震影響庶弃,放射性物質發(fā)生泄漏。R本人自食惡果不足惜歇攻,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一固惯、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧缴守,春花似錦葬毫、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至村砂,卻和暖如春烂斋,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背础废。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工汛骂, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人评腺。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓帘瞭,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蒿讥。 傳聞我的和親對象是個殘疾皇子蝶念,可洞房花燭夜當晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內容