Search code examples
swiftuiviewscaleswiftui-animationxcode15

Scale animation around Safe Area issue in SwiftUI


I'm attempting to resize a view, but when the view (YellowView) enlarges/shrinks to the safe area, all elements within the view experience a sudden shift (caused by the change in view size). How would you recommend addressing this issue to prevent the abrupt movement?

import SwiftUI

struct ContentView: View {
    @State private var scale = false
    var body: some View {
        GeometryReader { proxy in
            ZStack {
                YellowView()
                    .ignoresSafeArea()
                    .offset(x: 100)
                    .scaleEffect(scale ? 0.8 : 1)
                                    
                Button {
                    withAnimation(.easeInOut(duration: 3)) {
                        scale.toggle()
                    }
                } label: {
                    Text("Scale")
                }
                .frame(maxWidth: .infinity,alignment: .leading)
                .padding()
            }
        }
        
    }
}

struct YellowView: View {
    var body: some View {
        ZStack {
            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(.cyan)
        }
        .padding()
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(.yellow)
    }
    
}

#Preview {
    ContentView()
}


Solution

  • First, you should apply .ignoreSafeArea after applying the scale effect:

    YellowView()
        .offset(x: 100)
        .scaleEffect(scale ? 0.8 : 1)
        .ignoresSafeArea()
    

    After this you should see that the animation is a bit smoother, but the background fills still jump from "filling the whole safe area" to "not filling any of the safe area". This is because background tries to fill the entire safe area by default.

    To disable that, pass an empty set to the ignoresSafeAreaEdges argument.

    struct YellowView: View {
        var body: some View {
            ZStack {
                HStack {
                    Text("TopBar")
                        .frame(maxWidth: .infinity)
                        .background(.gray, ignoresSafeAreaEdges: [])
                }
                .frame(maxWidth: .infinity,maxHeight: .infinity, alignment: .top)
                
                VStack {
                    Image(systemName: "globe")
                        .imageScale(.large)
                        .foregroundStyle(.black)
                    Text("Hello, world!")
                }
                .padding()
                .background(.cyan, ignoresSafeAreaEdges: [])
            }
            .padding()
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(.yellow, ignoresSafeAreaEdges: [])
        }
        
    }
    

    Alternatively, you can use the overload of background that takes a ViewBuilder closure. This overload doesn't ignore the safe area by default. e.g.

    .background { Color.yellow }