-
Notifications
You must be signed in to change notification settings - Fork 37
Re-pad the view on re-render while keyboard continues to show #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,24 +12,38 @@ import Combine | |
/// Note that the `KeyboardAdaptive` modifier wraps your view in a `GeometryReader`, | ||
/// which attempts to fill all the available space, potentially increasing content view size. | ||
struct KeyboardAdaptive: ViewModifier { | ||
private let rerender: AnyPublisher<Any, Never> | ||
@State private var bottomPadding: CGFloat = 0 | ||
|
||
|
||
init(rerender: AnyPublisher<Any, Never>? = nil) { | ||
var publishers = [Publishers.keyboardHeight.map { arg -> Any in arg }.eraseToAnyPublisher()] | ||
if let rerender = rerender { | ||
publishers.append(rerender) | ||
} | ||
self.rerender = Publishers.MergeMany(publishers).eraseToAnyPublisher() | ||
} | ||
|
||
private func bottomPadding(forGeometry geometry: GeometryProxy) -> CGFloat { | ||
let keyboardTop = geometry.frame(in: .global).height - Publishers.keyboardHeight.value | ||
var focusedTextInputBottom = UIResponder.currentFirstResponder?.globalFrame?.maxY ?? 0 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be really nice if we could just observe the frame of this translating, that's the real condition that should prompt us to re-pad, without the client having to do anything. Maybe something using KVO? |
||
focusedTextInputBottom += bottomPadding / 2 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm dividing by 2 and multiplying by 2 here for reasons described here. My keyboard is around 500pt tall, like I mention in the comments there. |
||
return 2 * max(0, focusedTextInputBottom - keyboardTop - geometry.safeAreaInsets.bottom) | ||
} | ||
|
||
func body(content: Content) -> some View { | ||
GeometryReader { geometry in | ||
content | ||
.padding(.bottom, self.bottomPadding) | ||
.onReceive(Publishers.keyboardHeight) { keyboardHeight in | ||
let keyboardTop = geometry.frame(in: .global).height - keyboardHeight | ||
let focusedTextInputBottom = UIResponder.currentFirstResponder?.globalFrame?.maxY ?? 0 | ||
self.bottomPadding = max(0, focusedTextInputBottom - keyboardTop - geometry.safeAreaInsets.bottom) | ||
} | ||
.animation(.easeOut(duration: 0.16)) | ||
.onReceive(self.rerender, perform: { _ in | ||
self.bottomPadding = self.bottomPadding(forGeometry: geometry) | ||
}) | ||
.animation(.easeOut(duration: 0.16)) | ||
} | ||
} | ||
} | ||
|
||
extension View { | ||
func keyboardAdaptive() -> some View { | ||
ModifiedContent(content: self, modifier: KeyboardAdaptive()) | ||
func keyboardAdaptive(rerender: AnyPublisher<Any, Never>? = nil) -> some View { | ||
ModifiedContent(content: self, modifier: KeyboardAdaptive(rerender: rerender)) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,16 +9,22 @@ | |
import Combine | ||
import UIKit | ||
|
||
fileprivate var keyboardUpdates: AnyCancellable? | ||
fileprivate let keyboardHeightPublisher: CurrentValueSubject<CGFloat, Never> = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We change how we're publishing the keyboard height so that I tried caching the keyboard height on the |
||
let subject = CurrentValueSubject<CGFloat, Never>(0) | ||
|
||
let willShow = NotificationCenter.default.publisher(for: UIApplication.keyboardWillShowNotification) | ||
.map { $0.keyboardHeight } | ||
let willHide = NotificationCenter.default.publisher(for: UIApplication.keyboardWillHideNotification) | ||
.map { _ in CGFloat(0) } | ||
keyboardUpdates = Publishers.MergeMany(willShow, willHide).assign(to: \.value, on: subject) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be really slick to convert this publisher into a |
||
|
||
return subject | ||
}() | ||
|
||
extension Publishers { | ||
static var keyboardHeight: AnyPublisher<CGFloat, Never> { | ||
let willShow = NotificationCenter.default.publisher(for: UIApplication.keyboardWillShowNotification) | ||
.map { $0.keyboardHeight } | ||
|
||
let willHide = NotificationCenter.default.publisher(for: UIApplication.keyboardWillHideNotification) | ||
.map { _ in CGFloat(0) } | ||
|
||
return MergeMany(willShow, willHide) | ||
.eraseToAnyPublisher() | ||
static var keyboardHeight: CurrentValueSubject<CGFloat, Never> { | ||
keyboardHeightPublisher | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Futzing with the types is really frustrating here considering I don't even care what the values are when
self.rerender
fires. Ideally, I wouldn't have toeraseToAnyPublisher
normap
this to convert it to<Any, Never>
, and the client wouldn't have to do likewise when passing in their publisher, but I'm not sure how to type things to accomplish that.