0

I'm new to SwiftUI and have a question regarding how to setup a VStack relative to the safe area of the screen.

I'm currently writing an app that will have a single sign-in/sign-up button in the log-in screen. The idea is that if the email address entered in the login screen does not exist, a few more children views will be shown on the screen and the screen will now appear like a registration screen.

I was able to achieve what I wanted using this code...

import SwiftUI
import Combine

struct ContentView: View {
    @State var userLoggedIn = false
    @State var registerUser = false
    @State var saveLoginInfo = false

    @ObservedObject var user = User()
    
    var body: some View {
        
        // *** What modifier can I use for this VStack to clip subviews that exceed the screen's safe area?
        VStack(alignment: .leading) {
            // Show header image and title
            HeaderView()
            
            // Show views common to both log-in and registration screens
            CommonViews(registerUser: $registerUser, email: $user.email, password: $user.password)
            
            if !registerUser {
                // Initially show only interfaces needed for log-in
                LoginViewGroup(saveLoginInfo: $saveLoginInfo, registerUser: $registerUser, message: $user.message, signInAllowed: $user.isValid)
            } else {
                // Show user registration fields if user is new
                RegistrationScreenView(registerUser: $registerUser, message: $user.message)
            }
            
            Spacer()
            
            // Show footer message
            FooterMessage(message: $user.message)
            
        }
        .padding(.horizontal)
        // Background modifier will check the area occupied by ContentView in the screen
        .background(Color.gray)
        
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

class User: ObservableObject {
    @Published var email: String = ""
    @Published var password: String = ""
    @Published var message: String = ""
    @Published var isValid: Bool = false
    
    private var disposables: Set<AnyCancellable> = []
    
    var isEmailPasswordValid: AnyPublisher<Bool, Never> {
        Publishers.CombineLatest($email, $password)
            .dropFirst()
            .map { (email, pass) in
                if !self.isValidEmail(email) {
                    self.message = "Invalid email address"
                    return false
                }
                
                if self.password.isEmpty {
                    self.message = "Password should not be blank"
                    return false
                }
                
                self.message = ""
                return true
            
            }
            .eraseToAnyPublisher()
            
    }
    
    init() {
        isEmailPasswordValid
            .receive(on: RunLoop.main)
            .assign(to: \.isValid, on: self)
            .store(in: &disposables)
    }
    
    private func isValidEmail(_ email: String) -> Bool {
        let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
        
        let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx)
        return emailPred.evaluate(with: email)
    }
}


struct HeaderView: View {
    var body: some View {
        VStack {
            HStack {
                Spacer()
                    Image(systemName: "a.book.closed")
                    .resizable()
                    .scaledToFit()
                    .frame(height: UIScreen.main.bounds.height * 0.125)
                    .padding(.vertical)
                Spacer()
            }
            
            HStack {
                Spacer()
                //Text("Live Fit Mealkit Ordering App")
                Text("Some text underneath an image")
                    .font(.title3)
                Spacer()
            }
        }
    }
}

struct RegistrationScreenView: View {
    @State var phone: String = ""
    @State var confirmPassword: String = ""
    @Binding var registerUser: Bool
    @Binding var message: String
    
