skip to Main Content

I would like to connect two SwiftUI Views with an arrow, one pointing to the other. For example, in this View:

struct ProfileIconWithPointer: View {
    var body: some View {
        VStack {
            Image(systemName: "person.circle.fill")
                .padding()
                .font(.title)
            Text("You")
                .offset(x: -20)
        }
    }
}

I have a Text view and an Image view. I would like to have an arrow pointing from one to the other, like this:

example

However, all the solutions that I could find rely on knowing the position of each element apriori, which I’m not sure SwiftUI’s declarative syntax allows for.

2

Answers


  1. you can use the following code to display an arrow.

    struct CurvedLine: Shape {
        let from: CGPoint
        let to: CGPoint
        var control: CGPoint
        
        var animatableData: AnimatablePair<CGFloat, CGFloat> {
            get {
                AnimatablePair(control.x, control.y)
            }
            set {
                control = CGPoint(x: newValue.first, y: newValue.second)
            }
        }
        
        func path(in rect: CGRect) -> Path {
            var path = Path()
            path.move(to: from)
            path.addQuadCurve(to: to, control: control)
            
            let angle = atan2(to.y - control.y, to.x - control.x)
            let arrowLength: CGFloat = 15
            let arrowPoint1 = CGPoint(x: to.x - arrowLength * cos(angle - .pi / 6), y: to.y - arrowLength * sin(angle - .pi / 6))
            let arrowPoint2 = CGPoint(x: to.x - arrowLength * cos(angle + .pi / 6), y: to.y - arrowLength * sin(angle + .pi / 6))
            
            path.move(to: to)
            path.addLine(to: arrowPoint1)
            path.move(to: to)
            path.addLine(to: arrowPoint2)
            
            return path
        }
    }
    

    you can use this in your code as follows.

    struct ContentView: View {
        var body: some View {
            VStack {
                Text("You")
                    .alignmentGuide(.top) { d in
                        d[.bottom] - 30
                    }
                Image(systemName: "person.circle.fill")
                    .padding()
                    .font(.title)
            }
            .overlay(
                CurvedLine(from: CGPoint(x: 50, y: 50),
                           to: CGPoint(x: 300, y: 200),
                           control: CGPoint(x: 100, y: -300))
                    .stroke(Color.blue, lineWidth: 4)
            )
        }
    }
    

    This will looks like as follows.

    enter image description here

    Depend on your scenario modify the code. Hope this will helps you.

    Login or Signup to reply.
  2. This was a timely question for me as I was recently asked to do something similar. I embellished udi’s answer with Geometry Reader which allows assigning the positions(center point) of the views and the arrow’s start and end points. This idea might be helpful for some scenarios.

    struct ContentView: View {
        
        @State var position1: CGPoint = CGPoint(x: 150, y: 150)
        @State var position2: CGPoint = CGPoint(x: 250, y: 50)
        
        var body: some View {
            VStack {
                Text("Curved Arrow Example")
                    .font(.title)
                
                Spacer()
                
                GeometryReader { geo in
                    Group {
                        Text("You")
                            .font(.largeTitle)
                            .position(position1)
                        Image(systemName: "person.circle.fill")
                            .padding()
                            .font(.system(size: 60))
                            .position(position2)
                    }
                    .overlay(
                        CurvedLine(from: CGPoint(x: position1.x - 30 , y: position1.y),
                                             to: CGPoint(x: position2.x - 40, y: position2.y ),
                                             control: CGPoint(x: -5, y: 100))
                        .stroke(Color.red, lineWidth: 4)
                    )
                }
                .frame(width: 300, height: 300)
                
                Spacer()
            }
        }
    }
    
    
    struct CurvedLine: Shape {
        let from: CGPoint
        let to: CGPoint
        var control: CGPoint
        
        func path(in rect: CGRect) -> Path {
            var path = Path()
            path.move(to: from)
            path.addQuadCurve(to: to, control: control)
            
            let angle = atan2(to.y - control.y, to.x - control.x)
            let arrowLength: CGFloat = 15
            let arrowPoint1 = CGPoint(x: to.x - arrowLength * cos(angle - .pi / 6), y: to.y - arrowLength * sin(angle - .pi / 6))
            let arrowPoint2 = CGPoint(x: to.x - arrowLength * cos(angle + .pi / 6), y: to.y - arrowLength * sin(angle + .pi / 6))
            
            path.move(to: to)
            path.addLine(to: arrowPoint1)
            path.move(to: to)
            path.addLine(to: arrowPoint2)
            
            return path
        }
    }
    

    The result looking like this and adjusting as position1 and position2 are changed:
    enter image description hereenter image description here

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search