diff --git a/Signal/src/ConversationSearch.swift b/Signal/src/ConversationSearch.swift index c98d21237..ebfc27fe0 100644 --- a/Signal/src/ConversationSearch.swift +++ b/Signal/src/ConversationSearch.swift @@ -30,7 +30,8 @@ public class ConversationSearchController: NSObject { let thread: TSThread - let resultsBar: SearchResultsBar = SearchResultsBar(frame: .zero) + @objc + public let resultsBar: SearchResultsBar = SearchResultsBar(frame: .zero) // MARK: Initializer @@ -130,7 +131,7 @@ protocol SearchResultsBarDelegate: AnyObject { resultSet: ConversationScreenSearchResultSet) } -class SearchResultsBar: UIToolbar { +public class SearchResultsBar: UIToolbar { weak var resultsBarDelegate: SearchResultsBarDelegate? diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 2de12e297..1156d0376 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -656,7 +656,11 @@ typedef enum : NSUInteger { - (nullable UIView *)inputAccessoryView { - return self.inputToolbar; + if (self.isShowingSearchUI) { + return self.searchController.resultsBar; + } else { + return self.inputToolbar; + } } - (void)registerCellClasses @@ -1218,7 +1222,14 @@ typedef enum : NSUInteger { // We don't have to worry about the input toolbar being visible if the inputToolbar.textView is first responder // In fact doing so would unnecessarily dismiss the keyboard which is probably not desirable and at least // a distracting animation. - if (!self.inputToolbar.isInputTextViewFirstResponder) { + BOOL shouldBecomeFirstResponder = NO; + if (self.isShowingSearchUI) { + shouldBecomeFirstResponder = !self.searchController.uiSearchController.searchBar.isFirstResponder; + } else { + shouldBecomeFirstResponder = !self.inputToolbar.isInputTextViewFirstResponder; + } + + if (shouldBecomeFirstResponder) { OWSLogDebug(@"reclaiming first responder to ensure toolbar is shown."); [self becomeFirstResponder]; } @@ -4123,8 +4134,47 @@ typedef enum : NSUInteger { - (void)showSearchUI { self.isShowingSearchUI = YES; - self.navigationItem.titleView = self.searchController.uiSearchController.searchBar; + + UIView *searchBar = self.searchController.uiSearchController.searchBar; + + // Note: setting a searchBar as the titleView causes UIKit to render the navBar + // *slightly* taller (44pt -> 56pt) + self.navigationItem.titleView = searchBar; [self updateBarButtonItems]; + + // Hack so that the ResultsBar stays on the screen when dismissing the search field + // keyboard. + // + // Details: + // + // When the search UI is activated, both the SearchField and the ConversationVC + // have the resultsBar as their inputAccessoryView. + // + // So when the SearchField is first responder, the ResultsBar is shown on top of the keyboard. + // When the ConversationVC is first responder, the ResultsBar is shown at the bottom of the + // screen. + // + // When the user swipes to dismiss the keyboard, trying to see more of the content while + // searching, we want the ResultsBar to stay at the bottom of the screen - that is, we + // want the ConversationVC to becomeFirstResponder. + // + // If the SearchField were a subview of ConversationVC.view, this would all be automatic, + // as first responder status is percolated up the responder chain via `nextResponder`, which + // basically travereses each superView, until you're at a rootView, at which point the next + // responder is the ViewController which controls that View. + // + // However, because SearchField lives in the Navbar, it's "controlled" by the + // NavigationController, not the ConversationVC. + // + // So here we stub the next responder on the navBar so that when the searchBar resigns + // first responder, the ConversationVC will be in it's responder chain - keeeping the + // ResultsBar on the bottom of the screen after dismissing the keyboard. + if (![self.navigationController.navigationBar isKindOfClass:[OWSNavigationBar class]]) { + OWSFailDebug(@"unexpected navigationController: %@", self.navigationController); + return; + } + OWSNavigationBar *navBar = (OWSNavigationBar *)self.navigationController.navigationBar; + navBar.stubbedNextResponder = self; } - (void)hideSearchUI @@ -4134,8 +4184,17 @@ typedef enum : NSUInteger { self.navigationItem.titleView = self.headerView; [self updateBarButtonItems]; + if (![self.navigationController.navigationBar isKindOfClass:[OWSNavigationBar class]]) { + OWSFailDebug(@"unexpected navigationController: %@", self.navigationController); + return; + } + OWSNavigationBar *navBar = (OWSNavigationBar *)self.navigationController.navigationBar; + OWSAssertDebug(navBar.stubbedNextResponder == self); + navBar.stubbedNextResponder = nil; + // restore first responder to VC [self becomeFirstResponder]; + [self reloadInputViews]; } #pragma mark ConversationSearchControllerDelegate diff --git a/Signal/src/util/UI Categories/UIResponder+OWS.swift b/Signal/src/util/UI Categories/UIResponder+OWS.swift index e0b817bce..ce1d96c30 100644 --- a/Signal/src/util/UI Categories/UIResponder+OWS.swift +++ b/Signal/src/util/UI Categories/UIResponder+OWS.swift @@ -1,11 +1,12 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // // Based on https://stackoverflow.com/questions/1823317/get-the-current-first-responder-without-using-a-private-api/11768282#11768282 extension UIResponder { private weak static var firstResponder: UIResponder? + @objc public class func currentFirstResponder() -> UIResponder? { firstResponder = nil diff --git a/SignalMessaging/Views/OWSNavigationBar.swift b/SignalMessaging/Views/OWSNavigationBar.swift index aae117725..1d0997460 100644 --- a/SignalMessaging/Views/OWSNavigationBar.swift +++ b/SignalMessaging/Views/OWSNavigationBar.swift @@ -55,6 +55,20 @@ public class OWSNavigationBar: UINavigationBar { object: nil) } + // MARK: FirstResponder Stubbing + + @objc + public weak var stubbedNextResponder: UIResponder? + + override public var next: UIResponder? { + if let stubbedNextResponder = self.stubbedNextResponder { + Logger.debug("returning stubbed responder") + return stubbedNextResponder + } + + return super.next + } + // MARK: Theme private func applyTheme() {