skip to Main Content

I designed a SwiftUI view which is a scrollview. Now I need to add a vertical swipe gesture to it which shall take it to a different view. I tried to do it using the tabView and adding a rotating effect of -90 degrees to it. But that rotates my original view too and that’s not what I want. I couldn’t find any relevant help in SwiftUI which deals with swiping up a scrollview to a new view.
Here’s my code..

the vertical swipe I achieved using this. But my view get rotated. Setting other angles disappears the view somehow. I am new to SwiftUI, I am stuck on it for a week now.1

GeometryReader { proxy in
        TabView {
            ScrollView {
                VStack(alignment: .center) {
                    ZStack(alignment: .leading) {
                        Image("Asset 13").resizable().frame(width: percentWidth(percentage: 100), height: percentHeight(percentage: 50), alignment: .top)
                        HStack {
                            Spacer()
                            Image("Asset 1")//.padding(.bottom, 130)
                            Spacer()
                        }.padding(.bottom, 150)
                        
                        HStack {
                            VStack(spacing:2) {
                                Text("followers").foregroundColor(.white).padding(.leading, 20)
                                HStack {
                                    Image("Asset 3")
                                    Text("10.5k").foregroundColor(.white)
                                }
                            }
                            Spacer()
                            VStack {
                                Image("Asset 10").padding(.trailing)
                                Text("300K Review ").foregroundColor(.white)
                            }
                            
                        }.background(Image("Asset 2").resizable().frame(width: percentWidth(percentage: 100), height: percentHeight(percentage: 6), alignment: .leading))
                            .padding(.top, 410)
                        HStack {
                            Spacer()
                            Image("Asset 14").resizable().frame(width: percentWidth(percentage: 50), height: percentHeight(percentage: 25), alignment: .center)
                            Spacer()
                        }.padding(.top, 390)
                        
                        
                    }
                    VStack(spacing: 4) {
                        Text("Karuna Ahuja | Yoga Instructor").font(Font.custom(FontName.bold, size: 22))
                        Text("12 Years of Experience with Bhartiya Yog Sansthan").tracking(-1).font(Font.custom(FontName.light, size: 16)).opacity(0.4)
                    }
                    Divider()
                    HStack {
                        ZStack {
                            Image("Asset 6").resizable().frame(width: percentWidth(percentage: 30), height: percentHeight(percentage: 12), alignment: .center)
                            VStack {
                                Image("Asset 5").resizable().frame(width: percentWidth(percentage: 8), height: percentHeight(percentage: 4), alignment: .center)
                                Text("245").font(Font.custom(FontName.bold, size: 16))
                                Text("Video").font(Font.custom(FontName.medium, size: 16)).opacity(0.5)
                            }
                        }
                        
                        ZStack {
                            Image("Asset 6").resizable().frame(width: percentWidth(percentage: 30), height: percentHeight(percentage: 12), alignment: .center)
                            VStack {
                                Image("Asset 7").resizable().frame(width: percentWidth(percentage: 8), height: percentHeight(percentage: 4), alignment: .center)
                                Text("45").font(Font.custom(FontName.bold, size: 16))
                                Text("Live Class").font(Font.custom(FontName.medium, size: 16)).opacity(0.5)
                            }
                        }
                        
                        ZStack {
                            Image("Asset 6").resizable().frame(width: percentWidth(percentage: 30), height: percentHeight(percentage: 12), alignment: .center)
                            VStack {
                                Image("Asset 9").resizable().frame(width: percentWidth(percentage: 8), height: percentHeight(percentage: 4), alignment: .center)
                                Text("245").font(Font.custom(FontName.bold, size: 16))
                                Text("Sessions").font(Font.custom(FontName.medium, size: 16)).opacity(0.5)
                            }
                        }
                    }
                    Divider()
                    Text("Shine bright so that your light leads other. I'm a fitness junkie, high-energy yoga instructor. Let's make fitness FUN!").font(Font.custom(FontName.normal, size: 16)).tracking(-1).opacity(0.7).padding([.leading,.trailing], 6)
                    VideoPlayer(player: AVPlayer(url: videoUrl))
                        .frame(height: 320)
                    
                    Spacer()
                }.gesture(DragGesture(minimumDistance: 20, coordinateSpace: .global)
                    .onEnded { value in
                        let horizontalAmount = value.translation.width as CGFloat
                        let verticalAmount = value.translation.height as CGFloat
                        
                        if abs(horizontalAmount) > abs(verticalAmount) {
                            print(horizontalAmount < 0 ? "left swipe" : "right swipe")
                        } else {
                            print(verticalAmount < 0 ? "up swipe" : "down swipe")
                        }
                    })
            }.edgesIgnoringSafeArea(.all)
                .ignoresSafeArea()
            Text("this")
            Text("this")
            Text("this")
            
            //                ForEach(colors, id: .self) { color in
            //                    color // Your cell content
            //                }
            //                .rotationEffect(.degrees(-90)) // Rotate content
            //                .frame(
            //                    width: proxy.size.width,
            //                    height: proxy.size.height
            //                )
        }
        .frame(
            width: proxy.size.height, // Height & width swap
            height: proxy.size.width
        )
        .rotationEffect(.degrees(90), anchor: .topLeading) // Rotate TabView
        .offset(x: proxy.size.width) // Offset back into screens bounds
        .tabViewStyle(
            PageTabViewStyle(indexDisplayMode: .never)
        )
    }

