I'm trying to use the standard UIRefreshControl and UISearchController on a UITableViewController. However it doesn't look like it does what it's supposed to. When refreshing, scrolling leaves the navigation bar with a big blank area, presumably where the spinner is supposed to be:
I have a sample project on GitHub. Here's how the controls are set up:
override func viewDidLoad() {
super.viewDidLoad()
let spinner = UIRefreshControl()
spinner.addTarget(self, action: #selector(refresh), for: .valueChanged)
refreshControl = spinner
searchController = UISearchController(searchResultsController: nil)
navigationItem.searchController = searchController
}
I've tried assigning the refresh control to the property on UITableView instead of the one on UITableViewController, that doesn't make a difference.
Has anyone come across the same issue?
You can use UIRefreshController in the old way, which is like this:
override func viewDidLoad() {
super.viewDidLoad()
let spinner = UIRefreshControl()
spinner.addTarget(self, action: #selector(refresh), for: .valueChanged)
self.tableView.addSubview(spinner)
searchController = UISearchController(searchResultsController: nil)
navigationItem.searchController = searchController
}
Just recreate and assign the refresh control every time the table view controller appears on screen.
If you subclassed UITableViewController:
class MyTableViewController: UITableViewController {
...
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
refreshControl = UIRefreshControl(...)
}
}
If you use view controller containment:
class MyContainerViewController: UITableViewController {
private let tableViewController = UITableViewController(style: .plain)
...
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tableViewController.refreshControl = UIRefreshControl(...)
}
}
Related
I have a tableview displaying on a main view controller, and when a row is selected a detail view controller is pushed. I have a large title for the main view controller, and a small/regular title for the detail view controller. They are embedded in a navigation controller and tab bar controller.
Before selecting a row, the main view controller title is large, and when a row is selected the detail view controller title is regular as it should be. However when I return to the main view controller from the detail view controller (via "back" button), the title on the main view controller is no longer large.
I have "prefersLargeTitles" set to true on the main view controller, and "largeTitleDisplayMode" set to never on the detail view controller.
I have tried setting "largeTitleDisplayMode" to always on the main view controller to no avail. I've also tried setting it to automatic on either view controller and seems to have no effect.
I've also tried using "viewWillAppear" and "viewWillDisappear" and setting the title in there, and while it does indeed reset the main view controller title back to large, the animation lags and isn't smooth like it normally is when transitioning from a small title to a large title.
Also I'm pretty new to coding and this is my first app I'm building without using storyboards, so the code could be a mess.
MAIN view controller code:
class HomeViewController: UIViewController {
let tableView = UITableView()
override func loadView() {
super.loadView()
view.backgroundColor = .white
self.title = "Home"
// Set large title
navigationController?.navigationBar.prefersLargeTitles = true
// Make navigation bar transparent
navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
navigationController?.navigationBar.shadowImage = UIImage()
navigationController?.navigationBar.isTranslucent = true
}
extension HomeViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let vc = DetailViewController() as DetailViewController
if indexPath.section == 0 {
vc.detailTitle = itemsTop[indexPath.row]
} else if indexPath.section == 1 {
vc.detailTitle = itemsBottom[indexPath.row]
} else {
print("Failed to load title")
}
navigationController?.pushViewController(vc, animated: true)
tableView.deselectRow(at: indexPath, animated: true)
}
}
DETAIL view controller code:
class DetailViewController: UIViewController {
var detailTitle: String?
override func viewDidLoad() {
super.viewDidLoad()
title = detailTitle
// Make nav bar transparent
navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
navigationController?.navigationBar.shadowImage = UIImage()
navigationController?.navigationBar.isTranslucent = true
// Prevent large title
navigationController?.navigationBar.prefersLargeTitles = false
}
}
try this it's working.
In 'HomeViewController'
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.navigationBar.prefersLargeTitles = true
}
In 'DetailViewController'
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.largeTitleDisplayMode = .never
}
You need to set navigationController?.navigationBar.prefersLargeTitles to true in viewWillAppear of your main view controller to let it run every time you go back to main.
You can set it to false in viewWillAppear of detail and revert it in viewWillDisappear of detail to prevent it to remain as false as well.
The code for your DetailVC:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.navigationBar.prefersLargeTitles = false
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.navigationBar.prefersLargeTitles = true
}
There are many answers in SO that provide solutions for hiding the navigation bar shadow. Those work for me except for this particular case, which I'm describing here. Therefore, this question is not a duplicate.
To test this particular case, I created a new project using the master-detail app template. In the DetailViewController -> viewDidAppear, I coded the following:
self.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
self.navigationController?.navigationBar.shadowImage = UIImage()
The above code works for a Single View App and on iPad Air 2 simulator. However, it doesn't work on the detailViewController of a master-detail app in iPhoneX simulator.
Alternatively, I also tried retrieving the subviews of the navigationBar in viewDidAppear and tried hiding the shadow (see code below). However, the subview count is zero. How could that be?
for parent in self.navigationController!.navigationBar.subviews {
for childView in parent.subviews {
if(childView is UIImageView) {
childView.removeFromSuperview()
}
}
}
Any help on this is much appreciated.
For Generic flow
You could use this setup. Say ViewController is the all other view controller class (where you want the shadow), and DetailViewController is the detail view controller class.
The thing i'm doing preserving the shadow image.
ViewController.swift
class ViewController: UIViewController {
var shadowImage: UIImage!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
shadowImage = self.navigationController?.navigationBar.shadowImage
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.navigationBar.shadowImage = shadowImage
}
}
And DetailViewController.swift
class DetailViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.navigationController?.navigationBar.shadowImage = UIImage()
}
}
Storyboard setup
Output
Note: Another neat approach would be storing the shadow within the DetailsViewController and setting it while the view is about to disappear
class DetailsViewController: UIViewController {
var shadowImage: UIImage!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
shadowImage = self.navigationController?.navigationBar.shadowImage
self.navigationController?.navigationBar.shadowImage = UIImage()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.navigationController?.navigationBar.shadowImage = shadowImage
}
}
This solution is more elegant and resulting in a clean management.
For MasterDetailFlow, Using SplitViewController
In your MasterViewControlelr.swift
override func viewWillAppear(_ animated: Bool) {
clearsSelectionOnViewWillAppear = splitViewController!.isCollapsed // placeholder code when you created the project
super.viewWillAppear(animated)
self.navigationController?.navigationBar.shadowImage = nil
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.navigationController?.navigationBar.shadowImage = UIImage()
}
In your DetailViewController.swift
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.navigationBar.shadowImage = UIImage()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.navigationController?.navigationBar.shadowImage = nil
}
Output(Master/Detail flow)
Just for context my keyboard successfully appears so that's not the problem.
I have a searchButton as my rightBarButtonItem, when pressed it modally presents a vc that contains a SearchController. When the SearchController is presented the keyboard is also presented but the keyboard appears a second late, there's like a 1 second delay before it shows itself. Basically the vc appears on the scene and then the keyboard appears afterwards, I cannot get the keyboard to appear at the same time the SearchController is presented. I was on YouTube's and Vimeo's iOS apps and when I pressed their search button the keyboard is presented with the SearchController at the same exact time, there isn't a 1 second delay.
How can I get the keyboard to present itself at the same time the SearchController is presenting itself?
button to modally present SearchController:
#objc func searchButtonTapped() {
let searchVC = SearchController()
let nav = UINavigationController(rootViewController: searchVC)
present(nav, animated: true, completion: nil)
}
SearchController:
I've already tried adding searchController.isActive = true and searchController.searchBar.becomeFirstResponder() in DispatcQeue.main in viewWillAppear and viewDidAppear and it made no difference
class SearchController: UIViewController {
var searchController: UISearchController!
override func viewDidLoad() {
super.viewDidLoad()
searchController = UISearchController(searchResultsController: nil)
searchController.delegate = self
searchController.searchBar.delegate = self
searchController.searchResultsUpdater = self
searchController.searchBar.showsCancelButton = true
searchController.searchBar.placeholder = "Search"
searchController.searchBar.returnKeyType = .search
searchController.dimsBackgroundDuringPresentation = false
searchController.hidesNavigationBarDuringPresentation = false
searchController.searchBar.sizeToFit()
searchController.searchBar.tintColor = UIColor.black
definesPresentationContext = true
navigationItem.hidesBackButton = true
navigationItem.titleView = searchController.searchBar
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
searchController.isActive = true
}
// I tried both of these searchContrller delegate methods SEPERATELY but it made no difference, there's still a 1 second delay
func presentSearchController(_ searchController: UISearchController) {
DispatchQueue.main.async {
self.searchController.searchBar.becomeFirstResponder()
}
}
func didPresentSearchController(_ searchController: UISearchController) {
DispatchQueue.main.async {
self.searchController.searchBar.becomeFirstResponder()
}
}
}
Sure there is a delay.. ones this animation is completed, then keyboard appears.
present(nav, animated: true, completion: nil)
Please try this.
It will immediately open the keyboard if you would not preset view controller with an animation but if we present view controller animation it opens keyboard after the present animation is finished.
Thanks.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
DispatchQueue.main.async {
self.searchController.searchBar.becomeFirstResponder()
}
}
I'm currently trying to develop a custom search for an iOS app.
I have managed to get the search controller appearing and the search bar appearing properly, although my only problem is that I need the back button to appear on the right of the navigation bar rather than the left, see below
(As you can see the back button is on the left but I need it to be on the right)
http://imgur.com/qLPoIfG
Here is my code:
import UIKit
class SearchTop10Controller: UITableViewController, UISearchResultsUpdating {
override func viewDidLoad() {
super.viewDidLoad()
let searchController = UISearchController(searchResultsController: self);
self.definesPresentationContext = true;
searchController.searchResultsUpdater = self;
// searchController.hidesNavigationBarDuringPresentation = true;
searchController.dimsBackgroundDuringPresentation = false;
searchController.searchBar.sizeToFit();
self.navigationItem.titleView = searchController.searchBar;
self.tableView.tableHeaderView = searchController.searchBar;
}
override func viewDidAppear(animated: Bool) {
}
func updateSearchResultsForSearchController(searchController: UISearchController) {
//do whatever with searchController here.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
You can add a back button to the right bar item like this:
let backButton : UIBarButtonItem = UIBarButtonItem(image: UIImage(named: "back_icon"), style: UIBarButtonItemStyle.Plain, target: self, action: #selector(back))
self.navigationItem.rightBarButtonItem = backButton
Where back_icon is an image that you are using and back is the following function:
func back() {
self.navigationController?.popViewControllerAnimated(true)
}
To hide the left bar item:
self.navigationItem.leftBarButtonItem = nil
or:
self.navigationItem.hidesBackButton = true
I have a NavigationBar at the top of a TableView. It looks nice opening/closing the search.
However, if I click on a button in a cell and get directed to another page (with segue); and then use Back button to unwind, it seems like bugged.
()
So it looks like it is pressed and opened but it shouldn't have. It should be looked like the top picture instead (just UIBarButtonItem - search button)
I couldn't figure out the issue creating this problem.
Please note that < Back is created automatically and I didn't write any code to create it. Is there something I am doing wrong?
Update: Added some snippets...
First, created a different class for handling the search
class SearchBarViewController: UIViewController, UISearchBarDelegate {
var searchBar : UISearchBar?
var searchBarWrapper : UIView?
var searchBarButtonItem : UIBarButtonItem?
func constructSearchBar()
{
searchBarButtonItem = UIBarButtonItem(barButtonSystemItem: .Search, target: self, action: "showSearchBar")
self.navigationItem.rightBarButtonItem = searchBarButtonItem
}
func showSearchBar() {
// styling & configuration
}
func searchBarCancelButtonClicked(searchBar: UISearchBar) {
UIView.animateWithDuration(0.2, animations: {
self.searchBar?.resignFirstResponder()
self.searchBarWrapper?.alpha = 0
}, completion: { (success) -> Void in
self.searchBar = nil
self.searchBarWrapper = nil
self.navigationItem.rightBarButtonItem = self.searchBarButtonItem
})
}
}
And my ViewController:
class ViewController: SearchBarViewController {
override func viewDidLoad() {
super.viewDidLoad()
constructSearchBar()
}
}
Regarding to emrys57's answer, I tried adding viewWillAppear() in my ViewController but I couldn't make it work, as my cancel looks a little different:
override func viewWillAppear(animated: Bool) {
super.viewDidAppear(animated)
// Here, I couldn't figure out what to put because
// my searchBarCancelButtonClicked() needs searchBar and
// forces me to use (!) but then it says, it's optional..
}
The answer is...
override func viewWillAppear(animated: Bool) {
super.viewDidAppear(animated)
navigationItem.titleView = nil
constructSearchBar()
}
You have not posted code, so it's not entirely clear what's gone wrong. Using UISearchBar, I think you must be handling the buttons separately yourself, as opposed to using UISearchController. I think that you may not be clearing away the search bar when coming back from the second VC. This code clears out the search bar in viewWillAppear:
class ViewController: UIViewController {
var cancelButton: UIBarButtonItem?
var searchButton: UIBarButtonItem?
override func viewDidLoad() {
super.viewDidLoad()
cancelButton = UIBarButtonItem(barButtonSystemItem: .Cancel, target: self, action: Selector("searchCancelPressed:"))
searchButton = UIBarButtonItem(barButtonSystemItem: .Search, target: self, action: Selector("searchPressed:"))
}
override func viewWillAppear(animated: Bool) {
super.viewDidAppear(animated)
searchCancelPressed(nil)
}
func searchPressed(sender: AnyObject) {
navigationItem.titleView = UISearchBar()
navigationItem.rightBarButtonItem = cancelButton
}
func searchCancelPressed(sender: AnyObject?) {
navigationItem.titleView = nil
navigationItem.rightBarButtonItem = searchButton
}
}
and that is working nicely for me when I do a push from a button to the second VC and then hit back.
Following the edit to the original question, this code seems to work, although it may not be the most elegant way of constructing the answer:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
navigationItem.titleView = nil
searchBarButtonItem = UIBarButtonItem(barButtonSystemItem: .Search, target: self, action: "showSearchBar")
navigationItem.rightBarButtonItem = searchBarButtonItem
}
The function constructSearchBar no longer needs to be called in viewDidLoad, and can be deleted.