Reputation: 5961
I'm trying to create a SwiftUI view modifier that adds bottom padding in response to the on-screen keyboard appearing or disappearing. This is the approach I'm using:
struct KeyboardAdjustingModifier: ViewModifier {
@ObservedObject private var responder = KeyboardResponder()
func body(content: Content) -> some View {
GeometryReader { proxy in
content
.padding(.bottom, self.bottomPadding(in: proxy))
}
}
func bottomPadding(in proxy: GeometryProxy) -> CGFloat {
if responder.endFrame == .zero {
return 0
} else {
return max(0, proxy.frame(in: .global).maxY - self.responder.endFrame.minY)
}
}
}
fileprivate final class KeyboardResponder: ObservableObject {
@Published var endFrame: CGRect = .zero
init(notificationCenter: NotificationCenter = .default) {
notificationCenter.addObserver(self,
selector: #selector(keyboardWillShowOrChangeFrame(notification:)),
name: UIResponder.keyboardWillShowNotification, object: nil)
notificationCenter.addObserver(self,
selector: #selector(keyboardWillShowOrChangeFrame(notification:)),
name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
notificationCenter.addObserver(self,
selector: #selector(keyboardWillHide(notification:)),
name: UIResponder.keyboardWillHideNotification, object: nil)
}
@objc private func keyboardWillShowOrChangeFrame(notification: Notification) {
withAnimation(Animation(curve: notification.animationCurve, duration: notification.duration)) {
endFrame = notification.endFrame
}
}
@objc private func keyboardWillHide(notification: Notification) {
withAnimation(Animation(curve: notification.animationCurve, duration: notification.duration)) {
endFrame = .zero
}
}
struct Info {
let curve: UIView.AnimationCurve
let duration: TimeInterval
let endFrame: CGRect
}
}
fileprivate extension Animation {
init(curve: UIView.AnimationCurve, duration: TimeInterval) {
switch curve {
case .easeInOut:
self = .easeInOut(duration: duration)
case .easeIn:
self = .easeIn(duration: duration)
case .easeOut:
self = .easeOut(duration: duration)
case .linear:
self = .linear(duration: duration)
@unknown default:
self = .easeInOut(duration: duration)
}
}
}
fileprivate extension Notification {
var animationCurve: UIView.AnimationCurve {
guard let rawValue = (userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? Int) else {
return UIView.AnimationCurve.linear
}
return UIView.AnimationCurve(rawValue: rawValue)!
}
var duration: TimeInterval {
userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval ?? 0
}
var endFrame: CGRect {
userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect ?? .zero
}
}
The problem I'm running in to is that it seems as though the SwiftUI animation curve is quite different from the keyboard's animation curve:
Does anyone know if it's possible to sync up a SwiftUI animation with UIKit like this? It's quite possible there is an error in how I'm mapping the animation curve info from the notification's userInfo
to a SwiftUI animation curve. Or is there a different approach to this problem altogether that I ought to consider?
One other thing I tried is using a UIHostingController
that handles the animation directly in UIKit. It synced up properly with the keyboard, but I ran into crashes when calling layoutIfNeeded
in my animation blocks if SwiftUI was also animating stuff at the same time.
Upvotes: 2
Views: 955
Reputation: 2579
This is what I'm using and it works almost perfectly:
public extension View {
func adaptToKeyboard() -> some View {
modifier(AdaptToSoftwareKeyboard())
}
}
public struct AdaptToSoftwareKeyboard: ViewModifier {
@State var currentHeight: CGFloat = 0
public func body(content: Content) -> some View {
content
.padding(.bottom, currentHeight)
.edgesIgnoringSafeArea(.bottom)
.animation(.spring())
.onAppear(perform: subscribeToKeyboardEvents)
}
private func subscribeToKeyboardEvents() {
NotificationCenter.Publisher(
center: NotificationCenter.default,
name: UIResponder.keyboardWillShowNotification
)
.compactMap { $0.userInfo?["UIKeyboardFrameEndUserInfoKey"] as? CGRect }
.map { $0.height }
.subscribe(Subscribers.Assign(object: self, keyPath: \.currentHeight))
NotificationCenter.Publisher(
center: NotificationCenter.default,
name: UIResponder.keyboardWillHideNotification
)
.compactMap { _ in CGFloat.zero }
.subscribe(Subscribers.Assign(object: self, keyPath: \.currentHeight))
}
}
someView()
.adaptToKeyboard()
Test it with the demo here: https://github.com/youjinp/SwiftUIKit
Upvotes: 1