最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

swiftui - Spacer pushes items off screen on the iPad - Stack Overflow

programmeradmin5浏览0评论

I have the following View for a SwiftUI app:

struct WelcomeView: View {
    @State private var showSignIn = false
    @ObservedObject var appState: AppState

    init(appState: AppState) {
        self.appState = appState
    }

    var body: some View {
        ZStack {
            // Background image layer with overlay
            Image("bkgphoto")
                .resizable()
                .aspectRatio(contentMode: .fill)
                .edgesIgnoringSafeArea(.all)
        
            // Main content elements
            VStack(spacing: 30) {
                // Welcome text
                VStack(spacing: 0) {
                    Text("WELCOME TO")
                        .foregroundColor(.white)
                    Image("logo")
                        .padding(.top, 10)
                }
                .padding(.top, 60)
                
                Spacer()
                
                // Sign up and login buttons
                VStack(spacing: 16) {
                    Button(action: {
                        // Sign up action
                    }) {
                        Text("SIGN UP")
                            .foregroundColor(.white)
                            .frame(maxWidth: .infinity)
                            .padding(.vertical, 12)
                            .background(Color.black)
                            .cornerRadius(4)
                    }
                    
                    Button(action: {
                        showSignIn = true
                    }) {
                        Text("SIGN IN")
                            .foregroundColor(.white)
                            .frame(maxWidth: .infinity)
                            .padding(.vertical, 12)
                            .background(Color.black)
                            .cornerRadius(4)
                    }
                    .fullScreenCover(isPresented: $showSignIn, onDismiss: {
                        appState.checkAuthenticationStatus()
                    }) {
                        SignInView()
                            .environmentObject(appState)
                    }
                    
                    // Terms text
                    VStack {
                        Text("By continuing, you agree to our ") +
                            Text("Terms of Service").fontWeight(.bold).underline() +
                            Text(" and acknowledge that you have read our ") +
                            Text("Privacy Policy").fontWeight(.bold).underline() +
                            Text(".")
                    }
                    .foregroundColor(.black)
                    .multilineTextAlignment(.center)
                }
                .padding(.horizontal, 20)
                .padding(.bottom, 40)
            }
            .padding(.top, 40)
        }
    }
}

The problem is that the Spacer between the 2 VStacks is pushing them off screen on the iPad. On the iPhone everything looks fine: the welcome logo at the top and the buttons and disclaimer at the bottom. If I remove the Spacer, then everything moves to the center. I'm fairly new to SwiftUI but isn't this a fairly simple layout? VStack/Spacer/Vstack. I don't understand why it does not work on the iPad.

I have the following View for a SwiftUI app:

struct WelcomeView: View {
    @State private var showSignIn = false
    @ObservedObject var appState: AppState

    init(appState: AppState) {
        self.appState = appState
    }

    var body: some View {
        ZStack {
            // Background image layer with overlay
            Image("bkgphoto")
                .resizable()
                .aspectRatio(contentMode: .fill)
                .edgesIgnoringSafeArea(.all)
        
            // Main content elements
            VStack(spacing: 30) {
                // Welcome text
                VStack(spacing: 0) {
                    Text("WELCOME TO")
                        .foregroundColor(.white)
                    Image("logo")
                        .padding(.top, 10)
                }
                .padding(.top, 60)
                
                Spacer()
                
                // Sign up and login buttons
                VStack(spacing: 16) {
                    Button(action: {
                        // Sign up action
                    }) {
                        Text("SIGN UP")
                            .foregroundColor(.white)
                            .frame(maxWidth: .infinity)
                            .padding(.vertical, 12)
                            .background(Color.black)
                            .cornerRadius(4)
                    }
                    
                    Button(action: {
                        showSignIn = true
                    }) {
                        Text("SIGN IN")
                            .foregroundColor(.white)
                            .frame(maxWidth: .infinity)
                            .padding(.vertical, 12)
                            .background(Color.black)
                            .cornerRadius(4)
                    }
                    .fullScreenCover(isPresented: $showSignIn, onDismiss: {
                        appState.checkAuthenticationStatus()
                    }) {
                        SignInView()
                            .environmentObject(appState)
                    }
                    
                    // Terms text
                    VStack {
                        Text("By continuing, you agree to our ") +
                            Text("Terms of Service").fontWeight(.bold).underline() +
                            Text(" and acknowledge that you have read our ") +
                            Text("Privacy Policy").fontWeight(.bold).underline() +
                            Text(".")
                    }
                    .foregroundColor(.black)
                    .multilineTextAlignment(.center)
                }
                .padding(.horizontal, 20)
                .padding(.bottom, 40)
            }
            .padding(.top, 40)
        }
    }
}

