0

I have a scrollview who's content expands to fill the screen and stacks in front using the zIndex. The expanded content also has a scrollview for the content inside. I'm literally trying to mimic the Apps Stores "Today" tab with expanding cards and scrollable content inside. The way I built this though I realized the expanding view is still part of the parent scrollview. As a result the scrollviews are nested and conflict. This was not what I intended.

Im very new to programming. This is the code that basically expands each card. Its pretty basic. A ternary expands the cards. The CardView is the cards content. Attached is a screenshot of what I'm trying to achieve. Need help here. Any info would be great. Been searching the internet for a way to do this right.

        struct Media: View {

@EnvironmentObject var vm : ViewModel
@Environment(\.colorScheme) var mode
@State var showCard = false
@State var activeCard = -1
let height = UIScreen.main.bounds.height
var body: some View {
    
    ZStack {
        
        ScrollView (.vertical, showsIndicators: false) {
            
            VStack (alignment:.center, spacing: 30) {
                
                ForEach (vm.cards.indices, id: \.self) { i in
                    
                    let z=vm.cards[i].z
                    
                    GeometryReader { geom in
                        
                        ZStack {
                            
                            CardView (showCard: $showCard,
                                      activeCard: self.$activeCard,
                                      zValue: $vm.cards[i].z,
                                      i: i,
                                      cards: vm.cards[i])
                            
                        }//Z
                        .frame(minHeight: showCard && activeCard == i ? height : nil) //Animates Card Scaling
                        .padding(.horizontal, showCard && activeCard == i ? 0:20) // Card Padding
                        .offset(y: i==activeCard ? -geom.frame(in: .global).minY : 0)
                        
                    } //GEOM
                    .frame(minHeight: 450)
                    .zIndex(z)
                    
                } //LOOP
                
            } //V
            .padding([.bottom, .top])

        } //SCROLLVIEW
        

    } //Z
    .background(Color("Background Gray"))
    
}

} enter image description here

  • the easiest way would be to show the detail view with the copy text in a `.sheet` or `.fullScreenCover`... would that be acceptable? – ChrisR Feb 04 '22 at 15:49
  • Thanks. For this app I'm making I want it to function this specific way, with a smooth transition from card to fullscreen then back to card. Don't those slide up from the bottom only?? – TheManOfSteell Feb 04 '22 at 16:22
  • yes, those would slide up ... but I tried something. see below – ChrisR Feb 04 '22 at 16:22

3 Answers3

0

I don't see a problem with the nested ScrollViews, as you put the DetailView on top. I tried to rebuild a simplified version from your code, see below.

BTW: You don't need the outer ZStack.
And at some point you should consider using ForEach over the cards, not over the indices, otherwise you'll run into UI update problems when inserting or deleting cards. I left it for now to stay closer to your original.

struct ContentView: View {
    
    @State var activeCard = -1
    let height = UIScreen.main.bounds.height
    
    var body: some View {
        
        
        ScrollView (.vertical, showsIndicators: false) {
            
            VStack (alignment:.center, spacing: 30) {
                
                ForEach (data.indices, id: \.self) { i in
                    
                    GeometryReader { geom in
                        
                        ZStack {
                            
                            DetailCellView(entry: data[i], isActive: activeCard == i)
                            
                                .onTapGesture {
                                    if activeCard == i {
                                        activeCard = -1
                                    } else {
                                        activeCard = i
                                    }
                                }
                            
                        }//Z
                        .frame(minHeight: activeCard == i ? height : nil) //Animates Card Scaling
                        .padding(.horizontal, activeCard == i ? 0 : 20) // Card Padding
                        .offset(y: activeCard == i ? -geom.frame(in: .global).minY : 0)
                        
                    } //GEOM
                    .frame(minHeight: 450)
                    .zIndex(activeCard == i ? 1 : 0)
                    
                    .animation(.default, value: activeCard)
                    
                } //LOOP
                
            } //V
//            .padding([.bottom, .top])
            
        } //SCROLLVIEW
        
        .background(.gray)
    }
}

enter image description here

