iosswiftswiftui

Align button position in Swift UI


I have two views with basically same structures, including the same padding and spacing. The only difference is that the first "Welcome step" view uses a larger icon size of 240, while the other views use an icon size of 80. While I'm ok with the text not aligning, I’d like to align the button positions across these views. I’ve attempted adjusting padding and using spacer(), but neither approach has worked. Is there a solution to this problem?

Ideally, I would like to modify the button position in Welcom step view rather than other views.

Any help is appreciated.

struct WelcomeStepView: View {
    let onNext: () -> Void
    
    var body: some View {
        VStack(spacing: 40) {
            Spacer()
            // App Icon/Logo
            VStack(spacing: 20) {
                Image("kacardicon")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(height: 240)
                
                Text("Welcome to KaCard")
                    .font(.title)
                    .fontWeight(.bold)
                    .multilineTextAlignment(.center)

                Text("This is your smart credit card manager. To get the best experience, we'd like to set up a few features.")
                    .font(.body)
                    .foregroundColor(.secondary)
                    .multilineTextAlignment(.center)
                    .padding(.horizontal)
            }

            Spacer()

            VStack(spacing: 16) {
                Button(action: onNext) {
                    Text("Get Started")
                        .font(.headline)
                        .foregroundColor(.white)
                        .frame(maxWidth: .infinity)
                        .padding()
                        .background(Color.blue)
                        .cornerRadius(12)
                }
            }
            .padding(.horizontal, 40)
            
            Spacer(minLength: 50)
        }
        .padding()
    }
}
struct NotificationPermissionStepView: View {
    let onNext: () async -> Void
    let onSkip: () -> Void
    @State private var isLoading = false
    
    var body: some View {
        VStack(spacing: 40) {
            Spacer()
            
            VStack(spacing: 20) {
                Image(systemName: "bell.circle.fill")
                    .font(.system(size: 80))
                    .foregroundColor(.yellow)
                
                Text("Stay on Top of Your Bills")
                    .font(.title)
                    .fontWeight(.bold)
                    .multilineTextAlignment(.center)
                
                Text("We'll send you timely reminders for bill payments, annual fees, and signup bonus deadlines so you never miss an important date.")
                    .font(.body)
                    .foregroundColor(.secondary)
                    .multilineTextAlignment(.center)
                    .padding(.horizontal)
            }
            
            Spacer()
            
            VStack(spacing: 16) {
                Button(action: {
                    isLoading = true
                    Task {
                        await onNext()
                        isLoading = false
                    }
                }) {
                    HStack {
                        if isLoading {
                            ProgressView()
                                .scaleEffect(0.8)
                                .progressViewStyle(CircularProgressViewStyle(tint: .white))
                        }
                        Text(isLoading ? "Requesting..." : "Enable Notifications")
                            .font(.headline)
                    }
                    .foregroundColor(.white)
                    .frame(maxWidth: .infinity)
                    .padding()
                    .background(Color.yellow)
                    .cornerRadius(12)
                }
                .disabled(isLoading)
                
                Button("Skip for Now", action: onSkip)
                    .font(.subheadline)
                    .foregroundColor(.secondary)
            }
            .padding(.horizontal, 40)
            
            Spacer(minLength: 50)
        }
        .padding()
    }
}

Solution

  • Both your views are constructed in the same way:

    VStack(spacing: 40) {
        Spacer()
        VStack(spacing: 20) {
            // image and text here
        }
        Spacer()
        VStack(spacing: 16) {
            // button content here
        }
        Spacer(minLength: 50)
    }
    .padding()
    

    The presence of the Spacer is going to mean, the view will always use all the height available. But the excess space will be divided between the Spacer. Setting minLength on the bottom Spacer does not give you much control over how much space it consumes.

    Another complication is that the button panel in NotificationPermissionStepView includes a second button for skipping the step. If I understand correctly, you want the first button of this view to be aligned with the (only) button in WelcomeStepView.

    One way to fix the alignment is to remove the bottom Spacer and apply a suitable maximum height to the lower VStack using .frame with alignment: .top. This way,

    VStack(spacing: 16) {
        // button content here
    }
    .padding(.horizontal, 40)
    .frame(maxHeight: 100, alignment: .top) // 👈 added
    
    // Spacer(minLength: 50) // 👈 removed
    

    These changes need to be applied to both views in the same way.

    Here is how it works when using a TabView as parent for the two views:

    TabView {
        WelcomeStepView {}
        NotificationPermissionStepView {} onSkip: {}
    }
    .tabViewStyle(.page)
    

    Animation