The problem is that the Spacer between the 2 VStacks is pushing them off screen on the iPad. On the iPhone everything looks fine: the welcome logo at the top and the buttons and disclaimer at the bottom. If I remove the Spacer, then everything moves to the center. I'm fairly new to SwiftUI but isn't this a fairly simple layout? VStack/Spacer/Vstack. I don't understand why it does not work on the iPad.

Share Improve this question edited Mar 8 at 15:25 koen 5,7537 gold badges58 silver badges102 bronze badges asked Mar 8 at 14:26 cesarcarloscesarcarlos 1,4491 gold badge15 silver badges42 bronze badges 3
  • This may be because the first image is being scaled to fill, which upsets the layout. See the answer to Why doesn't my background image fill the entire screen consistently in SwiftUI? for a different way of showing it. – Benzy Neez Commented Mar 8 at 14:50
  • @cesarcarlos You should include screenshots so that everyone is clear on what the issue is. On my end, it doesn't look right on iPhone either because the disclaimer is only partially showing and it's only one line. – Andrei G. Commented Mar 9 at 17:23
  • @cesarcarlos I've provided an answer to your question below. Please take a moment to review it and accept it or reply as needed. – Andrei G. Commented Mar 18 at 19:14
Add a comment  | 

1 Answer 1

Reset to default 1

This is a fairly simple layout if done right, otherwise it's just a problematic layout, as you can see.

The issue here is that your background image Image("bkgphoto") is inside the ZStack and your ZStack doesn't have any constraints for its frame. So the background image scales to fill, affecting the size of the ZStack, which in turn affects how all other views fit in it.

This can be fixed in a number of ways (like setting max sizes for the image or the ZStack), but to avoid this issue, I suggest moving the background image where it belongs - in the ZStack's background modifier.

ZStack {

// content here...
}
.background {
     Image("bkgphoto")
        .resizable()
        .aspectRatio(contentMode: .fill)
    .ignoresSafeArea(edges: .all)
}

//Note: the above is not proper - see below

This will prevent the ZStack from stretching and affecting the other views, however, it's still not quite proper, because the background image will scale to fill relative to the .topLeading edge/anchor of the ZStack.

Depending on your image, this may be more or less obvious, but it will be especially obvious with image that may have a central focus point that will not be centered. To control how the image scales and its alignment, it needs a frame to use as reference, or a constraining parent. Basically, you want to ensure the image scales within the size of the screen/device.

To use a frame, you could use the bounds of the screen to set the max width and height:

//ZStack background
.background {
    Image("bkgphoto")
        .resizable()
        .aspectRatio(contentMode: .fill)
        .frame(maxWidth: UIScreen.main.bounds.width, maxHeight: UIScreen.main.bounds.height, alignment: .top) // <- set alignment as needed for the image
        .ignoresSafeArea(edges: .all)
}

Or, you could wrap the image in a GeometryReader:

//ZStack background
.background {
    GeometryReader { geo in
        Image("bkgphoto")
            .resizable()
            .aspectRatio(contentMode: .fill)
            .frame(maxWidth: geo.size.width, maxHeight: geo.size.height, alignment: .top)
    }
    .ignoresSafeArea(edges: .all) // <- note this was moved here to allow the geo reader to stretch into the safe areas
}

That should take care of the layout issues due to the background image.

As for the rest of the elements to make it simpler and flow better, I suggest taking advantage of the ZStack and instead of using a VStack with a spacer, use two separate VStacks aligned to the top and bottom, respectively.

You do that by giving each VStack a frame to ensure it stretches to fill the screen and then align its contents as needed within its frame. Since they're both in a ZStack, they will be layered one over the other, but their content will grow as needed from the top or bottom without pushing each other off the screen if the sizes don't add up. This is important if you consider that people may use various text size settings, which will definitely affect layout.

ZStack {
    //Top stack
    VStack(spacing: 10) {
        // logo content...
    }
    .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)

    //Bottom stack
    VStack(spacing: 16) {
        // sign up content...
    }
    .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
}

See the full code below and give the toolbar buttons a try.

Full code:

import SwiftUI