ChrisR
  • 9,523
  • 1
  • 8
  • 26
  • The problem I seem to have is when the scrollview comes completely to rest at the top or bottom and then I try to drag more for the bounce effect, I end up dragging the main scrollview instead. Not sure if that happens with you?? – TheManOfSteell Feb 04 '22 at 16:38
  • yes, I didn't realize, but I have the same problem. I can disable the inner (DetailView) ScrollView when not active by adding `.disabled(!isActive)`, but that won't work with the outer, because the outer overwrites the inner – so then nothing is scrollable anymore. I will keep trying ... – ChrisR Feb 04 '22 at 18:12
  • I tried so many solution but none seem to either work, or maintain a seamless experience. I feel like when the card expands it needs to escape the scrollview to the top level, if such am thing is possible. – TheManOfSteell Feb 04 '22 at 18:26
  • Actually this seems to be a bug. It makes no sene that you can scroll in one direction, but not in the other. You should file a bug report to Apple. – ChrisR Feb 04 '22 at 18:57
  • Dang. I was hoping I was just doing something wrong. Ok. – TheManOfSteell Feb 04 '22 at 19:48
0

for the while being: a custom outer dragview as workaround ...

struct ContentView: View {
    
    @State var activeCard = -1
    let height = UIScreen.main.bounds.height
    
    @State private var offset = CGFloat.zero
    @State private var drag = CGFloat.zero
    
    var body: some View {
        
        GeometryReader { geo in
            VStack (alignment:.center, spacing: 30) {
                
                ForEach (data.indices, id: \.self) { i in
                    
                    GeometryReader { geom in
                        
                        ZStack {
                            DetailCellView(entry: data[i],
                                           isActive: activeCard == i)
                            
                                .onTapGesture {
                                    if activeCard == i {
                                        activeCard = -1
                                    } else {
                                        activeCard = i
                                    }
                                }
                        }
                        .frame(minHeight: activeCard == i ? height : nil) //Animates Card Scaling
                        .padding(.horizontal, activeCard == i ? 0 : 20) // Card Padding
                        .offset(y: activeCard == i ? -geom.frame(in: .global).minY : 0)
                        
                    }
                    .frame(minHeight: 400)
                    .zIndex(activeCard == i ? 1 : 0)
                    
                    .animation(.default, value: activeCard)
                    
                }
            }
            
            // custom drag view
            .offset(x: 0 , y: offset + drag)
            .background(.gray)
            
            // drag
            .gesture(DragGesture()
                        .onChanged({ value in
                drag = value.translation.height
            })
                        .onEnded({ value in
                print(offset)
                
                withAnimation(.easeOut)  {
                    offset += value.predictedEndTranslation.height
                    offset = min(offset, 0)
                    offset = max(offset, -CGFloat(data.count * (400 + 30)) + height )
                    drag = 0
                }
                print(offset)
            })
            )
        }
    }
}
ChrisR
  • 9,523
  • 1
  • 8
  • 26
0

So I figured out how to accomplish what I was trying to accomplish.

struct ScrollingHelper: UIViewRepresentable {

let proxy: ScrollingProxy // reference type

func makeUIView(context: Context) -> UIView {
    
    return UIView() // managed by SwiftUI, no overloads
}

func updateUIView(_ uiView: UIView, context: Context) {
    
    proxy.catchScrollView(for: uiView) // here UIView is in view hierarchy
}
}


class ScrollingProxy {
private var scrollView: UIScrollView?
func catchScrollView(for view: UIView) {
    
    if nil == scrollView {
        
        scrollView = view.enclosingScrollView()
    }
}

func disableScrolling(_ flag: Bool) {
    scrollView?.isScrollEnabled = flag
    print(flag)
    
}

}

extension UIView {
func enclosingScrollView() -> UIScrollView? {
    var next: UIView? = self
    repeat {
        next = next?.superview
        if let scrollview = next as? UIScrollView {
            return scrollview
        }
    } while next != nil
    return nil
}
}

I used the above code from https://stackoverflow.com/a/60855853/12299030 & Disable Scrolling in SwiftUI List/Form

Then used in a TapGesture

scrollEnabled = false

scrollProxy.disableScrolling(scrollEnabled)

and used background modifier:

.background(ScrollingHelper(proxy: scrollProxy))