I have a bottom view with a textField. This bottom view will show up on the action of a button. But the bottom view does not move up on taps of a text field. I have added the code for this.
This is ContentView where the button is displayed.
struct ContentView: View {
@State var cardShown = false
@State var cardDismissal = false
var body: some View {
NavigationView {
ZStack {
Button(action: {
cardShown.toggle()
cardDismissal.toggle()
}, label: {
Text("Show Card")
.bold()
.foregroundColor(Color.white)
.background(Color.blue)
.frame(width: 200, height: 50)
})
BottomCard(cardShown: $cardShown, cardDismissal: $cardDismissal, height: 300, content: {
CardContent()
.padding()
})
}
}
}
}
This is the bottom card content view. This needs to be up on tap on the text field. There is a TextField. I have added keyboardAdaptive modifier to recieve the keyboard height but not working.
struct CardContent: View {
@State private var text = ""
var body: some View {
VStack {
Text("Photo Collage")
.bold()
.font(.system(size: 30))
.padding()
Text("You can create awesome photo grids and share them with all of your friends")
.font(.system(size: 18))
.multilineTextAlignment(.center)
TextField("Enter something", text: $text)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
.padding()
.keyboardAdaptive() // Apply the modifier
}
}
struct BottomCard<Content: View>: View {
let content: Content
@Binding var cardShown: Bool
@Binding var cardDismissal: Bool
let height: CGFloat
init(cardShown: Binding<Bool>, cardDismissal: Binding<Bool>, height: CGFloat, @ViewBuilder content: () -> Content) {
_cardShown = cardShown
_cardDismissal = cardDismissal
self.height = height
self.content = content()
}
var body: some View {
ZStack {
// Dimmed
GeometryReader { _ in
EmptyView()
}
.background(Color.gray.opacity(0.5))
.opacity(cardShown ? 1: 0)
.animation(Animation.easeIn, value: 0.9)
.onTapGesture {
// Dismiss
dismiss()
}
// Card
VStack {
Spacer()
VStack {
content
Button(action: {
// Dismiss
dismiss()
}, label: {
Text("Dismiss")
.foregroundColor(Color.white)
.frame(width: UIScreen.main.bounds.width/2, height: 50)
.background(Color.pink)
.cornerRadius(8)
})
.padding()
}
.background(Color(UIColor.secondarySystemBackground))
.frame(height: height)
.offset(y: (cardShown && cardShown) ? 0 : 500)
.animation(Animation.default.delay(0.2), value: 0.2)
}
}
.edgesIgnoringSafeArea(.all)
}
func dismiss() {
cardDismissal.toggle()
DispatchQueue.main.asyncAfter(deadline: .now()+0.25) {
cardShown.toggle()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
extension Publishers {
// 1.
static var keyboardHeight: AnyPublisher<CGFloat, Never> {
// 2.
let willShow = NotificationCenter.default.publisher(for: UIApplication.keyboardWillShowNotification)
.map { $0.keyboardHeight }
let willHide = NotificationCenter.default.publisher(for: UIApplication.keyboardWillHideNotification)
.map { _ in CGFloat(0) }
// 3.
return MergeMany(willShow, willHide)
.eraseToAnyPublisher()
}
}
extension Notification {
var keyboardHeight: CGFloat {
return (userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height ?? 0
}
}
struct KeyboardAdaptive: ViewModifier {
@State private var bottomPadding: CGFloat = 0
func body(content: Content) -> some View {
// 1.
GeometryReader { geometry in
content
.padding(.bottom, self.bottomPadding)
// 2.
.onReceive(Publishers.keyboardHeight) { keyboardHeight in
// 3.
let keyboardTop = geometry.frame(in: .global).height - keyboardHeight
// 4.
let focusedTextInputBottom = UIResponder.currentFirstResponder?.globalFrame?.maxY ?? 0
// 5.
self.bottomPadding = max(0, focusedTextInputBottom - keyboardTop - geometry.safeAreaInsets.bottom)
}
// 6.
.animation(.easeOut, value: 0.16)
}
}
}
extension View {
func keyboardAdaptive() -> some View {
ModifiedContent(content: self, modifier: KeyboardAdaptive())
}
}
extension UIResponder {
static var currentFirstResponder: UIResponder? {
_currentFirstResponder = nil
UIApplication.shared.sendAction(#selector(UIResponder.findFirstResponder(_:)), to: nil, from: nil, for: nil)
return _currentFirstResponder
}
private static weak var _currentFirstResponder: UIResponder?
@objc private func findFirstResponder(_ sender: Any) {
UIResponder._currentFirstResponder = self
}
var globalFrame: CGRect? {
guard let view = self as? UIView else { return nil }
return view.superview?.convert(view.frame, to: nil)
}
}
I have been playing with this a bit. First of all what system are you targeting. If it is 14 or later, keyboard avoidance is baked in and you don't need the .keyboardAdaptive()
code. But that, is not your problem. When the keyboard shows, it changes the safe area. So, regardless of whether you use the .keyboardAdaptive()
code or the baked in code, you are essentially telling the view to ignore the fact that the keyboard is on the screen. I also removed your cardDismissal
code because it is not needed.
struct CardKeyboardView: View {
@State var cardShown = false
var body: some View {
NavigationView {
ZStack {
Button(action: {
cardShown.toggle()
}, label: {
Text("Show Card")
.bold()
.foregroundColor(Color.white)
.background(Color.blue)
.frame(width: 200, height: 50)
})
BottomCard(cardShown: $cardShown, height: 300, content: {
CardContent()
.padding()
})
}
}
}
}
struct CardContent: View {
@State private var text = ""
var body: some View {
VStack {
Text("Photo Collage")
.bold()
.font(.system(size: 30))
.padding()
Text("You can create awesome photo grids and share them with all of your friends")
.font(.system(size: 18))
.multilineTextAlignment(.center)
TextField("Enter something", text: $text)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
.padding()
}
}
struct BottomCard<Content: View>: View {
let content: Content
@Binding var cardShown: Bool
let height: CGFloat
init(cardShown: Binding<Bool>, height: CGFloat, @ViewBuilder content: () -> Content) {
_cardShown = cardShown
self.height = height
self.content = content()
}
var body: some View {
ZStack {
// Dimmed
GeometryReader { _ in
EmptyView()
}
.background(Color.gray.opacity(0.5))
.opacity(cardShown ? 1: 0)
.animation(Animation.easeIn, value: 0.9)
.onTapGesture {
// Dismiss
dismiss()
}
// Card
VStack {
Spacer()
VStack {
content
Button(action: {
// Dismiss
dismiss()
}, label: {
Text("Dismiss")
.foregroundColor(Color.white)
.frame(width: UIScreen.main.bounds.width/2, height: 50)
.background(Color.pink)
.cornerRadius(8)
})
.padding()
}
.background(Color(UIColor.secondarySystemBackground))
.frame(height: height)
.offset(y: (cardShown && cardShown) ? 0 : 500)
.animation(Animation.default.delay(0.2), value: 0.2)
}
}
}
func dismiss() {
cardShown.toggle()
}
}
edit: I repasted the code and added a gif of it working.