iosswiftuiswiftui-transition

SwiftUI: matchedGeometryEffect With Nested Views


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


Solution

  • Here is an approach:

    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()
                            }
                        }
                )
            }
        }
    }