私は 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)
提供できる提案を高く評価します。ありがとう。