How do you force a view controller to call preferredFocusView? - ios

So I have a view controller that has the following methods:
var viewToFocus: UIView? = nil {
didSet {
if viewToFocus != nil {
self.setNeedsFocusUpdate()
self.updateFocusIfNeeded()
}
}
}
override weak var preferredFocusedView: UIView? {
if viewToFocus != nil {
let theView = viewToFocus
viewToFocus = nil
return theView
} else {
return super.preferredFocusedView;
}
}
So essentially I can force a focus update to the view I want by setting viewToFocus to a value, which works great in most cases. However, when removing a subview this doesn't seem to work properly and preferredFocusedView is never called.
In terms of my subview, it's an overlay that covers the screen (don't ask why I didn't use a modal, there were reasons for that) and has the parent view controller as a delegate. The parent view controller has the following methods:
// Delegate of 'OverlayView'. Called BEFORE it is added as a subview
func beforeOpeningOverlay() {
focusedViewBeforeOverlay = UIScreen.mainScreen().focusedView
}
// Delegate of 'OverlayView'. Called after its close action is triggered.
func closedOverlay(overlay: OverlayView) {
if focusedViewBeforeOverlay != nil {
viewToFocus = focusedViewBeforeOverlay
focusedViewBeforeOverlay = nil
}
overlay.delegate = nil
overlay.removeFromSuperview()
}
For some reason when closedOverlay is called, and focusedViewBeforeOverlay has a valid non-nil view, it is never the next view focused on after the overlay is removed from the superview. Like I said preferredFocusedView is never called, and focus instead focuses on whatever the focus engine decides should be the next focus.
Anyone know why that might be the case? Does removing a subview not allow you to trigger a focus update?
So the order, or at least expected order, should be:
Something triggers OverlayView to be instantiated
beforeOpeningOverlay() is called, and the currently focused view is set to focusedViewBeforeOverlay. The overlay is then opened and captures focus
Something triggers the overlay view to close, calling closedOverlay()
viewToFocus = focusedViewBeforeOverlay line is called
A focus update should be called for the parent viewcontroller, calling its preferredFocusedView
preferredFocusedView should return viewToFocus which was set to focusedViewBeforeOverlay and revert focus back to the view that was focused before the overlay opens
The issue seems to be that step 5 and onwards aren't called

As Roman commented, referredFocusedView was deprecated in tvOS 10. Now you have to define your preferredFocusEnvironments.
That in your case would be something like:
override var preferredFocusEnvironments: [UIFocusEnvironment] {
if let focusEnviroment = self.viewToFocus { return [focusEnviroment] }
return super.preferredFocusEnvironments
}
You also need to call self.setNeedsFocusUpdate() for updating the focus.

Related

CollectionView Touch is now working when placed on top of tabBar Swift

