SwiftUI: matchedGeometryEffect With Nested Views

927 views Asked by At

I'm facing some dilemma. I like to separate my views for readability . so for example i'm having this kind of structure

MainView -> 
--List1
----Items1
--List2 
----Items2
----DetailView
------CellView

so cellView having same namespace for matchedGeometryEffect as DetailsView. to make the effect of transition to detail view from cell item in list. the problem is that this details view is limited to List2 screen /View.

Here's some code to make it more clear

First I have main View

struct StartFeedView: View {

     
    var body: some View {
        ScrollView(.vertical) {
            ShortCutView()

            PopularMoviesView()
        }
    }
}

then I have PopularMoviesView()

struct PopularMoviesView: View {
    @Namespace var namespace
    @ObservedObject var viewModel = ViewModel()
    @State var showDetails: Bool = false
    @State var selectedMovie: Movie?

    var body: some View {
        ZStack {
            if !showDetails {
                VStack {
                    HStack {
                        Text("Popular")
                            .font(Font.itemCaption)
                            .padding()
                        Spacer()
                        Image(systemName: "arrow.forward")
                            .font(Font.title.weight(.medium))
                            .padding()

                    }
                    ScrollView(.horizontal) {
                        if let movies = viewModel.popularMovies {
                            HStack {
                                ForEach(movies.results, id: \.id) { movie in
                                    MovieCell(movie: movie, namespace: namespace, image: viewModel.imageDictionary["\(movie.id)"]!)
                                        .padding(6)
                                        .frame(width: 200, height: 300)
                                        .onTapGesture {
                                            self.selectedMovie = movie

                                            withAnimation(Animation.interpolatingSpring(stiffness: 270, damping: 15)) {
                                                showDetails.toggle()
                                            }

                                        }
                                }
                            }
                        }
                    }
                    .onAppear {
                        viewModel.getMovies()
                    }
                }


            }

            if showDetails, let movie = selectedMovie, let details = movie.details {
                MovieDetailsView(details: details, namespace: namespace, image: viewModel.imageDictionary["\(movie.id)"]!)
                    .onTapGesture {
                        withAnimation(Animation.interpolatingSpring(stiffness: 270, damping: 15)) {
                            showDetails.toggle()
                        }
                    }
            }
        }
    }
}

so whenever I click on MovieCell.. it will expand to the limit of the PopularMoviesView boundaries on the main view.

Is there some kind of way to make it full screen without to have to inject the detail view into the MainView? Cause that would be really dirty

1

There are 1 answers

0
ChrisR On BEST ANSWER

Here is an approach:

  • Get the size of MainView with GeometryReader and pass it down.
  • in DetailView use .overlay which can grow bigger than its parent view, if you specify an explicit .frame
  • You need another inner GeometryReaderto get the top pos of inner view for offset.

enter image description here

struct ContentView: View {
    
    var body: some View {
        // get size of overall view
        GeometryReader { geo in
            ScrollView(.vertical) {
                Text("ShortCutView()")
                
                PopularMoviesView(geo: geo)
            }
        }
    }
}


struct PopularMoviesView: View {
    
    // passed in geometry from parent view
    var geo: GeometryProxy
    // own view's top position, will be updated by GeometryReader further down
    @State var ownTop = CGFloat.zero
    
    @Namespace var namespace
    @State var showDetails: Bool = false
    @State var selectedMovie: Int?
    
    
    var body: some View {
        
        if !showDetails {
            VStack {
                HStack {
                    Text("Popular")
                        .font(.caption)
                        .padding()
                    Spacer()
                    Image(systemName: "arrow.forward")
                        .font(Font.title.weight(.medium))
                        .padding()
                    
                }
                ScrollView(.horizontal) {
                    HStack {
                        ForEach(0..<10, id: \.self) { movie in
                            Text("MovieCell \(movie)")
                                .padding()
                                .matchedGeometryEffect(id: movie, in: namespace)
                                .frame(width: 200, height: 300)
                                .background(.yellow)
                            
                                .onTapGesture {
                                    self.selectedMovie = movie
                                    
                                    withAnimation(Animation.interpolatingSpring(stiffness: 270, damping: 15)) {
                                        showDetails.toggle()
                                    }
                                }
                        }
                    }
                }
            }
        }
        
        if showDetails, let movie = selectedMovie {
            // to get own view top pos
            GeometryReader { geo in Color.clear.onAppear {
                ownTop = geo.frame(in: .global).minY
                print(ownTop)
            }}
            
            // overlay can become bigger than parent
            .overlay (
                Text("MovieDetail \(movie)")
                    .font(.largeTitle)
                    .matchedGeometryEffect(id: movie, in: namespace)
                    .frame(width: geo.size.width, height: geo.size.height)
                    .background(.gray)
                    .position(x: geo.frame(in: .global).midX, y: geo.frame(in: .global).midY - ownTop)
                
                    .onTapGesture {
                        withAnimation(Animation.interpolatingSpring(stiffness: 270, damping: 15)) {
                            showDetails.toggle()
                        }
                    }
            )
        }
    }
}