Search code examples
swiftuikeyboardcombinepublisherviewbuilder

unable to move bottom view up on tap of TextField in swiftui?


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

Solution

  • 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