Search code examples
swiftanimationswiftuiswiftui-navigationstackxcode15

Scale Animation and Safe Area with NavigationStack in SwiftUI


I'm trying to wrap a content view inside a NavigationStack. However, when I use NavigationStack, the scale animation shows strange behavior and it does not ignore the safe area either (it works perfectly without NavigationStack). I've already found a solution for the scale animation around the safe area (without NavigationStack) here. SideMenuControllerView serves as the parent view responsible for managing all animations related to the content view.

struct SideMenuControllerView<Content: View>: View {
    @State private var scale = false
    private var content: Content
    
    
    init(content: Content) {
        self.content = content
    }
    
    var body: some View {
        ZStack {
            content
                .offset(x: 100)
                .scaleEffect(scale ? 0.8 : 1)
                .ignoresSafeArea()
            
            Button {
                withAnimation(.easeInOut(duration: 3)) {
                    scale.toggle()
                }
            } label: {
                Text("Scale")
            }
            .frame(maxWidth: .infinity,alignment: .leading)
            .padding()
        }
    }
    
}

struct OrangeView: View {
    @Binding var path: [String]
    
    var body: some View {
        NavigationStack(path: $path) { /// <- NOT wroking
            OrangeViewContent()
        }
        //OrangeViewContent() /// <- works fine
    }
}

struct OrangeViewContent: View {
    var body: some View {
        GeometryReader { proxy in
            ZStack {
                Color.orange
                
                HStack {
                    Text("TopBar")
                        .frame(maxWidth: .infinity)
                        .background { Color.gray }
                }
                .frame(maxWidth: .infinity,maxHeight: .infinity, alignment: .top)
                
                VStack {
                    Image(systemName: "globe")
                        .imageScale(.large)
                        .foregroundStyle(.black)
                    Text("Hello, world!")
                }
                .padding()
                .background{ Color.cyan }
            }
        }
    }
}


#Preview {
    SideMenuControllerView(
        content: OrangeView(path: .constant([]))
    )
}



Solution

  • The modifiers for transformation and ignoring safe area need to be applied directly to OrangeViewContent. So now that this content is surrounded by a NavigationStack, it means moving the modifiers into OrangeView and passing the boolean flag as a parameter to this view.

    Like this:

    struct ContentViewTest: View {
        @State private var scale = false
    
        var body: some View {
            ZStack {
                OrangeView(path: .constant([]), scale: scale)
    
                Button {
                    withAnimation(.easeInOut(duration: 3)) {
                        scale.toggle()
                    }
                } label: {
                    Text("Scale")
                }
                .frame(maxWidth: .infinity,alignment: .leading)
                .padding()
            }
        }
    }
    
    struct OrangeView: View {
        @Binding var path: [String]
        let scale: Bool
    
        var body: some View {
            NavigationStack(path: $path) {
                OrangeViewContent()
                    .offset(x: 100)
                    .scaleEffect(scale ? 0.8 : 1)
                    .ignoresSafeArea()
            }
        }
    }
    

    EDIT Following from your comments, I tried quite hard to find a way to avoid having to move the modifiers inside the NavigationStack, but I couldn't get it to work. So the best I can suggest is that you pass the Bool as a Binding to SideMenuControllerView (where it is controlled), so that it can also be passed as a let constant to the underlying content view (where it is used, as per my answer above). The parent container would then look something like this:

    @State private var scale = false
    var body: some View {
        SideMenuControllerView(
            scale: $scale,
            content: OrangeView(path: .constant([]), scale: scale)
        )
    }