arraysswiftswiftuitextcurve

How to show String of array text in curve shape in SwiftUI?


I need array of text should scroll in curve shape like below:

enter image description here

Code: but with this code i can create green colour curve shape view but how to show array of text in curve shape ? please help me to achieve the output.

struct HomeNew: View {
    
    @State private var stringArray = ["Dashboard", "Attendance", "Feed", "Time table"]
    
    var body: some View {
        ZStack{
            Color.white
                .ignoresSafeArea()
            
            VStack{
                ZStack {
                    Color.appGreen2
                    Spacer()
                    ScrollView(.horizontal, showsIndicators: false) {
                        HStack(spacing: 10) {
                            ForEach(stringArray, id: \.self) { item in
                                Text(item)
                                    .foregroundColor(.white)
                                    .padding()
                            }
                        }
                        .padding(.horizontal, 10)
                    }
                }
                .frame(height: 160)
                .clipShape(TopProtractorCurveShape())
            }
        }
    }
}

struct TopProtractorCurveShape: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        
        // Starting from the bottom-left corner
        path.move(to: CGPoint(x: 0, y: rect.height))
        
        // Drawing line to the top-left corner
        path.addLine(to: CGPoint(x: 0, y: rect.height * 0.4))
        
        // Drawing the protractor-like curve
        path.addQuadCurve(to: CGPoint(x: rect.width, y: rect.height * 0.4),
                          control: CGPoint(x: rect.width / 2, y: rect.height * 0.2))
        
        // Drawing line to the bottom-right corner
        path.addLine(to: CGPoint(x: rect.width, y: rect.height))
        
        return path
    }
}

o/p: My output string array shows in line, But i need like above screen shot curve shape text with scrolling. How to write code for that? can we get it by writing SwiftUI coding? or do we need to use any framework in swiftui? if it is there please share.

enter image description here


Solution

  • It seems that what you are wanting to do is to convert the x-displacement from scrolling into an angle of rotation. Also, you probably want to use curved text for the labels.

    Here is how it all comes together:

    struct HomeNew: View {
        let stringArray = ["Dashboard", "Attendance", "Feed", "Time table"]
        let backgroundHeight: CGFloat = 160
        let labelSpacing: CGFloat = 40
        @State private var selectedIndex = 0
        @State private var xOffset = CGFloat.zero
        @GestureState private var dragOffset = CGFloat.zero
    
        var body: some View {
            GeometryReader { outer in
                let w = outer.size.width
                let halfWidth = w / 2
                let curveHeight = backgroundHeight * 0.1
                let slopeLen = sqrt((halfWidth * halfWidth) + (curveHeight * curveHeight))
                let arcRadius = (slopeLen * slopeLen) / (2 * curveHeight)
                let arcAngle = 4 * asin((slopeLen / 2) / arcRadius)
                HStack(spacing: labelSpacing) {
                    ForEach(Array(stringArray.enumerated()), id: \.offset) { index, item in
                        Text(item)
                            .lineLimit(1)
                            .fixedSize()
                            .hidden()
                            .overlay {
                                GeometryReader { proxy in
                                    let midX = proxy.frame(in: .global).midX
                                    let offsetFromMiddle = midX - (w / 2)
    
                                    // https://stackoverflow.com/a/77280669/20386264
                                    CurvedText(string: item, radius: arcRadius)
                                        .foregroundStyle(.white)
                                        .offset(y: -arcRadius)
                                        .rotationEffect(.radians((offsetFromMiddle / w) * arcAngle))
                                        .offset(x: -offsetFromMiddle, y: arcRadius)
                                        .onAppear {
                                            if selectedIndex == index {
                                                xOffset = -offsetFromMiddle
                                            }
                                        }
                                        .onChange(of: dragOffset) { oldVal, newVal in
                                            if newVal == 0 {
                                                let halfWidth = (proxy.size.width + labelSpacing) / 2
                                                if (abs(offsetFromMiddle) < halfWidth) ||
                                                    (index == 0 && offsetFromMiddle > 0) ||
                                                    (index == stringArray.count - 1 && offsetFromMiddle < 0) {
                                                    selectedIndex = index
                                                    xOffset -= offsetFromMiddle
                                                }
                                            }
                                        }
                                }
                            }
                    }
                }
                .offset(x: xOffset + dragOffset)
                .animation(.easeInOut, value: xOffset)
                .gesture(
                    DragGesture(minimumDistance: 0)
                        .updating($dragOffset) { val, state, trans in
                            state = val.translation.width
                        }
                        .onEnded { val in
                            xOffset += val.translation.width
                        }
                )
                .padding(.top, curveHeight + 36)
                .frame(width: w)
            }
            .frame(height: backgroundHeight, alignment: .top)
            .background(.green) //appGreen2
            .clipShape(TopProtractorCurveShape())
            .ignoresSafeArea()
        }
    }
    

    Animation