Related
We have the following app, where user can switch to different "page" (purple, yellow, ... colours) from side menu.
I was wondering, should the "page" be implemented as UIView, or should the "page" be implemented as UIViewController?
The pages shall responsible to
Read/ write from/ to CoreData.
Possible holding a UIPageView, which user can swipe through multiple child pages as shown in https://i.stack.imgur.com/v0oNo.gif
Holding a UICollectionView.
User can drag and move the items in the UICollectionView
User can perform various contextual action (Delete, clone, ...) on the items in UICollectionView.
Can easily port to iPad in the future.
Currently, my implementation of using UIView are as follow.
private func archive() {
if let trashView = self.trashView {
trashView.removeFromSuperview()
self.trashView = nil
}
if self.archiveView != nil {
return
}
let archiveView = ArchiveView.instanceFromNib()
self.view.addSubview(archiveView)
archiveView.translatesAutoresizingMaskIntoConstraints = false
archiveView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
archiveView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
archiveView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
archiveView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
self.archiveView = archiveView
}
private func trash() {
if let archiveView = self.archiveView {
archiveView.removeFromSuperview()
self.archiveView = nil
}
if self.trashView != nil {
return
}
let trashView = TrashView.instanceFromNib()
self.view.addSubview(trashView)
trashView.translatesAutoresizingMaskIntoConstraints = false
trashView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
trashView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
trashView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
trashView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
self.trashView = trashView
}
I notice that if I implement the "pages" using UIView, I will lost some capability of UIViewController like
viewDidLoad callback.
viewWillLoad callback.
viewDidLayoutSubviews callback.
...
However, I am not clear whether losing those capabilities will stop me from implementing a proper "page"?
May I know, should I implement those "pages" using UIView, or using UIViewController?
I would do this with UIViewController because of all the UIKit callback reasons you listed in the question already.
I assume that you have a UINavigationController instance that's set as window.rootViewController for your app. You have a reference to this instance using which you can easily switch between different screens.
Example
class SlideMenuViewController: UIViewController {
enum Option {
case archive
case trash
}
var onSelect: ((_ option: Option) -> Void)?
}
class ArchiveViewController: UIViewController {}
class TrashViewController: UIViewController {}
class AppNavigator {
let mainNavigationController: UINavigationController
init(navigationController: UINavigationController) {
self.mainNavigationController = navigationController
}
private lazy var slideMenuVC: SlideMenuViewController = {
let slideMenu = SlideMenuViewController()
slideMenu.onSelect = { [weak self] (option) in
self?.openScreen(for: option)
}
return slideMenu
}()
private lazy var archiveVC: ArchiveViewController = {
return ArchiveViewController()
}()
private lazy var trashVC: TrashViewController = {
return TrashViewController()
}()
func openScreen(for option: SlideMenuViewController.Option) {
let targetVC: UIViewController
switch option {
case .archive: targetVC = archiveVC
case .trash: targetVC = trashVC
}
mainNavigationController.setViewControllers([targetVC], animated: true)
}
}
Perhaps I totally misunderstood your question, but your UIView should only hold logic related to how the view itself will appear to the user. The UIView should not contain any logic related to the other views or to the model.
UIKit assumes that you use the MVC model to implement apps on the Apple platforms. This means that any code related to controlling which view has to appear and which data the view should get from the model, should be written in the ViewController.
In Xcode you have both the UICollectionViewController and the UIPageViewController to implement page swipes and dragging an dropping views.
View controllers can give the control to other view controllers to present views. The view controller also determine the data that should be presented by the view. Please, check out this article about the MVC model.
Kind regards,
MacUserT
As you can see I'm having trouble formulating the question. Let me try to explain:
I'm using a search bar in my swift ios app. In order to get a desired animation effect I put it in a vertical stack view and then animate its isHidden property. This way search bar pushes down other children of the stack view as it animates in or pulls them up as it animates out. So far so good.
I've noticed a behavior that I think is strange. Could be a bug or could be me not understanding how things work. Basically if I call search bar hiding method x times in a row I need to call search bar showing method x times before it would show. I'd expect to have to make just one call to show search bar regardless of how many times I called hiding method. The issue doesn't exist other way around: if I call search bar showing code x times I only need to call hiding method once for it to go away. This doesn't happen if I set isHidden without animating it...
Here's a sample code and a video of the issue. I'd appreciate it if someone would help me understand this behavior.
class ViewController: UIViewController {
#IBOutlet weak var searchBar: UISearchBar! {
didSet {
searchBar.isHidden = true
}
}
#IBAction func showAction(_ sender: UIButton) {
expandSearch()
}
#IBAction func hideAction(_ sender: UIButton) {
collapseSearch()
}
private func expandSearch() {
UIView.animate(withDuration: 0.3){
self.searchBar.isHidden = false
}
}
private func collapseSearch() {
UIView.animate(withDuration: 0.3){
self.searchBar.isHidden = true
}
searchBar.resignFirstResponder()
}
}
You should not call an asynchronous animation of searchbar x times, instead of I suggest you to keep its state in variable, something like isSearchBarHidden,
and check it before show/hide search bar. You could use just one method with such signature showSearchBar(show: Bool) and setting this variable there.
#IBAction func showAction(_ sender: UIButton) {
showSearchBar(true)
}
#IBAction func hideAction(_ sender: UIButton) {
showSearchBar(false)
}
private
func showSearchBar(_ show: Bool) {
guard isSearchBarHidden != show else {
return
}
UIView.animate(withDuration: 0.3, animations: {
self.searchBar.isHidden = show
}) {
self.isSearchBarHidden = show
if !show && searchBar.isFerstResponder {
searchBar.resignFirstResponder
}
}
}
private
var isSearchBarHidden: Bool = true
Also it is a good practice to check if your textView/textField/searchBar isFirstResponder before call resignFirstResponder.
Hope it will help. Good luck!
I have an application that has two UIViewcontroller embedded in a UITabBarcontroller. When I am in UIViewController-1, i would like to press a button that disables all item selection of the tab bar. My effort is below but I am not sure how to complete the code ...
When I am in the 'Folders' UIViewController I would like to disable the selection of any tab bar item:
class Folders: UIViewController, UITableViewDataSource, UITableViewDelegate{
...
// DISABLE TAB BAR ITEMS
func disable (){
let tabBarItemsArray = self.tabBarController?.tabBar.items
tabBarItemsArray[0].enabled = false // THIS BIT OF CODE IS NOT RECOGNIZED BY XCODE
}
...
}
tabBarItemsArray is optional, its type is [UITabBarItem]?.
You could initially force unwrap it: tabBarItemsArray![0], but the right way is to use if let construct:
if let tabBarItemsArray = tabBarController.tabBar.items {
tabBarItemsArray[0].isEnabled = false
}
or:
guard let tabBarItemsArray = tabBarController.tabBar.items else {
fatalError("Error")
}
let item = tabBarItemsArray[0]
item.isEnabled = false
You can do that using single line of code. Please check following code.
You can execute this from any controller.
self.navigationController?.tabBarController?.tabBar.items![0].isEnabled = false
Another way
You can define NotificationCenter observer to achieve. Please check following code. *In TabBar Controller file.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
NotificationCenter.default.addObserver(self, selector: #selector(disableTab(notification:)), name: Notification.Name("disableTab"), object: nil)
}
#objc func disableTab(notification: Notification) {
self.TabBarItem.isEnabled = false
}
Fire from anywhere as following...
NotificationCenter.default.post(name: Notification.Name("disableTab"), object: nil)
if you want to disable one tabbar item at once then this is for disabling the first one:
guard let tabbars = self.tabBar.items else {
return
}
tabbars[0].isEnabled = false
but if you want them all to be disabled at once then this is the one to be implemented:
self.tabBar.items?.map{$0.isEnabled = false}
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)
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)