2

Answers


  1. The only pure SwiftUI way I see is to do your own ScrollView implementation, which is not too complicated. This example has two views on top of each other. If you drag the first view further up than to the middle of the screen, it swipes away to reveal the second view.

    enter image description here

    struct ContentView: View {
        
        @State private var offset = CGFloat.zero
        @State private var dragOffset = CGFloat.zero
        @State private var viewHeight = CGFloat.zero
        
        var body: some View {
            
            GeometryReader { fullgeo in
                ZStack(alignment: .top) {
                    
                    SecondView()
                    // necessary for second view to resize individually
                        .frame(height: fullgeo.size.height)
                    
                    ScrollingView()
                        .overlay( GeometryReader { geo in Color.clear.onAppear { viewHeight = geo.size.height }})
                    
                        .offset(y: offset + dragOffset)
                    
                        .gesture(DragGesture()
                            .onChanged { value in
                                dragOffset = value.translation.height
                            }
                            .onEnded { value in
                                withAnimation(.easeOut) {
                                    dragOffset = .zero
                                    offset += value.predictedEndTranslation.height
                                    
                                    // if bottom dragged higher than 50% of screen > second view
                                    if offset < -(viewHeight - fullgeo.size.height/2) {
                                        dragOffset = -viewHeight
                                        return
                                    }
                                    
                                    // else constrain to top / bottom of ScrollingView
                                    offset = max(min(offset, 0), -(viewHeight - fullgeo.size.height))
                                    
                                    
                                }
                            }
                        )
                }
            }
        }
    }
    
    struct ScrollingView: View {
        var body: some View {
            VStack {
                Text("View Top").font(.headline)
                ForEach(0..<10) { _ in
                    Text("Content")
                        .frame(width: 200, height: 100)
                        .background(.white)
                }
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(.gray)
        }
    }
    
    struct SecondView: View {
        var body: some View {
            Text("View Bottom").font(.headline)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(.orange)
        }
    }
    
    Login or Signup to reply.
  2. Use this code:

    import SwiftUI
    
    struct PullToRefreshView: View
    {
        private static let minRefreshTimeInterval = TimeInterval(0.2)
        private static let triggerHeight = CGFloat(100)
        private static let indicatorHeight = CGFloat(100)
        private static let fullHeight = triggerHeight + indicatorHeight
        
        let backgroundColor: Color
        let foregroundColor: Color
        let isEnabled: Bool
        let onRefresh: () -> Void
        
        @State private var isRefreshIndicatorVisible = false
        @State private var refreshStartTime: Date? = nil
        
        init(bg: Color = .neutral0, fg: Color = .neutral90, isEnabled: Bool = true, onRefresh: @escaping () -> Void)
        {
            self.backgroundColor = bg
            self.foregroundColor = fg
            self.isEnabled = isEnabled
            self.onRefresh = onRefresh
        }
        
        var body: some View
        {
            VStack(spacing: 0)
            {
                LazyVStack(spacing: 0)
                {
                    Color.clear
                        .frame(height: Self.triggerHeight)
                        .onAppear
                        {
                            if isEnabled
                            {
                                withAnimation
                                {
                                    isRefreshIndicatorVisible = true
                                }
                                refreshStartTime = Date()
                            }
                        }
                        .onDisappear
                        {
                            if isEnabled, isRefreshIndicatorVisible, let diff = refreshStartTime?.distance(to: Date()), diff > Self.minRefreshTimeInterval
                            {
                                onRefresh()
                            }
                            withAnimation
                            {
                                isRefreshIndicatorVisible = false
                            }
                            refreshStartTime = nil
                        }
                }
                .frame(height: Self.triggerHeight)
                
                indicator
                    .frame(height: Self.indicatorHeight)
            }
            .background(backgroundColor)
            .ignoresSafeArea(edges: .all)
            .frame(height: Self.fullHeight)
            .padding(.top, -Self.fullHeight)
        }
        
        private var indicator: some View
        {
            ProgressView()
                .progressViewStyle(CircularProgressViewStyle(tint: foregroundColor))
                .opacity(isRefreshIndicatorVisible ? 1 : 0)
        }
    }
    

    sample:

    ScrollView{
         VStack(spacing:0) {
         //top of scrollView
             PullToRefreshView{
                  //todo
            }
       }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search