struct WelcomeView: View {
    
    //State values
    @State private var showBorders = false
    @State private var blurBackground = false

    //Body
    var body: some View {
        ZStack {
            
            //MARK: - Top/Logo stack

            VStack(spacing: 10) {
                Text("WELCOME TO")
                Image(systemName: "applelogo") // <- set logo image here
                    .font(.system(size: 100))
            }
            .foregroundStyle(.white)
            .border(showBorders ? .yellow : .clear)
            
            //Align the VStack to the top within the ZStack
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)

            //MARK: - Bottom stack
            
            // Sign up and login buttons
            VStack(spacing: 16) {
                Group {
                    Button {
                        // Sign up action
                    } label: {
                        Text("SIGN UP")
                            .padding(.vertical, 12)
                            .frame(maxWidth: .infinity)
                    }
                    
                    Button {
                        //action...
                    } label: {
                        Text("SIGN IN")
                            .padding(.vertical, 12)
                            .frame(maxWidth: .infinity)
                    }
                }
                .buttonStyle(.borderedProminent) // <- use this and .tint instead of .foregroundStyle and .background inside the button label
                .tint(.black)
                
                // Terms text
                VStack {
                    Text("By continuing, you agree to our ") +
                    Text("Terms of Service").fontWeight(.bold).underline() +
                    Text(" and acknowledge that you have read our ") +
                    Text("Privacy Policy").fontWeight(.bold).underline() +
                    Text(".")
                }
                .foregroundStyle(.black)
                .multilineTextAlignment(.center)
            }
            .border(showBorders ? .pink : .clear)
            
            //Align the VStack to the bottom within the ZStack
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)

        }
        
        //Padding for the wrapping ZStack
        .padding(.horizontal, 20)
        .padding(.vertical, 40)
        
        //Stretch the ZStack to fill the screen
        .containerRelativeFrame([.horizontal, .vertical])
        
        //Background
        .background {
            Image("bgr") // <- set background image here
                .resizable()
                .scaleEffect( blurBackground ? 1.1 : 1, anchor: .center)
                .aspectRatio(contentMode: .fill)
                .frame(maxWidth: UIScreen.main.bounds.width, maxHeight: UIScreen.main.bounds.height, alignment: .top)
                .border(showBorders ? .green : .clear)
                .blur(radius: blurBackground ? 10 : 0)
                .ignoresSafeArea(edges: .all)
        }
        
        //MARK: - Toolbar
        
        //Toolbar for helper buttons
        .toolbar {
            ToolbarItem(placement: .primaryAction) {
                Button {
                    withAnimation {
                        showBorders.toggle()
                    }
                } label: {
                    Image(systemName: "square.3.layers.3d.down.forward")
                }
            }
            
            ToolbarItem(placement: .topBarLeading) {
                Button {
                    withAnimation(.smooth(duration: 1.5)) {
                        blurBackground.toggle()
                    }
                } label: {
                    Image(systemName: "camera.aperture")
                }
            }
            
        }
        .border(showBorders ? .blue : .clear)
    }
}

//Preview
#Preview {
    NavigationStack {
        WelcomeView()
    }
}

Note: If you're new to SwiftUI and you're not working on an old app that targets older iOS versions, you may want to look up some new reference material, as your code uses several deprecated modifiers:

.foregroundColor(.white) // <- DEPRECATED - use .foregroundStyle()
.cornerRadius(4) // <- DEPRECATED - use .background(Color.black, in: RoundedRectangle(cornerRadius: 4)) or .clipShape(RoundedRectangle(cornerRadius: 4))
.edgesIgnoringSafeArea(.all) // <- DEPRECATED - use .ignoresSafeArea()

@ObservedObject var appState: AppState // < not deprecated, but outdated. Should use @Observable framework

Bonus Tip:

Instead of setting .foregroundColor(), .background() and .cornerRadius inside the label of each button, group them and use a .buttonStyle() and .tint() to achieve an even more polished look with far less code:

Group {
    Button {
            // Sign up action
    } label: {
            Text("SIGN UP")
        .padding(.vertical, 12)
        .frame(maxWidth: .infinity)
    }
    
    Button {
            //action...
    } label: {
            Text("SIGN IN")
        .padding(.vertical, 12)
        .frame(maxWidth: .infinity)
    }
}
.buttonStyle(.borderedProminent) 
.tint(.black)
发布评论

评论列表(0)

  1. 暂无评论