0

我是 SwiftUI 的新手,并且有一个关于如何设置相对于屏幕安全区域的 VStack 的问题。

我目前正在编写一个应用程序,该应用程序将在登录屏幕上有一个单一的登录/注册按钮。这个想法是,如果在登录屏幕中输入的电子邮件地址不存在,屏幕上将显示更多的子视图,并且屏幕现在看起来像一个注册屏幕。

我能够使用这段代码实现我想要的......

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()
        }
    }
}

如果 VStack 内视图的总高度小于安全视图区域高度,则 ContentView 中最顶部 VStack 中的子视图似乎包含在屏幕的安全视图区域内(这是我的预期)。当我向最顶部的 VStack 添加灰色背景修饰符以查看相对于手机屏幕覆盖了多少视图时,可以验证这一点。

VStack 位于安全区域内(点击查看图片)

但是,我注意到当 VStack 中的子视图超过安全区域的高度时(例如显示附加注册字段时),子视图不会被剪裁,而是溢出到安全区域之外。

VStack 溢出安全区域(点击查看图片)

是否有一个修改器可以用于最顶部的 VStack,它允许我剪裁将溢出安全区域的子视图的顶部和底部边缘?

我想在使用不同的手机预览运行我的应用程序时将其用作视觉指示器,以便我更容易看到如果 VStack 的子视图溢出安全区域,我将需要执行多少高度调整特定的 iphone 屏幕尺寸。

我尝试查找此信息,但我看到的都与我想要的相反。:)

此外,有没有更好的方法来实现 VStack 中子视图的自动调整大小,以使其适合屏幕的安全区域高度,而不是使用:

.frame(minHeight, idealHeight, maxHeight)

感谢可以提供的任何建议。谢谢。

4

0 回答 0