iosswiftswiftui

Overlay view doesn't dismiss entirely in Swift UI


I'm showing an overlay to get the user's name on app launch; combined with a move from bottom transition, but the dismissing animation always stops at 90% and then disappears.

@ViewBuilder
func userNameOverlay() ->  some View {
    GroupBox {
        Image(systemName: "person.circle.fill")
            .font(.system(size: 60))
            .foregroundStyle(.colorTextPrimary)
            .padding()
        
        Text("Enter Your Name")
            .appTitle(withSpacing: true)
        
        TextField("John Doe", text: $username)
            .padding(12)
            .background(.colorBackground)
            .clipShape(.rect(cornerRadius: 16))
            .shadow(radius: 1, x: 1, y: 1)
            .padding(.bottom)
        
        Button {
            withAnimation {
                vm.showUserNameOverlay = false
            }
        } label: {
            Text("Save")
                .padding(.vertical, 10)
                .frame(maxWidth: .infinity)
                .background(.accent)
                .foregroundStyle(.colorBackground)
                .fontWeight(.bold)
                .clipShape(.rect(cornerRadius: 16))
        }
    }.padding(.horizontal)
        .groupBoxStyle(.customStyle)
        .transition(.move(edge: .bottom))
}

At first it was a vstack with an overlay, and now I've switched over to a zstack, but still not working, why is that?

            ZStack(alignment: .bottom) {
                ScrollView(showsIndicators: false) {
                    VStack {
                        main_views…
                    
                        Spacer()
                        
                    }.padding()
                        .foregroundStyle(.colorTextPrimary)
                        .blur(radius: vm.showUserNameOverlay ? 3 : 0)
                        .disabled(vm.showUserNameOverlay)
                }
                
                if vm.showUserNameOverlay {
                    Color.gray.opacity(0.4)
                        .ignoresSafeArea()
                    
                    userNameOverlay() // that’s the view I’m talking about
                        .zIndex(100)
                }
            }

and that's where it's called

video showing the issue


Solution

  • This problem is being caused by the safe area inset.

    The problem can be avoided by ignoring the bottom safe area inset for both the parent view and the overlay layer. However, this will probably mean, you will need to add bottom padding to both of the views, to keep their content out of the safe area.

    A simpler workaround is to add top padding to the overlay, equivalent to the bottom safe area inset. This way, all that remains of the view before it disappears completely is the (invisible) padding.

    The size of the inset can be found by wrapping the top-level parent view with a GeometryReader. If the top-level parent view is the ZStack then it works as follows:

    GeometryReader { geo in
        ZStack(alignment: .bottom) {
            ScrollView(showsIndicators: false) {
                // ...
            }
            if vm.showUserNameOverlay {
                Color.gray.opacity(0.4)
                    .ignoresSafeArea()
    
                userNameOverlay()
                    .padding(.top, geo.safeAreaInsets.bottom) // 👈 here
                    .zIndex(100)
            }
        }
        .animation(.easeInOut(duration: 2), value: vm.showUserNameOverlay)
    }
    

    Animation