1

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)
    }
}
user1960279
  • 494
  • 1
  • 8
  • 24

1 Answers1

1

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.

keyboard avoidance

Yrb
  • 8,103
  • 2
  • 14
  • 44
  • ios 14.0 or later. This code is not working for me. – user1960279 Nov 02 '21 at 21:58
  • Please define "not working". Everything worked on the simulator, Xcode 13.1 iOS 15 – Yrb Nov 02 '21 at 22:28
  • not working means, card is not moving up. – user1960279 Nov 03 '21 at 10:06
  • I repasted the code and added a gif. – Yrb Nov 03 '21 at 11:57
  • yes, now it is working for me too. but getting a warning in the console. – user1960279 Nov 03 '21 at 13:20
  • Class _PathPoint is implemented in both /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore (0x12bd96a78) and /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/TextInputUI.framework/TextInputUI (0x12f0668b0). One of the two will be used. Which one is undefined. – user1960279 Nov 03 '21 at 13:21
  • Class _PointQueue is implemented in both /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore (0x12bd96a50) /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/TextInputUI.framework/TextInputUI (0x12f0668d8). One of the two will be used. Which one is undefined. – user1960279 Nov 03 '21 at 13:21
  • That is [Log Noise](https://developer.apple.com/forums/thread/115461). – Yrb Nov 03 '21 at 13:55