    var body: some View {
        VStack(alignment: .leading) {
            Text("Confirm Password")
                .font(.headline)
                .padding(.top, 5)
            SecureField("Re-enter password", text: $confirmPassword)
                .padding(.all, 10)
                .background(Color(.secondarySystemBackground))
            
            Text("Phone number")
                .font(.headline)
                .padding(.top, 10)
                
            TextField("e.g. +1-416-555-6789", text: $phone)
                .textContentType(.emailAddress)
                .autocapitalization(.none)
                .padding(.all, 10)
                .background(Color(.secondarySystemBackground))

            Text("Profile photo")
                .font(.headline)
                .padding(.top, 10)
            HStack {
                Image(systemName: "camera")
                    .resizable()
                    .scaledToFit()
                    .frame(width: UIScreen.main.bounds.size.width * 0.35, height: UIScreen.main.bounds.size.width * 0.35)
                    .padding(.leading, 15)
                Spacer()
                VStack {
                    Button("Use Camera", action: {print("Launch Camera app")})
                        .padding(.all)
                        .frame(width: UIScreen.main.bounds.size.width * 0.4)
                        .background(Color.blue)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                        
                    Button("Choose Photo", action: {print("Launch Photos app")})
                        .padding(.all)
                        .frame(width: UIScreen.main.bounds.size.width * 0.4)
                        .background(Color.blue)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                    
                }.padding(.trailing, 15)
            }
            
            HStack {
                Button("Register", action: {})
                    .padding(.all)
                    .frame(width: UIScreen.main.bounds.size.width * 0.5)
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(10)
                    
                Spacer()
                
                Button("Cancel", action: {
                    registerUser.toggle()
                    message = ""
                })
                    .padding(.all)
                    .frame(width: UIScreen.main.bounds.size.width * 0.3)
                    .background(Color.red)
                    .foregroundColor(.white)
                    .cornerRadius(10)
            }.padding(.horizontal)
        }
    }
}

struct CommonViews: View {
    @Binding var registerUser: Bool
    @Binding var email: String
    @Binding var password: String
    var body: some View {
        VStack {
            Text("Email")
                .font(.headline)
                .padding(.top, registerUser ? 10 : 20)
            
            TextField("Enter email address", text: $email)
                .textContentType(.emailAddress)
                .autocapitalization(.none)
                .padding(.all, 10)
                .background(Color(.secondarySystemBackground))
            
            Text("Password")
                .font(.headline)
                .padding(.top, registerUser ? 5 : 10)
            SecureField("Enter password", text: $password)
                .padding(.all, 10)
                .background(Color(.secondarySystemBackground))
        }
    }
}



struct LoginViewGroup: View {
    @Binding var saveLoginInfo: Bool
    @Binding var registerUser: Bool
    @Binding var message: String
    @Binding var signInAllowed: Bool
    var body: some View {
        VStack {
            HStack {
                Spacer()
                Button("Sign-in / Sign-up") {
                    signInButtonPressed()
                }
                .font(.title2)
                .padding(15)
                .background(Color.blue)
                .foregroundColor(.white)
                .cornerRadius(10)
                .opacity(signInAllowed ? 1.0: 0.5)
                .disabled(!signInAllowed)
                
                Spacer()
            }
            .padding(.top, 30)
            
            Toggle("Save username and password?", isOn: $saveLoginInfo)
                .padding()
        }
    }
    
    private func signInButtonPressed() {
        // **Note**:
        // The code inside this function will  check a database to see whether the user's email address exists and will decide whether to login the user (if corresponding password is correct) or display the registration view.
    
        // To simplify things, the database checking code was removed and the button action will simply just show the additional user registration fields regardless of the email input

        registerUser = true
        message = "New user registration"
    }
}

struct FooterMessage: View {
    @Binding var message: String
    var body: some View {
        HStack {
            Spacer()
            Text(message)
                .foregroundColor(.red)
                .padding(.bottom)
            Spacer()
        }
    }
}

The children views within the top most VStack in ContentView appear to be contained within the safe view area of the screen (which is what I expected) if the total height of views inside the VStack is smaller compared to the safe view area height. This can be verified when I added a gray background modifier to the top most VStack to see how much of the view is covered relative to the phone screen.

VStack falls within safe area (Click to see image)

However, I noticed that when the children views within VStack exceeds the height of the safe area (such as when the additional registration fields are shown), the children views are not clipped but spills outside of the safe area.

VStack spills out of safe area (Click to see image)

Is there a modifier that I can use for the top most VStack that will allow me to clip the top and bottom edges of the children views that will spill out of the safe area?

I want to use this as a visual indicator when running my app using different phone previews so that it will be easier for me to see how much height resizing I will have to perform if the children views of the VStack spills out of the safe area for a specific iphone screen size.

I tried looking for this info but all I see are the opposite of what I want. :)

Also, is there a better way to implement auto-resizing of children views within a VStack to make it fit within the safe area height of the screen other than using:

.frame(minHeight, idealHeight, maxHeight)

Appreciate any suggestions that can be provided. Thanks.

Louise C.
  • 11
  • 2
  • Try at first to narrow the problem to views layout code and update your post. – Asperi Mar 23 '21 at 20:28
  • @Asperi Thanks for replying. I extracted the subview groups to make ContentView smaller and more readable. I'm not sure if this is what you meant when you suggested to narrow the problem to views layout code. Please let me know if you mean something else. Cheers! – Louise C. Mar 23 '21 at 21:10

0 Answers0