SwiftUI 键盘遮挡布局?两种方案轻松解决!
2025-01-06 18:30:17
键盘遮挡 SwiftUI 布局问题解决方案
当应用界面出现键盘时,经常会发生布局被遮挡或变形的情况。这是因为键盘的出现会影响视图的尺寸,特别是在使用 ScrollView
或其他依赖可用空间的视图时。本文将介绍一些常见的解决方法,并提供相应的代码示例,帮助你避免这类问题。
问题分析
在SwiftUI中,键盘默认会覆盖一部分屏幕空间,尤其当屏幕较小时,这个问题会更加明显。 ScrollView
的内容通常会因为键盘的出现而被压缩,或者其他固定位置的元素会被推挤变形,导致视觉体验下降。
从代码示例中可以看出,应用使用 ScrollView
来包装内容,但并未采取额外的措施来处理键盘的出现。ignoresSafeArea(.keyboard, edges: .bottom)
只是阻止了安全区域在底部对布局的干扰, 并未实际解决布局错乱问题。因此当文本输入框获得焦点,弹出键盘时,整个视图向上偏移导致底部布局被遮挡。
解决方案
方案一:使用 GeometryReader
计算可用空间
使用 GeometryReader
可以获取当前视图的大小和位置,据此动态调整布局。这是一种较为灵活的方案,可以在代码层面控制布局的适配。
原理:
通过GeometryReader
读取屏幕大小,进而得到在键盘弹起后可以被用于展示布局的区域,从而动态调整ScrollView
内容的高度。这样就可以确保视图不会被键盘完全覆盖。
步骤:
- 使用
GeometryReader
包裹整个ZStack
,以便可以访问视图尺寸。 - 获取可用高度,计算
ScrollView
可以显示的最大高度。 - 动态调整
VStack
高度,使其小于等于计算得到的最大可用高度,从而确保内容可见。
代码示例:
struct RegisterUserView: View {
//...省略已有代码
var body: some View {
NavigationView {
GeometryReader { geometry in
ZStack {
Image(backgroundImage)
.resizable()
.aspectRatio(contentMode: .fill)
.ignoresSafeArea()
ScrollView {
VStack(spacing: 20) {
Text("Neuer Account")
.foregroundColor(.black)
.font(.custom("Avenir", size: 40))
.fontWeight(.bold)
VStack(alignment: .leading, spacing: 15) {
TextField("Dein Name", text: $username)
.padding()
.background(Color.white.opacity(0.7))
.cornerRadius(10)
.font(.custom("Avenir", size: 18))
.padding(.horizontal, 40)
TextField("E-Mail", text: $email)
.padding()
.background(Color.white.opacity(0.7))
.cornerRadius(10)
.font(.custom("Avenir", size: 18))
.padding(.horizontal, 40)
SecureField("Passwort", text: $password)
.padding()
.background(Color.white.opacity(0.7))
.cornerRadius(10)
.font(.custom("Avenir", size: 18))
.padding(.horizontal, 40)
SecureField("Passwort bestätigen", text: $confirmPassword)
.padding()
.background(Color.white.opacity(0.7))
.cornerRadius(10)
.font(.custom("Avenir", size: 18))
.padding(.horizontal, 40)
HStack {
CheckboxView(isChecked: $isTermsAccepted)
Text("Ich akzeptiere die AGBs und die Datenschutzbedingungen")
.foregroundColor(.white)
.font(.custom("Avenir", size: 18))
}
.padding(.horizontal, 20)
.padding(.vertical, 20)
}
.padding(.horizontal)
Button("Registrieren") {
// Registrierungslogik hier einfügen
}
.padding()
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(10)
.disabled(!isTermsAccepted)
}
.padding()
}
}.frame(height: geometry.size.height) // 关键:固定ScrollView的内容高度
.ignoresSafeArea(.keyboard, edges: .bottom)
}
}
.navigationBarBackButtonHidden(true)
}
}
安全性建议: GeometryReader
依赖于父视图提供的几何尺寸,如果父视图尺寸不稳定,布局可能仍会出现问题。 使用时,务必考虑其依赖性。
方案二:利用 KeyboardReadable
扩展
创建一个能够检测键盘状态的 ViewModifier
,并根据键盘状态来调整视图布局。
原理:
通过订阅 UIResponder.keyboardWillChangeFrameNotification
和 UIResponder.keyboardWillHideNotification
通知,动态获取键盘高度,从而动态调整界面布局,防止遮挡。
步骤:
- 创建
KeyboardReadable
协议,让View
遵守, 添加一个能够表示键盘高度的属性。 - 实现
KeyboardReadableModifier
,该修改器会监听键盘通知并更新键盘高度。 - 创建
keyboardHeight
环境变量,用于在其他View
中获取键盘高度。 - 在
RootView
添加environmentObject
传递。 - 视图内使用该扩展,通过键盘高度调整
padding
等,规避遮挡。
代码示例:
import SwiftUI
import Combine
protocol KeyboardReadable {
var keyboardHeight: CGFloat { get }
}
struct KeyboardReadableModifier: ViewModifier, KeyboardReadable {
@State private var keyboardHeightInternal: CGFloat = 0
var keyboardHeight: CGFloat {
keyboardHeightInternal
}
private var keyboardWillChangePublisher: AnyPublisher<CGRect, Never> {
NotificationCenter.default
.publisher(for: UIResponder.keyboardWillChangeFrameNotification)
.compactMap { notification in
(notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
}
.eraseToAnyPublisher()
}
private var keyboardWillHidePublisher: AnyPublisher<Void, Never> {
NotificationCenter.default
.publisher(for: UIResponder.keyboardWillHideNotification)
.map{ _ in return }
.eraseToAnyPublisher()
}
func body(content: Content) -> some View {
content
.onReceive(keyboardWillChangePublisher){ rect in
guard let currentWindow = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else {return}
let keyboardHeight = currentWindow.frame.height - rect.origin.y
self.keyboardHeightInternal = keyboardHeight
}.onReceive(keyboardWillHidePublisher){_ in
self.keyboardHeightInternal = 0
}
}
}
extension View{
func keyboardReadable() -> some View{
self.modifier(KeyboardReadableModifier())
}
}
private struct KeyboardHeightEnvironmentKey: EnvironmentKey{
static let defaultValue: CGFloat = 0
}
extension EnvironmentValues {
var keyboardHeight : CGFloat {
get { self[KeyboardHeightEnvironmentKey.self]}
set {self[KeyboardHeightEnvironmentKey.self] = newValue }
}
}
struct RegisterUserView: View {
@State private var username = ""
@State private var email = ""
@State private var password = ""
@State private var confirmPassword = ""
@State private var isTermsAccepted = false
let backgroundImage = "backgroundlogin"
@Environment(\.keyboardHeight) var keyboardHeight: CGFloat
var body: some View {
NavigationView {
ZStack {
Image(backgroundImage)
.resizable()
.aspectRatio(contentMode: .fill)
.ignoresSafeArea()
ScrollView {
VStack(spacing: 20) {
Text("Neuer Account")
.foregroundColor(.black)
.font(.custom("Avenir", size: 40))
.fontWeight(.bold)
VStack(alignment: .leading, spacing: 15) {
TextField("Dein Name", text: $username)
.padding()
.background(Color.white.opacity(0.7))
.cornerRadius(10)
.font(.custom("Avenir", size: 18))
.padding(.horizontal, 40)
TextField("E-Mail", text: $email)
.padding()
.background(Color.white.opacity(0.7))
.cornerRadius(10)
.font(.custom("Avenir", size: 18))
.padding(.horizontal, 40)
SecureField("Passwort", text: $password)
.padding()
.background(Color.white.opacity(0.7))
.cornerRadius(10)
.font(.custom("Avenir", size: 18))
.padding(.horizontal, 40)
SecureField("Passwort bestätigen", text: $confirmPassword)
.padding()
.background(Color.white.opacity(0.7))
.cornerRadius(10)
.font(.custom("Avenir", size: 18))
.padding(.horizontal, 40)
HStack {
CheckboxView(isChecked: $isTermsAccepted)
Text("Ich akzeptiere die AGBs und die Datenschutzbedingungen")
.foregroundColor(.white)
.font(.custom("Avenir", size: 18))
}
.padding(.horizontal, 20)
.padding(.vertical, 20)
}
.padding(.horizontal)
Button("Registrieren") {
// Registrierungslogik hier einfügen
}
.padding()
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(10)
.disabled(!isTermsAccepted)
}
.padding()
}
.ignoresSafeArea(.keyboard, edges: .bottom)
}.padding(.bottom, keyboardHeight)
}.keyboardReadable()
.navigationBarBackButtonHidden(true) // Behält das Ausblenden des standardmäßigen "Zurück"-Buttons bei
}
}
struct ContentView: View {
@State var keyboardHeight: CGFloat = 0
var body: some View {
RegisterUserView()
.environment(\.keyboardHeight, keyboardHeight)
}
}
struct CheckboxView: View {
@Binding var isChecked: Bool
var body: some View {
Image(systemName: isChecked ? "checkmark.square.fill" : "square")
.foregroundColor(isChecked ? .blue : .gray)
.onTapGesture {
self.isChecked.toggle()
}
}
}
struct RegisterUserView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
安全性建议: 注意 keyboardHeight
值有可能因为一些边界情况(例如键盘切换等)而出现短暂的错误,确保你的逻辑能够处理这些情况,必要时使用debounce
操作符。
总结
针对键盘遮挡SwiftUI布局的问题,存在多种解决方式。上述的 GeometryReader
方法能够灵活地根据可用空间调整视图, KeyboardReadable
扩展可以通过动态计算键盘高度,在布局调整方面更加灵活。选择合适的方案需要根据项目的具体需求和复杂程度进行考量。 此外, 及时关注 SwiftUI 的更新和社区实践,能够帮助我们获取更有效和适应性强的布局解决方案。