User95797654974
User95797654974

Reputation: 644

How to animate a proper transition of a ZStack of views on top of each other like what the default iOS navigation does in SwiftUI?

I have 3 views that are completely separate. I furthermore, do not want the ultraThinMaterial to add on as views stack over each other.

When View 2 appears over view 1 from the bottom edge, it should get removed from the bottom edge as well.

When I tried, either only a simple opacity animation runs or the view behind gets transitioned through the bottom edge as well..

Basically aiming for ZStack of views like how the iOS default navigation works or how the sheet in Apple Maps get overlaid on top of each other.

enum ViewToShow: Identifiable {
    var id: ViewToShow {
        return self
    }
    
    case view1
    case view2
    case view3
}

@Observable
class SheetStackModel {
    var viewsItemManagedByThisSheet: [ViewToShow] = []
    
    init() {
        pushIntoStack(view: .view1)
    }
    
    func getLastView() -> ViewToShow? {
        return viewsItemManagedByThisSheet.last
    }
    
    func pushIntoStack(view: ViewToShow) {
        withAnimation(.default.speed(1.8)) {
            viewsItemManagedByThisSheet.append(view)
        }
    }
    
    func popOutOfStack() {
        withAnimation(.default.speed(1.8)) {
            _ = viewsItemManagedByThisSheet.removeLast()
        }
    }
}

struct View1: View {
    @Environment(SheetStackModel.self) var sheetStackModel
    
    var body: some View {
        GeometryReader { localGeo in
            VStack {
                Text("View 1")
                Button(action: {
                    sheetStackModel.pushIntoStack(view: .view2)

                }, label: {
                    Text("Go to View 2")
                })
            }
        }
    }
}

struct View2: View {
    @Environment(SheetStackModel.self) var sheetStackModel
         
    var body: some View {
        GeometryReader { localGeo in
            VStack {
                Text("View 2")
                Button(action: {
                    sheetStackModel.popOutOfStack()
                }, label: {
                    Text("Go back to View 1")
                })
                
                Button("Go to Detail 3") {
                    sheetStackModel.pushIntoStack(view: .view3)
                }
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(.ultraThinMaterial)
    }
}

struct DetailView3: View {
    @Environment(SheetStackModel.self) var sheetStackModel
    
    var body: some View {
        VStack {
            Text("Even more detail View")
            
            Button("go back to view 2") {
                sheetStackModel.popOutOfStack()
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(.ultraThinMaterial)
    }
}

struct ContentView: View {
     var body: some View {
           ZStack {
                            if let lastView = sheetStackModel.getLastView()  {
                                switch lastView {
                                case .view1:
                                    View1()
                                case .view2:
                                    View2()
                                case .view3:
                                    DetailView3()
                                }
                            }
                        }
                        .transition(.asymmetric(insertion: .move(edge: .bottom), removal: .move(edge: .bottom)))
     }
}

Upvotes: 0

Views: 284

Answers (1)

Benzy Neez
Benzy Neez

Reputation: 21740

I would suggest the following changes:

  • Apply the transition to the individual views, instead of to the ZStack.
  • The transitions do not need to be applied as asymmetric, because you are using the bottom edge for both insertion and removal.
  • Not quite sure why some views had a GeometryReader, this is not needed for the animation.
  • Apply .frame(maxWidth: .infinity, maxHeight: .infinity) to the individual views, so that they fill the screen.
  • If you don't want the .ultraThinMaterial to be seen on each layer then apply it to the ZStack instead.

Like this:

struct View1: View {
    @Environment(SheetStackModel.self) var sheetStackModel

    var body: some View {
        // No GeometryReader needed
        VStack {
            // content as before
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity) // <- ADDED
    }
}

struct View2: View {
    @Environment(SheetStackModel.self) var sheetStackModel

    var body: some View {
        // No GeometryReader needed
        VStack {
            // content as before
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        // .ultraThinMaterial background removed
    }
}

struct DetailView3: View {
    @Environment(SheetStackModel.self) var sheetStackModel

    var body: some View {
        VStack {
            // content as before
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        // .ultraThinMaterial background removed
    }
}

struct ContentView: View {
    @Environment(SheetStackModel.self) var sheetStackModel // <- ADDED

    var body: some View {
        ZStack {
            if let lastView = sheetStackModel.getLastView()  {
                switch lastView {

                // transitions added to separate views
                case .view1:
                    View1()
                        .transition(.move(edge: .bottom))
                case .view2:
                    View2()
                        .transition(.move(edge: .bottom))
                case .view3:
                    DetailView3()
                        .transition(.move(edge: .bottom))
                }
            }
        }
        .background(.ultraThinMaterial) // <- Moved to here
    }
}

Hope this gets you closer to the effect you are trying to achieve.

Upvotes: 0

Related Questions