Skip to content
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

Re-pad the view on re-render while keyboard continues to show #6

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

wearhere
Copy link

The current implementation will only update the padding if the keyboard shows or hides. But what if the view re-renders, moving the first responder up or down, while the keyboard is showing?

This happens in my app, which is a tutorial for setting up a custom keyboard. My view is like this:

struct SwitchToKeyboardView: View {
    @EnvironmentObject var keyboardState: KeyboardState

    @State var dummyText = ""
    @State var dummyInputIsEditing = true
    
    @ViewBuilder
    var body: some View {
        VStack {
            if keyboardState.hasBeenOpened {
                Text("Great!").font(.title)
                Text("Searchboard is now ready")
                Text("to use in any app.")
            } else {
                Text("Tap and hold the ") + Text("Globe").bold() + Text(" icon")
                Text("Tap ") + Text("Searchboard").bold() + Text(" to switch keyboards")
            }
            // Use an invisible text view to bring up the keyboard and to
            // permit the keyboard to tell us that we're opened (see
            // `KeyboardState`). We don't use a text field since you can't
            // force those to become first responder
            // https://stackoverflow.com/a/56508132/495611 . Keep the text
            // view visible even after the user has switched, the first
            // time that they switch, so that they can play around with the
            // keyboard.
            TextView(text: $dummyText, isEditing: $dummyInputIsEditing).frame(width: 0, height: 0, alignment: .center)
        }
        .keyboardAdaptive()
    }
}

Where TextView is an instance of this. I won't get into the implementation of KeyboardState unless you like, but I have a mechanism where once my custom keyboard opens, it causes keyboardState.hasBeenOpened to become true, asynchronously with respect to the keyboard changing.

You may notice that the text above the field grows bigger when the keyboard has been opened, so the padding needs to increase. But it doesn't, because the update comes in after the keyboard has already changed.


Here's a PR that's a stab at re-rendering the ViewModifier when the keyboard height changes or when a client-provided publisher fires. In my case, that publisher is a binding to keyboardState.hasBeenOpened: I change my .keyboardAdaptive() invocation to look like

.keyboardAdaptive(rerender: keyboardState.$hasBeenOpened.map({ arg -> Any in
    arg
}).eraseToAnyPublisher())

There are a bunch of things I don't like about this approach, but I figured I'd put it up to start a conversation.

If the view re-renders, and the position of the first responder changes, while
the keyboard is showing.
@wearhere wearhere force-pushed the adjust_when_responder_moves branch from 3abff55 to 624a5de Compare May 26, 2020 23:52


init(rerender: AnyPublisher<Any, Never>? = nil) {
var publishers = [Publishers.keyboardHeight.map { arg -> Any in arg }.eraseToAnyPublisher()]
Copy link
Author

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 to eraseToAnyPublisher nor map 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.

private func bottomPadding(forGeometry geometry: GeometryProxy) -> CGFloat {
let keyboardTop = geometry.frame(in: .global).height - Publishers.keyboardHeight.value
var focusedTextInputBottom = UIResponder.currentFirstResponder?.globalFrame?.maxY ?? 0
focusedTextInputBottom += bottomPadding / 2
Copy link
Author

Choose a reason for hiding this comment

The 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.

.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)
Copy link
Author

Choose a reason for hiding this comment

The 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 CurrentValueSubject so that we didn't have to retain the return value separately, but not sure how to do that.

@@ -9,16 +9,22 @@
import Combine
import UIKit

fileprivate var keyboardUpdates: AnyCancellable?
fileprivate let keyboardHeightPublisher: CurrentValueSubject<CGFloat, Never> = {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We change how we're publishing the keyboard height so that KeyboardAdaptive can read the current value when re-rendering not in response to the keyboard changing, but rather in response to another condition.

I tried caching the keyboard height on the KeyboardAdaptive modifier e.g. in a @State property and it didn't work but rather got reset, I would assume when the view hierarchy re-renders.


private func bottomPadding(forGeometry geometry: GeometryProxy) -> CGFloat {
let keyboardTop = geometry.frame(in: .global).height - Publishers.keyboardHeight.value
var focusedTextInputBottom = UIResponder.currentFirstResponder?.globalFrame?.maxY ?? 0
Copy link
Author

Choose a reason for hiding this comment

The 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?

@wearhere wearhere marked this pull request as ready for review May 26, 2020 23:59
@wearhere wearhere changed the title Re-pad the view on re-render Re-pad the view on re-render while keyboard continues to show May 27, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant