Group Group Group Group Group Group Group Group Group

Creating a Mind-Map UI in SwiftUI | raywenderlich.com

In this tutorial, you’ll learn how to create an animated spatial UI in SwiftUI with support for pan and zoom interactions.


This is a companion discussion topic for the original entry at https://www.raywenderlich.com/7705231-creating-a-mind-map-ui-in-swiftui

Hi Warren, thanks for sharing this tutorial. It’s complex but doable. Could you provide a path to delete & add nodes?

Hi Tim

Thanks for reading the tutorial. The routine for adding nodes is all demonstrated in Mesh.sampleProceduralMesh()

I’ve also attached the code for view called Helpers that provides controls to add child & sibling , and delete node.

You can add Helpers into the VStack of SurfaceView to enable all the controls.

import SwiftUI

struct Helpers: View {
  
  @ObservedObject var mesh: Mesh
  @ObservedObject var selection: SelectionHandler
  
  func onlySelectedNode() -> Node? {
    return selection.onlySelectedNode(in: mesh)
  }
  
  var onlyOneNodeSelected: Bool {
    return selection.onlySelectedNode(in: mesh) != nil
  }
  
  var rootNodeSelected: Bool {
    if let node = self.onlySelectedNode() {
      return node == mesh.rootNode()
    }
    return false
  }
  
  var canAddSibling: Bool {
    if let _ = self.onlySelectedNode(), rootNodeSelected == false {
      return true
    }
    return false
  }
  
  
  var body: some View {
    ZStack {
      HStack(spacing: 16.0) {
        Button(action: {
          if let parent = self.onlySelectedNode() {
            let position = self.mesh.positionForNewChild(parent, length: 300)
            let child = self.mesh.addChild(parent, at: position)
            self.selection.selectNode(child)
          }
        }) {
          HStack {
            Text("Add Child")
            Image(systemName: "arrow.turn.right.down")
          }
          .padding(8.0)
          .overlay(
            RoundedRectangle(cornerRadius: 5)
              .stroke(self.onlyOneNodeSelected ? Color.purple:Color.gray, lineWidth: 5)
          )
          
        }
        .foregroundColor(self.onlyOneNodeSelected ? Color.purple:Color.gray)
        Button(action: {
          if let node = self.onlySelectedNode(),
            self.canAddSibling,
            let parent = self.mesh.locateParent(node) {
            if let sibling = self.mesh.addSibling(node) {
              let position = self.mesh.positionForNewChild(parent, length: 300)
              self.mesh.positionNode(sibling, position: position)
              self.selection.selectNode(sibling)
            }
          }
        }) {
          HStack {
            Text("Add Sibling")
            Image(systemName: "arrow.turn.down.right")
          }
          .padding(8.0)
          .overlay(
            RoundedRectangle(cornerRadius: 5)
              .stroke(self.canAddSibling ? Color.green:Color.gray, lineWidth: 5)
          )
        }
        .foregroundColor(self.canAddSibling ? Color.green:Color.gray)
        Button(action: {
          let selectedNodes = self.selection.selectedNodes(in: self.mesh)
          self.mesh.deleteNodes(selectedNodes)
        }) {
          HStack {
            Text("Delete Node")
            Image(systemName: "trash")
          }
          .padding(8.0)
          .overlay(
            RoundedRectangle(cornerRadius: 5)
              .stroke(self.onlyOneNodeSelected && rootNodeSelected == false ?  Color.red: Color.gray, lineWidth: 5)
          )
        }
        .foregroundColor(self.onlyOneNodeSelected && rootNodeSelected == false ?  Color.red: Color.gray)
      }
      .padding(8.0)
    }
  }
}

struct Helpers_Previews: PreviewProvider {
  
  @State static var mesh = Mesh()
  @State static var selection = SelectionHandler()
  
  static var previews: some View {
    Helpers(mesh: mesh, selection: selection)
  }
}

Hi Warren, Thanks very much!

Stupid question: How to do that and where exactly?
“You can add Helpers into the VStack of SurfaceView to enable all the controls.”

Regards,
Tim

No worries

  1. Create a new file in the project called Helpers.swift. Copy/Paste that code into the file.

  2. Open SurfaceView.swift

  3. Examine the body property of struct SurfaceView. You’ll see that the top level item is a VStack.

  4. Delete the 3 Informational text fields. They don’t need to be there anymore.

  5. Instantiate Helpers , injecting the mesh and the selection. Your body should now start like this.

     var body: some View {
         VStack {
           // 1
           Helpers(mesh: mesh, selection: selection)
           TextField("Breathe…", text:
           .....
    

My version has a white background because I’m painting the backing rect with Rectangle().fill(Color(UIColor.systemBackground))

Hi Warren,

This works perfect!

Next question how to make the changes persistent? :wink:

Regards,

Tim

Hi Tim

Thats not something I implemented for the tutorial but think about what you need to do for this.

  1. Conform Node to Codable .
extension Node: Codable {}
  1. Conform Edge to Codable
extension Edge: Codable {}
  1. Conform Mesh to Codable .
class Mesh: ObservableObject, Codable

This is harder, you’ll need to override

init(from decoder: Decoder) throws

and

func encode(to encoder: Encoder) throws

and then consider what you need to store & restore.

  • the nodes array.
  • the edges array.
  • the rootNodeID value.

Theres a great tutorial on RW on the Codable protocol and how to create custom implementations.

Once you have that you can read & write to a file. You’d do that in the SceneDelegate class by implementing the appropriate UIWindowSceneDelegate methods.

That would be the basic path to a persistent model.

Good luck.

Hi Warren,

Thanks so much!

Your support is very much appreciated.

It’s a great tutorial!

Kind Regards,

Tim

I’ve started a GitHub for some ongoing work as I play some more with the concept.

I added persistence this morning.