3

With the help of the following, I was able to follow the button on the keyboard display. However, animation cannot be applied well.

How to show complete List when keyboard is showing up in SwiftUI

import SwiftUI
import Combine
import UIKit

class KeyboardResponder: ObservableObject {
    let willset = PassthroughSubject<CGFloat, Never>()
    private var _center: NotificationCenter
    @Published var currentHeight: CGFloat = 0
    var keyboardDuration: TimeInterval = 0

    init(center: NotificationCenter = .default) {
        _center = center
        _center.addObserver(self, selector: #selector(keyBoardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        _center.addObserver(self, selector: #selector(keyBoardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
    }

    deinit {
        _center.removeObserver(self)
    }

    @objc func keyBoardWillShow(notification: Notification) {
        if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
            currentHeight = keyboardSize.height

            guard let duration:TimeInterval = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else { return }
            keyboardDuration = duration
        }
    }

    @objc func keyBoardWillHide(notification: Notification) {
        currentHeight = 0
    }
}
import SwiftUI

struct Content: View {
    @ObservedObject var keyboard = KeyboardResponder()

    var body: some View {
        VStack {
            Text("text")

            Spacer()

            NavigationLink(destination: SubContentView()) {
                Text("Done")
            }
        }
        .padding(.bottom, keyboard.currentHeight)
        animation(Animation.easeInOut(duration: keyboard.keyboardDuration))
    }
}

enter image description here

Ika
  • 1,271
  • 1
  • 12
  • 20
  • So, what *specifically* is your issue? You have to admit, *"However, animation cannot be applied well"* is rather vague. –  Aug 31 '19 at 20:21
  • I want to move a button from bottom to top with the same animation (duration, delay, easing, etc.) that the keyboard appears from below. – Ika Sep 01 '19 at 03:48

1 Answers1

6

Your main problem, is that you are using an implicit animation. Not only it may be animating things you may not want to animate, but also, you should never apply .animation() on containers. Of the few warnings in SwiftUI's documentation, this is one of them:

Use this modifier on leaf views rather than container views. The animation applies to all child views within this view; calling animation(_:) on a container view can lead to unbounded scope.

Source: https://developer.apple.com/documentation/swiftui/view/3278508-animation

The modified code removes the implicit .animation() call and replaces it with two implicit withAnimation closures.

I also replaced keyboardFrameEndUserInfoKey with keyboardFrameEndUserInfoKey, second calls were giving useless geometry.

class KeyboardResponder: ObservableObject {
    let willset = PassthroughSubject<CGFloat, Never>()
    private var _center: NotificationCenter
    @Published var currentHeight: CGFloat = 0
    var keyboardDuration: TimeInterval = 0

    init(center: NotificationCenter = .default) {
        _center = center
        _center.addObserver(self, selector: #selector(keyBoardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        _center.addObserver(self, selector: #selector(keyBoardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
    }

    deinit {
        _center.removeObserver(self)
    }

    @objc func keyBoardWillShow(notification: Notification) {
        if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {

            guard let duration:TimeInterval = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else { return }
            keyboardDuration = duration

            withAnimation(.easeInOut(duration: duration)) {
                self.currentHeight = keyboardSize.height
            }

        }
    }

    @objc func keyBoardWillHide(notification: Notification) {
        guard let duration:TimeInterval = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else { return }

        withAnimation(.easeInOut(duration: duration)) {
            currentHeight = 0
        }
    }
}


struct ContentView: View {
    @ObservedObject var keyboard = KeyboardResponder()

    var body: some View {

        return VStack {
            Text("text \(keyboard.currentHeight)")

            TextField("Enter text", text: .constant(""))
            Spacer()

            NavigationLink(destination: Text("SubContentView()")) {
                Text("Done")
            }
        }
        .padding(.bottom, keyboard.currentHeight)
//        .animation(Animation.easeInOut(duration: keyboard.keyboardDuration))
    }
}
kontiki
  • 37,663
  • 13
  • 111
  • 125
  • Thank you! I fixed [it](https://i.stack.imgur.com/CmZg9.gif), but the button movement is slower than the keyboard. – Ika Sep 01 '19 at 07:34
  • I hardcoded the curve, maybe you should get that value too (`keyboardAnimationCurveUserInfoKey`: https://developer.apple.com/documentation/uikit/uiviewanimationcurve?language=objc – kontiki Sep 01 '19 at 07:38
  • Very nice! :+1: – Jacob Relkin Oct 16 '19 at 09:29
  • 4
    @kontiki Value for `keyboardAnimationCurveUserInfoKey` is 7. Apparently, they use an undocumented curve for keyboard animation. We can initialise the undocumented `UIView.AnimationCurve` value with the raw value of 7, but that curve can only be used in `UIKit`, not in `SwiftUI`. Unless I am missing something, it is not currently possible to properly follow keyboard animation in `SwiftUI`. – Ivica M. Nov 09 '19 at 15:11