A Fix for The Bug Where The Keyboard Breaks Buttons in SwiftUI
More specifically, this is a fix for the bug where the position of the tap is misaligned with the actual position of a button. The end result is an impression that your buttons don’t work because when you tap directly on them, nothing happens. However if the user were to tap about half an inch above the button, it would work. This is obviously not desirable.
I captured a short video demonstrating the issue:
Here's the SwiftUI bug I keep running into where buttons get misaligned. Notice the position of the cursor after dismissing the modal. pic.twitter.com/5l5itdyR5F
— Serious Bret (@serious_bret) March 19, 2021
This bug seems to occur in projects that mix SwiftUI and UIKit code. In my case I have a UIHostingController wrapped in a UIViewController context. The view pictured above is a UIViewController with three main subviews stacked vertically: (1) the navigation bar which happens to be a UIHostingController but is irrelevant to this post, (2) a UITextView and (3) a UIHostingController containing all of the controls right above the keyboard.
The conditions that would reproduce the issue reliably are (1) Trigger the keyboard, (2) dismiss the keyboard as a result of presenting a modal and (3) dismiss the modal. Thereafter the buttons would become misaligned.
So... On to the fix...
Basically the fix is to remove then re-add a new instance of the UIHostingController to the view hierarchy every time the modal is dismissed. This forces the view back into its initial state, bypassing whatever tangled mess it contorted itself into.
First, I use a little Combine to be notified when a modal is dismissed. I will omit the code in the MainViewModel
singleton
because you can probably guess what that looks like. Basically modalDidDisappear
is a PassthroughSubject that emits when a modal
UIViewController's viewDidDisappear
method is invoked.
// Re-make the UIHostingController when the modal is dismissed.
cancellables.append(MainViewModel.inst.modalDidDisappear.sink(receiveValue: { [weak self] _ in
self?.remakeControlbar()
}))
And below is the remakeControlbar()
method. This thing I'm calling the "controlbar" is everything between the bottom of the
UITextView and the top of the keyboard.
private func remakeControlbar() {
controlbar?.removeFromParent()
controlbar?.view.removeFromSuperview()
let ctlbar = UIHostingController(rootView: ButtonBarView())
self.controlbar = ctlbar
ctlbar.view.translatesAutoresizingMaskIntoConstraints = false
controlbarContainer.addSubview(ctlbar.view)
addChild(ctlbar)
ctlbar.didMove(toParent: self)
NSLayoutConstraint.activate([
ctlbar.view.topAnchor.constraint(equalTo: controlbarContainer.topAnchor),
ctlbar.view.trailingAnchor.constraint(equalTo: controlbarContainer.trailingAnchor),
ctlbar.view.bottomAnchor.constraint(equalTo: controlbarContainer.bottomAnchor),
ctlbar.view.leadingAnchor.constraint(equalTo: controlbarContainer.leadingAnchor),
])
}
The method is simple. It removes the existing controlbar from the view hierarchy then creates a new one and re-inserts it, forcing the view back into its initial state before the keyboard appeared and screwed up the layout.
Before settling on this fix I tried a number of other approaches including @seitpete's "ignore keyboard" thing, and the referenced "ignore insets" thing but nothing else worked.
I should probably file a feedback for this, maybe post it on the developer forums. Yet every other SwiftUI-related thing ive posted has been summarily ignored by Apple so perhaps I shouldn't waste my time. Perhaps the volume of feedback they receive is simply unmanageable. It leaves me wondering if something like SwiftUI should be open-sourced. It has so much potential yet so many issues. I'll just sit here and shake my head disapprovingly which is about as effective as "filing a feedback."