I have added a collectionView on top of a UITabBar but its touch is not working.The screeshot for the tabBar and collectionView
The code is attached below, I want the collectionView to be touchable. Here quickAccessView is the UIView that contains the collectionView. For constraints I'm using snapKit
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
self.tabBar.bringSubviewToFront(quickAccessView)
}
private func setupQuickAccessView(){
print("this is tabBar's height", self.tabBar.frame.size.height)
self.tabBar.frame.size.height = 150
print("this is new tabBar's height", self.tabBar.frame.size.height)
self.tabBar.addSubview(quickAccessView)
quickAccessView.clipsToBounds = true
}
private func addQuickAccessViewConstraints(){
quickAccessView.snp.makeConstraints { make in
make.right.left.equalTo(self.tabBar.safeAreaLayoutGuide)
make.height.equalTo(76)
make.bottom.equalTo(self.tabBar.snp.bottom).offset(-80)
}
}
this is after modification that Aman told
The UITabBarController
final class MainTabBarController: UITabBarController {
private lazy var quickAccessView: QuickAccessView = .fromNib()
var quickAccessSupportedTabBar: QuickAccessSupportedTabBar {
self.tabBar as! QuickAccessSupportedTabBar // Even code is crashing here
}
// Even code is crashing here
override func viewDidLoad() {
super.viewDidLoad()
self.tabBar.backgroundColor = .white
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.view.frame = self.quickAccessView.bounds
setupUI()
}
}
extension MainTabBarController{
private func setupUI(){
setupQuickAcessView()
addQuickAcessViewConstraints()
}
}
// MARK: - Setting Up Quick Access view
extension MainTabBarController {
private func setupQuickAcessView(){
self.quickAccessSupportedTabBar.addSubview(quickAccessView)
}
private func addQuickAcessViewConstraints(){
quickAccessView.snp.makeConstraints { make in
make.left.right.equalTo(self.quickAccessSupportedTabBar.safeAreaLayoutGuide)
make.height.equalTo(66)
make.bottom.equalTo(self.quickAccessSupportedTabBar.snp.top)
}
}
}
the UItabBar and here it is throwing error and I too am confuse that how to access it and convert it to points
class QuickAccessSupportedTabBar: UITabBar {
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// if `quickAccessView` is visible, then convert `point` to its coordinate-system
// and check if it is within its bounds; if it is, then ask `quickAccessView`
// to perform the hit-test; you may skip the `isHidden` check, in-case this view
// is always present in your app; I'm assuming based on your screenshot that
// the user can dismiss / hide the `quickAccessView` using the cross icon
if !quickAccessView.isHidden {
// Convert the point to the target view's coordinate system.
// The target view isn't necessarily the immediate subview
let targetPoint = quickAccessView.convert(point, from: self)
if quickAccessView.bounds.contains(targetPoint) {
// The target view may have its view hierarchy, so call its
// hitTest method to return the right hit-test view
return quickAccessView.hitTest(targetPoint, with: event)
}
}
// else execute tabbar's default implementation
return super.hitTest(point, with: event)
}
}
I think what may be happening here is that since you've added quickAccessView as tab bar's subview, it is not accepting touches. This would be so because the tabbar's hist test will fail in this scenario.
To get around this, instead of using a UITabBar, subclass UITabBar, let us call it ToastyTabBar for reference. See the code below:
class ToastyTabBar: UITabBar {
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// if `quickAccessView` is visible, then convert `point` to its coordinate-system
// and check if it is within its bounds; if it is, then ask `quickAccessView`
// to perform the hit-test; you may skip the `isHidden` check, in-case this view
// is always present in your app; I'm assuming based on your screenshot that
// the user can dismiss / hide the `quickAccessView` using the cross icon
if !quickAccessView.isHidden {
// Convert the point to the target view's coordinate system.
// The target view isn't necessarily the immediate subview
let targetPoint = quickAccessView.convert(point, from: self)
if quickAccessView.bounds.contains(targetPoint) {
// The target view may have its view hierarchy, so call its
// hitTest method to return the right hit-test view
return quickAccessView.hitTest(targetPoint, with: event)
}
}
// else execute tabbar's default implementation
return super.hitTest(point, with: event)
}
}
Set this as the class of your tabBar (both in the storyboard and the swift file), and then see if that solves it for you.
You'll have to figure out a way to make quickAccessView accessible to the tabbar for the hit test check. I haven't advised on that above because I'm not familiar with your class hierarchy, and how and where you set stuff up, but this should be trivial.
If this solves it for you, please consider marking this as the answer, and if it does not then please share a little more info here about where you're having the problem.
Edit (for someone using a UITabBarController):
In response to your comment about "how to access UITabBar class from UITabBarController" here's how I would go about it.
I'm assuming you have a storyboard with the UITabBarController.
The first step (ignore this step if you already have a UITabBarController custom subclass) is that you need to subclass UITabBarController. Let us call this class ToastyTabBarController for reference. Set this class on the UITabBarController in your storyboard using the identity inspector pane in xcode.
The second step is to set the class of the UITabBar in your storyboard as ToastyTabBar (feel free to name it something more 'professional' 😊).
This is to be done in the same storyboard, in your UITabBarController scene itself. It will show the tabBar under your UITabBarController, and you can set the custom class on it using the identity inspector pane just like earlier.
The next step is to expose a computed property on your custom UITabBarController class, as shown below.
var toastyTabBar: ToastyTabBar {
self.tabBar as! ToastyTabBar
}
And that's it. Now you have a property on your UITabBarController subclass which is of ToastyTabBar type and you can use this new property, toastyTabBar, however you require.
Hope this helps.

