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.

1 Like

This was phenomenal. I am looking into having gravity at the root node with “spring”-ing drags on the nodes. Any pointers on where to begin?

It’s a nice idea. I’d love to see it working.

SwiftUI animation does support spring dynamics e.g

.animation(.interpolatingSpring(stiffness: 50, damping: 1))

You’d apply that to the nodes maybe by bouncing them round the end position.

But AFAIK none of the UIKitDynamics API have been ported over yet and I didn’t see any mention of them in the WWDC session. Maybe poke around Xcode12 and see if that’s been done.

Hi Warren,

Thank you for providing this tutorial. I am interested in some of the algorithm you used in this project (e.g. zoom viewport, infinite 2d surface). Could you provide some recommend readings for learning these graphical related techniques systematically? It doesn’t have to be Apple-tech specific.

Thank you for your attention

Hi , thanks for reading.

TBH I don’t really know. I just thought about it for a while, imagined how I wanted it to be in my head and applied learnings from my personal projects which involve large drawings in 2D. Then I tried to use that head model with what SwiftUI provides which took a lot of iteration and failures.

Imagine you are sitting in a room and you have a window to look out of. Just outside that window is a drawing on a sheet of paper that’s bigger than the window. Draw an X on the center of the paper. There’s the origin and it never changes. You also have a paper shifting guy to move the paper.

If you want to see everything that’s on the paper you need to get your paper shifting guy to shift the paper up, down, left and right. You know where the origin is relative to your window. That’s the essence of the view port at 1x resolution.

Then you have a great idea, you want to see more stuff at the same time so you get your paper shifting guy to move the paper away from you. Some of the details are harder to see but now you get a better perspective, forest vs trees. That’s zoom.

Last, you need to draw some stuff beyond the current limits of the paper. Grab some sellotape and stick some more paper on the edge, you can do this as much as you need to. That’s the infinite surface.

I don’t know any particular books but I’m guessing there’s great works on 2D graphics from anytime in the past 30 years.

1 Like