How to display empty tableview by default when using UISearchController in Swift? [duplicate]

As I understand, the default behaviour of UISearchController is:
On tapping search bar, background is dimmed and 'cancel' button is shown. SearchResultsController is not shown till this point.
SearchResultsController is displayed only if search bar is not empty.
I want to display SearchResultsController even when search bar is empty but selected (i.e is case 1 above).
Simply put, instead of background dimming, I would like to show Search results.
Is there a way for doing this?
More Clarification:
I am not using UISearchController to filter results shown on the view on which it is shown, but some other unrelated results.
It will be like what facebook does on its 'News Feed'. Tapping on search bar shows search suggestions initially and then, when we start editing, it shows search results which might not be related to news feed.
You can simply implement the UISearchResultsUpdating protocol and set the results controller view to always show in updateSearchResultsForSearchController:
func updateSearchResultsForSearchController(searchController: UISearchController) {
// Always show the search result controller
searchController.searchResultsController?.view.hidden = false
// Update your search results data and reload data
..
}
This works because the method is called even when the search bar is activated, without any text.
If your searchBar is active but has no text, the underlying tableView results are shown. That's the built-in behavior, and the reason why searchResultsController is hidden for that state.
To change the behavior when search is active but not filtering, you're going to have to show the searchResultsController when it is normally still hidden.
There may be a good way to accomplish this via <UISearchResultsUpdating> and updateSearchResultsForSearchController:. If you can solve it via the protocol, that's the preferred way to go.
If that doesn't help, you're left with hacking the built-in behavior. I wouldn't recommend or rely on it, and it's going to be fragile, but here's an answer if you choose that option:
Make sure your tableViewController conforms to <UISearchControllerDelegate>, and add
self.searchController.delegate = self;
Implement willPresentSearchController:
- (void)willPresentSearchController:(UISearchController *)searchController
{
dispatch_async(dispatch_get_main_queue(), ^{
searchController.searchResultsController.view.hidden = NO;
});
}
This makes the searchResultsController visible after its UISearchController set it to hidden.
Implement didPresentSearchController:
- (void)didPresentSearchController:(UISearchController *)searchController
{
searchController.searchResultsController.view.hidden = NO;
}
For a better way to work around the built-in behavior, see malhal's answer.
Updated for iOS 13
From iOS13, we got system API support for this behaviour. You can set the property showsSearchResultsController = true
For iOS 12 and below
I am recently working on UISearchController. I want to show search history in searchResultsController when search bar is empty. So searchResultsController needs to show up whenever UISearchController gets presented.
Here, I use another solution to make the searchResultsController always visible by overriding the hidden property in a custom view.
for example, my searchResultsController is a UITableViewController. I create a VisibleTableView as a subclass of UITableView, and then change the UITableView custom class of searchResultsController to VisibleTableView in xib or storyboard. This way, my searchResultsController will never be hidden by UISearchController.
The good things here:
Easier to implement than KVO.
No delay to show searchResultsController. Flipping the hidden flag in "updateSearchResults" delegate method works, but there is a delay to show the searchResultsController.
It does't reset the hidden flag, so there is no UI gap/jumping between hidden and visible.
Swift 3 sample code:
class VisibleTableView: UITableView {
override var isHidden: Bool {
get {
return false
}
set {
// ignoring any settings
}
}
}
I have tried PetahChristian solution, the preload result did show up when we first focus the searchbar, but when we enter something then clear it, the preload results will not reappear.
I came up with another solution. We only need to add a delegate into SearchResultsController and call it when our searchController.searchBar.text is empty. Something like this:
SearchResultsController:
protocol SearchResultsViewControllerDelegate {
func reassureShowingList() -> Void
}
class FullSearchResultsViewController: UIViewController, UISearchResultsUpdating{
var delegate: SearchResultsViewControllerDelegate?
...
func updateSearchResultsForSearchController(searchController: UISearchController) {
let query = searchController.searchBar.text?.trim()
if query == nil || query!.isEmpty {
...
self.delegate?.reassureShowingList()
...
}
...
}
And in the controller contains the SearchController, we add our delegate:
self.searchResultsController.delegate = self
func reassureShowingList() {
searchController.searchResultsController!.view.hidden = false
}
With tricky things like this I recommend the sledge hammer approach! That is to detect when something tries to make it hidden and when it does, change it back. This can be done via KVO (Key Value Observing). This will work no matter what, without having to handle all the intricacies of the search bar. Sorry the code is complicated but KVO is an older style API but my code follows recommend practice. In your SearchResultsViewController put this:
static int kHidden;
#implementation SearchResultsViewController
-(void)viewDidLoad{
[super viewDidLoad];
[self.view addObserver:self
forKeyPath:#"hidden"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:&kHidden];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
// if it was our observation
if(context == &kHidden){
// if the view is hidden then make it visible.
if([[change objectForKey:NSKeyValueChangeNewKey] boolValue]){
self.view.hidden = NO;
}
}
else{
// if necessary, pass the method up the subclass hierarchy.
if([super respondsToSelector:#selector(observeValueForKeyPath:ofObject:change:context:)]){
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
}
- (void)dealloc
{
[self.view removeObserver:self forKeyPath:#"hidden"];
}
// Here have the rest of your code for the search results table.
#end
This works in all cases including if the text is cleared.
Lastly, to prevent the table doing an ugly fade to grey then to white when the search activates, use this:
self.searchController.dimsBackgroundDuringPresentation = NO;
Swift 3 Version:
If your searchResultController is not nil and you are using a separate table view controller to show the search results, then you can make that table view controller conform to UISearchResultUpdating and in the updateSearchResults function, you can simply unhide the view.
func updateSearchResults(for searchController: UISearchController) {
view.hidden = false
}
Swift 4 Version:
func updateSearchResults(for searchController: UISearchController) {
view.isHidden = false
}
What is being hidden is the search results controller's view. Therefore it is sufficient to unhide it any time it might be hidden. Simply do as follows in the search results controller:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.view.isHidden = false
}
func updateSearchResults(for searchController: UISearchController) {
self.view.isHidden = false
// ... your other code goes here ...
}
Now the results view (i.e. the table view) is always visible, even when the search bar text is empty.
By the way, the iOS Mail app behaves like this, and I assume that's how it's implemented (unless Apple has access to some secret private UISearchController setting).
[Tested in iOS 10 and iOS 11; I didn't test on any earlier system.]
The Swift 2.3 version of #malhal's approach:
class SearchResultsViewController : UIViewController {
var context = 0
override func viewDidLoad() {
super.viewDidLoad()
// Add observer
view.addObserver(self, forKeyPath: "hidden", options: [ .New, .Old ], context: &context)
}
deinit {
view.removeObserver(self, forKeyPath: "hidden")
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if context == &self.context {
if change?[NSKeyValueChangeNewKey] as? Bool == true {
view.hidden = false
}
} else {
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
}
}
}
Swift 4 version of malhals answer:
class SearchController: UISearchController {
private var viewIsHiddenObserver: NSKeyValueObservation?
override func viewDidLoad() {
super.viewDidLoad()
viewIsHiddenObserver = self.searchResultsController?.view.observe(\.hidden, changeHandler: { [weak self] (view, _) in
guard let searchController = self else {return}
if view.isHidden && searchController.searchBar.isFirstResponder {
view.isHidden = false
}
})
}
}
Please note the [weak self]. Otherwise you would introduce a retain cycle.
I think you are mistaken.
SearchResultsController only appears when there are results. This is slightly different than your interpretation.
The results are loaded manually based on the text in the search bar. So you can intercept it if the search bar is empty and return your own set of results.
If you don't want to dim the results, set the dimsBackgroundDuringPresentation property to false.
This will make sure that the underlying content is not dimmed during a search.
You will also have to make sure you return results even when the searchText is empty otherwise an empty tableview will be displayed.
I spent a lot of time with this, and ultimately the solution I went with is like #malhals's, but the amount of code is significantly reduced by using facebook's KVOController: https://github.com/facebook/KVOController . Another advantage here is that if your searchResultsController is a UINavigationController then you don't need to subclass it just to add #malhal's code.
// always show searchResultsController, even if text is empty
[self.KVOController observe:self.searchController.searchResultsController.view keyPath:#"hidden" options:NSKeyValueObservingOptionNew block:^(id observer, UIView* view, NSDictionary *change) {
if ([change[NSKeyValueChangeNewKey] boolValue] == YES) {
view.hidden = NO;
}
}];
self.searchController.dimsBackgroundDuringPresentation = NO;
The easiest way is to use ReactiveCocoa with this extension https://github.com/ColinEberhardt/ReactiveTwitterSearch/blob/master/ReactiveTwitterSearch/Util/UIKitExtensions.swift
presentViewController(sc, animated: true, completion: {
sc.searchResultsController?.view.rac_hidden.modify({ value -> Bool in
return false
})
} )
where sc is your UISearchController
I really liked Simon Wang's answer and worked with it and this is what I did and it works perfectly:
I subclass the UISearchController in my custom class:
class CustomClass: UISearchController {
override var searchResultsController: UIViewController? {
get {
let viewController = super.searchResultsController
viewController?.view.isHidden = false
return viewController
}
set {
// nothing
}
}
}
Also make sure you don't have this anywhere in your code:
self.resultsSearchController.isActive = true
resultsSearchController is my UISearchController
Simply what I was using this case
func updateSearchResults(for searchController: UISearchController) {
if let inputText = searchController.searchBar.text, !inputText.isEmpty {
self.view.isHidden = false
}
}
where self.view is a view of "searchResultsController" during initialisation of UISearchController.
var searchController = UISearchController(searchResultsController: searchResultsController)

Weird underlying gray outlined view trying to dismiss programmatically the master of UISplitViewController

I use a UISplitViewController with preferredDisplayMode = UISplitViewControllerDisplayModePrimaryOverlay and I was looking for a way to dismiss master view controller. My master contains a table view and I'd like to close it whenever I select a cell. Surprisingly UISplitViewController doesn't seem to offer a method for that (but I do see Apple Mail doing that we you select an email in portrait mode).
I found the following workaround reported here: Hiding the master view controller with UISplitViewController in iOS8 (look at phatmann answer). This works but it also creates a weird animation when it's dismissed, there's an underlying gray outlined view which is not animated together with my master's view. The problem has been reported also here: iOS Swift 2 UISplitViewController opens detail screen on master's place when on iPad/ iPhone 6+
The problem occurs only when I dismiss master with this workaround, not when I tap on the secondary so I guess UISplitViewController is not following the regular dismiss flow when you just call sendAction on the button.
I used the following code to address this problem. There might be a better way to match on the specific view that is causing the issue. That said, this code was in an app approved by Apple in April of this year. What the code does is look for a specific view of a certain type, and if found, then it makes it hidden until the animation is complete. Its somewhat future proof, since if it does't detect the special view, it does nothing. I also added some comments for adopters on where you might want to make changes.
func closePrimaryIfOpen(finalClosure fc: (() -> Void)? = nil) {
guard let
primaryNavController = viewControllers[0] as? MySpecialNavSubclass,
primaryVC = primaryNavController.topViewController as? MySpecialCalss
else { fatalError("NO Special Class?") }
// no "official" way to know if its open or not.
// The view could keep track of didAppear and willDisappear, but those are not reliable
let isOpen = primaryVC.view.frame.origin.x >= -10 // -10 because could be some slight offset when presented
if isOpen {
func findChromeViewInView(theView: UIView) -> UIView? {
var foundChrome = false
var view: UIView! = theView
var popView: UIView!
repeat {
// Mirror may bring in a lot of overhead, could use NSStringFromClass
// Also, don't match on the full class name! For sure Apple won't like that!
//print("View: ", Mirror(reflecting: view).subjectType, " frame: \(view.frame)")
if Mirror(reflecting: view).description.containsString("Popover") { // _UIPopoverView
for v in view.subviews {
//print("SV: ", Mirror(reflecting: v).subjectType, " frame: \(v.frame)")
if Mirror(reflecting: v).description.containsString("Chrome") {
foundChrome = true
popView = v
//popView.hidden = true
break
}
}
if foundChrome { break }
}
view = view.superview
} while view != nil
return popView
}
// Note: leave it as optional - Apple changes things and we don't find the view, things still work!
let chromeView = findChromeViewInView(self.view)
UIView.animateWithDuration(0.250, animations: {
chromeView?.hidden = true
self.preferredDisplayMode = .PrimaryHidden
}, completion: { Bool in
self.preferredDisplayMode = .PrimaryOverlay
chromeView?.hidden = false
if let finalClosure = fc {
finalClosure()
}
//print("SLIDER CLOSED DONE!!!")
} )
}
}

UISearchController: show results even when search bar is empty

As I understand, the default behaviour of UISearchController is:
On tapping search bar, background is dimmed and 'cancel' button is shown. SearchResultsController is not shown till this point.
SearchResultsController is displayed only if search bar is not empty.
I want to display SearchResultsController even when search bar is empty but selected (i.e is case 1 above).
Simply put, instead of background dimming, I would like to show Search results.
Is there a way for doing this?
More Clarification:
I am not using UISearchController to filter results shown on the view on which it is shown, but some other unrelated results.
It will be like what facebook does on its 'News Feed'. Tapping on search bar shows search suggestions initially and then, when we start editing, it shows search results which might not be related to news feed.
You can simply implement the UISearchResultsUpdating protocol and set the results controller view to always show in updateSearchResultsForSearchController:
func updateSearchResultsForSearchController(searchController: UISearchController) {
// Always show the search result controller
searchController.searchResultsController?.view.hidden = false
// Update your search results data and reload data
..
}
This works because the method is called even when the search bar is activated, without any text.
If your searchBar is active but has no text, the underlying tableView results are shown. That's the built-in behavior, and the reason why searchResultsController is hidden for that state.
To change the behavior when search is active but not filtering, you're going to have to show the searchResultsController when it is normally still hidden.
There may be a good way to accomplish this via <UISearchResultsUpdating> and updateSearchResultsForSearchController:. If you can solve it via the protocol, that's the preferred way to go.
If that doesn't help, you're left with hacking the built-in behavior. I wouldn't recommend or rely on it, and it's going to be fragile, but here's an answer if you choose that option:
Make sure your tableViewController conforms to <UISearchControllerDelegate>, and add
self.searchController.delegate = self;
Implement willPresentSearchController:
- (void)willPresentSearchController:(UISearchController *)searchController
{
dispatch_async(dispatch_get_main_queue(), ^{
searchController.searchResultsController.view.hidden = NO;
});
}
This makes the searchResultsController visible after its UISearchController set it to hidden.
Implement didPresentSearchController:
- (void)didPresentSearchController:(UISearchController *)searchController
{
searchController.searchResultsController.view.hidden = NO;
}
For a better way to work around the built-in behavior, see malhal's answer.
Updated for iOS 13
From iOS13, we got system API support for this behaviour. You can set the property showsSearchResultsController = true
For iOS 12 and below
I am recently working on UISearchController. I want to show search history in searchResultsController when search bar is empty. So searchResultsController needs to show up whenever UISearchController gets presented.
Here, I use another solution to make the searchResultsController always visible by overriding the hidden property in a custom view.
for example, my searchResultsController is a UITableViewController. I create a VisibleTableView as a subclass of UITableView, and then change the UITableView custom class of searchResultsController to VisibleTableView in xib or storyboard. This way, my searchResultsController will never be hidden by UISearchController.
The good things here:
Easier to implement than KVO.
No delay to show searchResultsController. Flipping the hidden flag in "updateSearchResults" delegate method works, but there is a delay to show the searchResultsController.
It does't reset the hidden flag, so there is no UI gap/jumping between hidden and visible.
Swift 3 sample code:
class VisibleTableView: UITableView {
override var isHidden: Bool {
get {
return false
}
set {
// ignoring any settings
}
}
}
I have tried PetahChristian solution, the preload result did show up when we first focus the searchbar, but when we enter something then clear it, the preload results will not reappear.
I came up with another solution. We only need to add a delegate into SearchResultsController and call it when our searchController.searchBar.text is empty. Something like this:
SearchResultsController:
protocol SearchResultsViewControllerDelegate {
func reassureShowingList() -> Void
}
class FullSearchResultsViewController: UIViewController, UISearchResultsUpdating{
var delegate: SearchResultsViewControllerDelegate?
...
func updateSearchResultsForSearchController(searchController: UISearchController) {
let query = searchController.searchBar.text?.trim()
if query == nil || query!.isEmpty {
...
self.delegate?.reassureShowingList()
...
}
...
}
And in the controller contains the SearchController, we add our delegate:
self.searchResultsController.delegate = self
func reassureShowingList() {
searchController.searchResultsController!.view.hidden = false
}
With tricky things like this I recommend the sledge hammer approach! That is to detect when something tries to make it hidden and when it does, change it back. This can be done via KVO (Key Value Observing). This will work no matter what, without having to handle all the intricacies of the search bar. Sorry the code is complicated but KVO is an older style API but my code follows recommend practice. In your SearchResultsViewController put this:
static int kHidden;
#implementation SearchResultsViewController
-(void)viewDidLoad{
[super viewDidLoad];
[self.view addObserver:self
forKeyPath:#"hidden"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:&kHidden];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
// if it was our observation
if(context == &kHidden){
// if the view is hidden then make it visible.
if([[change objectForKey:NSKeyValueChangeNewKey] boolValue]){
self.view.hidden = NO;
}
}
else{
// if necessary, pass the method up the subclass hierarchy.
if([super respondsToSelector:#selector(observeValueForKeyPath:ofObject:change:context:)]){
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
}
- (void)dealloc
{
[self.view removeObserver:self forKeyPath:#"hidden"];
}
// Here have the rest of your code for the search results table.
#end
This works in all cases including if the text is cleared.
Lastly, to prevent the table doing an ugly fade to grey then to white when the search activates, use this:
self.searchController.dimsBackgroundDuringPresentation = NO;
Swift 3 Version:
If your searchResultController is not nil and you are using a separate table view controller to show the search results, then you can make that table view controller conform to UISearchResultUpdating and in the updateSearchResults function, you can simply unhide the view.
func updateSearchResults(for searchController: UISearchController) {
view.hidden = false
}
Swift 4 Version:
func updateSearchResults(for searchController: UISearchController) {
view.isHidden = false
}
What is being hidden is the search results controller's view. Therefore it is sufficient to unhide it any time it might be hidden. Simply do as follows in the search results controller:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.view.isHidden = false
}
func updateSearchResults(for searchController: UISearchController) {
self.view.isHidden = false
// ... your other code goes here ...
}
Now the results view (i.e. the table view) is always visible, even when the search bar text is empty.
By the way, the iOS Mail app behaves like this, and I assume that's how it's implemented (unless Apple has access to some secret private UISearchController setting).
[Tested in iOS 10 and iOS 11; I didn't test on any earlier system.]
The Swift 2.3 version of #malhal's approach:
class SearchResultsViewController : UIViewController {
var context = 0
override func viewDidLoad() {
super.viewDidLoad()
// Add observer
view.addObserver(self, forKeyPath: "hidden", options: [ .New, .Old ], context: &context)
}
deinit {
view.removeObserver(self, forKeyPath: "hidden")
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if context == &self.context {
if change?[NSKeyValueChangeNewKey] as? Bool == true {
view.hidden = false
}
} else {
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
}
}
}
Swift 4 version of malhals answer:
class SearchController: UISearchController {
private var viewIsHiddenObserver: NSKeyValueObservation?
override func viewDidLoad() {
super.viewDidLoad()
viewIsHiddenObserver = self.searchResultsController?.view.observe(\.hidden, changeHandler: { [weak self] (view, _) in
guard let searchController = self else {return}
if view.isHidden && searchController.searchBar.isFirstResponder {
view.isHidden = false
}
})
}
}
Please note the [weak self]. Otherwise you would introduce a retain cycle.
I think you are mistaken.
SearchResultsController only appears when there are results. This is slightly different than your interpretation.
The results are loaded manually based on the text in the search bar. So you can intercept it if the search bar is empty and return your own set of results.
If you don't want to dim the results, set the dimsBackgroundDuringPresentation property to false.
This will make sure that the underlying content is not dimmed during a search.
You will also have to make sure you return results even when the searchText is empty otherwise an empty tableview will be displayed.
I spent a lot of time with this, and ultimately the solution I went with is like #malhals's, but the amount of code is significantly reduced by using facebook's KVOController: https://github.com/facebook/KVOController . Another advantage here is that if your searchResultsController is a UINavigationController then you don't need to subclass it just to add #malhal's code.
// always show searchResultsController, even if text is empty
[self.KVOController observe:self.searchController.searchResultsController.view keyPath:#"hidden" options:NSKeyValueObservingOptionNew block:^(id observer, UIView* view, NSDictionary *change) {
if ([change[NSKeyValueChangeNewKey] boolValue] == YES) {
view.hidden = NO;
}
}];
self.searchController.dimsBackgroundDuringPresentation = NO;
The easiest way is to use ReactiveCocoa with this extension https://github.com/ColinEberhardt/ReactiveTwitterSearch/blob/master/ReactiveTwitterSearch/Util/UIKitExtensions.swift
presentViewController(sc, animated: true, completion: {
sc.searchResultsController?.view.rac_hidden.modify({ value -> Bool in
return false
})
} )
where sc is your UISearchController
I really liked Simon Wang's answer and worked with it and this is what I did and it works perfectly:
I subclass the UISearchController in my custom class:
class CustomClass: UISearchController {
override var searchResultsController: UIViewController? {
get {
let viewController = super.searchResultsController
viewController?.view.isHidden = false
return viewController
}
set {
// nothing
}
}
}
Also make sure you don't have this anywhere in your code:
self.resultsSearchController.isActive = true
resultsSearchController is my UISearchController
Simply what I was using this case
func updateSearchResults(for searchController: UISearchController) {
if let inputText = searchController.searchBar.text, !inputText.isEmpty {
self.view.isHidden = false
}
}
where self.view is a view of "searchResultsController" during initialisation of UISearchController.
var searchController = UISearchController(searchResultsController: searchResultsController)

Fullscreen video in UIWebView interferes with UIViewController's input accessory view

I have a view which I set up as input accessory view for view controller the following way:
#IBOutlet private weak var bottomPane: UIView!
override func canBecomeFirstResponder() -> Bool {
return true
}
override var inputAccessoryView: UIView {
return bottomPane
}
Everything works just fine until I try to view YouTube video in fullscreen mode (video is loaded in UIWebView). When video enters fullscreen mode, keyboard and my input accessory view disappear (which is normal, I guess), but when I exit fullscreen mode, they do not appear. If I keep the reference to bottomPane weak, it becomes nil and application crashes, if I change it to strong, input accessory view remains hidden until the keyboard appears next time.
Can anybody explain what's going on and how to fix this?
Here's what's going on.
When user interacts with UIWebView, it becomes first responder and inputAccessoryView provided by view controller disappears (no idea why behavior in this case is different from, say, UITextField). Subclassing UIWebView and overriding inputAccessoryView property does not work (never gets called). So I block interaction with UIWebView until user loads video.
private func displayVideo(URL: String) {
if let video = Video(videoURL: URL) {
// load video in webView
webView.userInteractionEnabled = true
} else {
webView.userInteractionEnabled = false
}
}
When user loads video, the only way to detect that user has entered/exited fullscreen mode is to listen to UIWindowDidBecomeKeyNotification and UIWindowDidResignKeyNotification and detect when our window loses/gains key status:
//in view controller:
private func windowDidBecomeKey(notification: NSNotification!) {
let isCurrentWindow = (notification.object as! UIWindow) == view.window
if isCurrentWindow {
// this restores our inputAccessoryView
becomeFirstResponder()
}
}
private func windowDidResignKey(notification: NSNotification!) {
let isCurrentWindow = (notification.object as! UIWindow) == view.window
if isCurrentWindow {
// this hides our inputAccessoryView so that it does not obscure video
resignFirstResponder()
}
}
And, of course, since inputAccessoryView can be removed at some point, we should recreate it if needed:
//in view controller:
override var inputAccessoryView: UIView {
if view == nil {
// load view here
}
return view
}

Resources