UISearchController in iOS 9 - TextField from UISearchBar clipping out of navigation item - ios

I am trying to get a smooth Searchbar on navigation item on iOS 9, which means I can't use navigationItem.searchController property since its only iOS 11 only.
class SearchContainerViewController: UITableViewController {
let dataSource = ["1", "2", "3", "4", "5"]
override public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource.count
}
override public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
cell.textLabel?.text = dataSource[indexPath.row]
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
dismiss(animated: true, completion: nil)
}
}
class SearchViewController: UISearchController {
override func viewDidLoad() {
super.viewDidLoad()
}
}
class MyViewController : UIViewController, UISearchResultsUpdating, UISearchBarDelegate {
lazy var searchButton = UIBarButtonItem(title: "Search", style: UIBarButtonItem.Style.plain, target: self, action: #selector(showSearchBar))
var searchViewController: SearchViewController = {
let container = SearchContainerViewController()
let searchController = SearchViewController(searchResultsController: container)
return searchController
}()
override func viewDidLoad() {
super.viewDidLoad()
setupSearchController()
setupSearchButton()
}
func setupSearchController() {
searchViewController.searchResultsUpdater = self
searchViewController.searchBar.delegate = self
searchViewController.dimsBackgroundDuringPresentation = false
searchViewController.hidesNavigationBarDuringPresentation = false
searchViewController.searchBar.searchBarStyle = .minimal
searchViewController.searchBar.showsCancelButton = true
definesPresentationContext = true
}
#objc func showSearchBar() {
UIView.animate(withDuration: 0.75) {
self.navigationItem.titleView = self.searchViewController.searchBar
self.navigationItem.rightBarButtonItem = nil
self.searchViewController.searchBar.becomeFirstResponder()
}
}
func setupSearchButton() {
UIView.animate(withDuration: 0.75) {
self.navigationItem.titleView = nil
self.navigationItem.rightBarButtonItem = self.searchButton
}
}
// MARK: Conforms to UISearchResultUpdating
public func updateSearchResults(for searchController: UISearchController) { }
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
setupSearchButton()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
view.layoutSubviews()
}
}
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
let newWindow = UIWindow(frame: UIScreen.main.bounds)
let mainViewController = MyViewController()
let navigationController = UINavigationController(rootViewController: mainViewController)
newWindow.backgroundColor = .white
newWindow.rootViewController = navigationController
newWindow.makeKeyAndVisible()
window = newWindow
return true
}
}
Though the result is kinda disappointing since the textview with StatusBar is clipping out of the navigation item context, there is anything im doing wrong and could've done better?
Appreciate your support.

In before people down voting the question for no reasons, I am going to do something different and answer my own question.
The clipping happened because in both situations the height of the navigationItem were different because of they are somewhat stretchable if put big content in titleView (like a searchBar).
I've set the searchBar from the start on the navigationItem and just toogle their isHidden property when it should be done.
#objc private func activateSearch() {
UIView.animate(withDuration: 0.75) {
self.navigationItem.titleView?.isHidden = false
self.navigationItem.rightBarButtonItem = nil
self.searchController.isActive = true
}
}
private func deactivateSearch() {
UIView.animate(withDuration: 0.75) {
self.navigationItem.titleView?.isHidden = true
self.navigationItem.rightBarButtonItem = self.searchButton
self.searchController.isActive = false
}
}

Related

iOS: RefreshControl + large title doesn't work well in UITabBarController

I have UIViewController with NavigationBar that is a part of UITabController.
Inside UIViewController I have only UITableView.
NavigationBar is transparent and blurred.
TableView top constraint is to superview, so when I scroll content it goes behind navigation bar.
I have large titles enabled:
Problem:
loadData data triggered immediately when I start to scroll down.
Right after I scroll few pixels down.
All works fine if I remove largeTitles,
but with large titles it feels like refreshControl already at position when it ready to trigger .valueChanged
Also it works fine if to remove tab bar and load navigationController directly as root. But I need tabbar.
Full code:
#main
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let window = UIWindow(frame: UIScreen.main.bounds)
window.makeKeyAndVisible()
self.window = window
window.rootViewController = TabBarController()
return true
}
}
class TabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
let navigationController = UINavigationController(rootViewController: ViewController())
navigationController.navigationBar.prefersLargeTitles = true
viewControllers = [navigationController]
}
}
class ViewController: UIViewController {
private let tableView = UITableView()
private let refreshControl = UIRefreshControl()
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "Test"
view.addSubview(tableView)
tableView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
tableView.delegate = self
tableView.dataSource = self
tableView.refreshControl = refreshControl
refreshControl.addTarget(self, action: #selector(loadData), for: .valueChanged)
}
#objc func loadData() {
print("loadData triggered")
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
self.refreshControl.endRefreshing()
}
}
}
extension ViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: UITableViewCell
if let defaultCell = tableView.dequeueReusableCell(withIdentifier: "defaultCell") {
cell = defaultCell
} else {
cell = UITableViewCell(style: .value1, reuseIdentifier: "defaultCell")
}
cell.textLabel?.text = "Test"
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
10
}
}
BUT
All works fine if it's from StoryBoard:

iOS - stack fullscreen viewController when presenting another viewController

I have a UITabBarViewController with two tabs. I want to present a viewController fullscreen in one of the tabs. I have used the following code to do so.
let navCtrl = UINavigationController(rootViewController: eventViewController)
navCtrl.modalPresentationStyle = .fullScreen
self.navigationController?.present(navCtrl, animated: true)
It works. And EventViewController is fullscreen. However, when presenting another viewController in EventViewController, EventViewController is still fullscreen. But I want it to shrink in size and stack-up as it normally do( as in the image). In order to do so, I have changed modalPresentationStyle to overCurrentContext.
let navCtrl = UINavigationController(rootViewController: eventViewController)
navCtrl.modalPresentationStyle = .overCurrentContext
self.navigationController?.present(navCtrl, animated: true)
It does so, but it causes another problem: If I change tabs and dismiss EventViewController, the presenting viewController is black as described in this question (none of the answers was helpful).
Basically I want the EventController to be fullscreen but shrink in size when presenting another controller in it. How to do so?
Update
A simple project with the same issue.
class TabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
let ctrl = TabZeroViewController()
ctrl.tabBarItem.image = UIImage(named: "archived-task")
ctrl.tabBarItem.title = "One"
let test = TabOneViewController()
test.tabBarItem.image = UIImage(named: "Test")
test.tabBarItem.title = "Test"
let tabBarList = [ctrl, test ]
self.viewControllers = tabBarList.map {
let nav = UINavigationController(rootViewController: $0)
nav.interactivePopGestureRecognizer?.isEnabled = true
return nav
}
}
}
class TabZeroViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.delegate = self
self.tableView.dataSource = self
self.view.backgroundColor = .white
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = "\(indexPath.row)"
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let ctrl = ModalTableViewController()
let nav = UINavigationController(rootViewController: ctrl)
nav.modalPresentationStyle = .fullScreen
self.navigationController?.present(nav, animated: true)
}
}
class ModalTableViewController: UITableViewController {
override func viewDidLoad() {
self.view.backgroundColor = .red
let button = UIButton()
button.setTitle("Cancel", for: .normal)
button.addTarget(self, action: #selector(dismissModal), for: .allEvents)
let item = UIBarButtonItem()
item.customView = button
self.navigationItem.leftBarButtonItem = item
self.tableView.dataSource = self
self.tableView.delegate = self
}
#objc func dismissModal() {
self.dismiss(animated: true, completion: nil)
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = "Event"
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let ctrl = EventViewController()
let nav = UINavigationController(rootViewController: ctrl)
nav.modalPresentationStyle = .overCurrentContext
self.navigationController?.present(nav, animated: true)
}
}
class TabOneViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
}
class EventViewController: UITableViewController {
override func viewDidLoad() {
self.view.backgroundColor = .red
let button = UIButton()
button.setTitle("Cancel", for: .normal)
button.addTarget(self, action: #selector(dismissModal), for: .allEvents)
let item = UIBarButtonItem()
item.customView = button
self.navigationItem.leftBarButtonItem = item
self.tableView.dataSource = self
self.tableView.delegate = self
}
#objc func dismissModal() {
self.dismiss(animated: true, completion: nil)
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = "Event"
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let ctrl = EventViewController()
let nav = UINavigationController(rootViewController: ctrl)
self.navigationController?.present(nav, animated: true)
}
}
Add this code in willConnectTo of SceneDelegate.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = TabBarController()
self.window = window
window.makeKeyAndVisible()
}
While you are on first tab, select a table cell to open the ModalTableViewController. And then change tabs and dismiss ModalTableViewController.
As for example project - presenting view over full screen hides TabBar. But I changed code a bit to propose working solution. Probably you would want to change it a bit, but I hope this will push you in good direction :)
It was actually needed to dismiss ModalTableViewController to avoid black screen.
class TabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
let ctrl = TabZeroViewController()
ctrl.tabBarItem.image = UIImage(named: "archived-task")
ctrl.tabBarItem.title = "One"
let test = TabOneViewController()
test.tabBarItem.image = UIImage(named: "Test")
test.tabBarItem.title = "Test"
let tabBarList = [ctrl, test ]
let viewControllers: [UIViewController] = tabBarList.map {
let nav = UINavigationController(rootViewController: $0)
nav.interactivePopGestureRecognizer?.isEnabled = true
nav.tabBarItem = $0.tabBarItem
return nav
}
self.setViewControllers(viewControllers, animated: false)
}
override var selectedViewController: UIViewController? {
get {return super.selectedViewController}
set {
if super.selectedViewController?.presentedViewController != nil {
super.selectedViewController?.dismiss(animated: false, completion: nil)
}
super.selectedViewController = newValue
}
}
}
class TabZeroViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.delegate = self
self.tableView.dataSource = self
self.view.backgroundColor = .white
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = "\(indexPath.row)"
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let ctrl = ModalTableViewController()
let nav = UINavigationController(rootViewController: ctrl)
nav.modalPresentationStyle = .currentContext
self.present(nav, animated: true)
}
}
class ModalTableViewController: UITableViewController {
override func viewDidLoad() {
self.view.backgroundColor = .red
let button = UIButton()
button.setTitle("Cancel", for: .normal)
button.addTarget(self, action: #selector(dismissModal), for: .allEvents)
let item = UIBarButtonItem()
item.customView = button
self.navigationItem.leftBarButtonItem = item
self.tableView.dataSource = self
self.tableView.delegate = self
}
#objc func dismissModal() {
self.presentingViewController?.dismiss(animated: false, completion: nil)
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = "Event"
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let ctrl = EventViewController()
let nav = UINavigationController(rootViewController: ctrl)
nav.modalPresentationStyle = .fullScreen
self.navigationController?.present(nav, animated: true)
}
}
class TabOneViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
}
}
class EventViewController: UITableViewController {
override func viewDidLoad() {
self.view.backgroundColor = .red
let button = UIButton()
button.setTitle("Cancel", for: .normal)
button.addTarget(self, action: #selector(dismissModal), for: .allEvents)
let item = UIBarButtonItem()
item.customView = button
self.navigationItem.leftBarButtonItem = item
self.tableView.dataSource = self
self.tableView.delegate = self
}
#objc func dismissModal() {
self.dismiss(animated: true, completion: nil)
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = "Event"
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let ctrl = EventViewController()
let nav = UINavigationController(rootViewController: ctrl)
self.navigationController?.present(nav, animated: true)
}
}
Good luck!
Try this code to present screen modally:
func getImageFromView() -> UIImage {
let layer = UIApplication.shared.keyWindow?.layer
let scale = UIScreen.main.scale
UIGraphicsBeginImageContextWithOptions(layer?.frame.size ?? CGSize.zero, false, scale)
if let context = UIGraphicsGetCurrentContext() {
layer?.render(in: context)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image ?? UIImage()
}
return UIImage()
}
/// This is the method to present screen modally
/// - parameter controller: controller instance on which screen will be presented
func presentScreenModally(controller: UIViewController, animated: Bool) {
let loginController = UIStoryboard.loadLoginViewController()//Get instance of controller form storyboard
loginController.bgTranParentImg = getImageFromView()
let bgImage = getImageFromView()
let presentationStyleViewController = UIStoryboard.loadPresentationStyleController()// This is another controller, which I am pasting below
presentationStyleViewController.bgimage = bgImage
presentationStyleViewController.loginController = loginController
presentationStyleViewController.addChild(loginController)
controller.view.window?.addSubview(presentationStyleViewController.view)
loginController.view.frame = presentationStyleViewController.containerView.bounds
presentationStyleViewController.containerView.addSubview(loginController.view)
let navigationController = UINavigationController(rootViewController: presentationStyleViewController)
navigationController.navigationBar.isHidden = true
navigationController.modalPresentationStyle = .fullScreen
controller.navigationController?.present(navigationController, animated: animated, completion: nil)
}
PresentationStyleViewController class:
class PresentationStyleViewController: UIViewController {
#IBOutlet var containerView: UIView!
#IBOutlet var containeTopConstraint: NSLayoutConstraint!
#IBOutlet var containerBottomConstraint: NSLayoutConstraint!
#IBOutlet var backgroundImage: UIImageView!
var bgimage: UIImage?
let topPadding: CGFloat = 30
var loginController: LoginViewController?
override func viewDidLoad() {
super.viewDidLoad()
self.uiSetup()
}
override func viewDidAppear(_ animated: Bool) {
restorePopup()
}
/// Initial UI setup
func uiSetup() {
containeTopConstraint.constant = self.view.frame.size.height
backgroundImage.image = bgimage
}
#IBAction func panGesture(_ sender: UIPanGestureRecognizer) {
guard let piece = sender.view else {return}
let translation = sender.translation(in: piece.superview)
containeTopConstraint.constant = translation.y >= topPadding ? translation.y : topPadding
if sender.state == .ended || sender.state == .cancelled {
if containeTopConstraint.constant > self.view.frame.size.height/4 && translation.y > 0 {
self.dismissPopup()
} else {
self.restorePopup()
}
}
}
/// Dismisses popup and controller
func dismissPopup() {
containeTopConstraint.constant = self.view.frame.size.height
UIView.animate(withDuration: 0.3,
animations: {
self.view.layoutIfNeeded()
}, completion: { (_) in
self.loginController?.btnClick_cross(UIButton())
self.dismiss(animated: false)
})
}
/// Restores popup at initial position
func restorePopup() {
containeTopConstraint.constant = topPadding
UIView.animate(withDuration: 0.3,
animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
}

Swift search bar(controller) memory leaks

I have main screen, with a button on which I segue to searchVC screen.I have a navigation controller between them, in searchVC there are searchController and searchBar.
Problem:I need to activate search when screen appears, but searchBar activation(tap or becomeFirstResponder() ) causes memory leaks(image below)
I tried to remove delegates and the problem disappears, but I need to know when cancel button pressed to segue/dismiss to mainVC
Code:tableView for results, resultView with label for empty results
class SearchViewController: UIViewController,UISearchBarDelegate,UISearchControllerDelegate {
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var resultView: ResultView!
let searchController = UISearchController(searchResultsController: nil)
var filteredSongs = [SongListModel]()
var songs = SongListModel.fetchSongs()
override func viewDidLoad() {
super.viewDidLoad()
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Search songs"
if #available(iOS 11.0, *) {
navigationItem.titleView = searchController.searchBar
navigationItem.hidesSearchBarWhenScrolling = false
// navigationController?.navigationBar.topItem?.searchController = searchController
// navigationItem.titleView?.isHidden = true
searchController.dimsBackgroundDuringPresentation = false
searchController.hidesNavigationBarDuringPresentation = false
} else {
tableView.tableHeaderView = searchController.searchBar
}
searchController.searchBar.showsCancelButton = true
searchController.definesPresentationContext = true
searchController.searchBar.sizeToFit()
searchController.delegate = self
searchController.searchBar.delegate = self
tableView.keyboardDismissMode = .interactive
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
delay(0.1) { [unowned self] in
self.searchController.searchBar.becomeFirstResponder()
}
}
func delay(_ delay: Double, closure: #escaping ()->()) {
let when = DispatchTime.now() + delay
DispatchQueue.main.asyncAfter(deadline: when, execute: closure)
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
searchController.searchBar.resignFirstResponder()
}
func searchBarIsEmpty() -> Bool {
// Returns true if the text is empty or nil
return searchController.searchBar.text?.isEmpty ?? true
}
func filterContentForSearchText(_ searchText: String, scope: String = "All") {
filteredSongs = songs.filter({( song : SongListModel) -> Bool in
return song.musicFileName.lowercased().contains(searchText.lowercased())
})
tableView.reloadData()
}
func isFiltering() -> Bool {
return searchController.isActive && !searchBarIsEmpty()
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
searchController.searchBar.endEditing(true)
searchController.searchBar.resignFirstResponder()
// searchController.searchBar.delegate = nil
// searchController.searchResultsUpdater = nil
dismiss(animated: true, completion: nil)
}
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "PlayTilesSegue", let destinationVC = segue.destination as? TilesViewController, let selectedIndex = tableView.indexPathForSelectedRow?.row {
let song: SongListModel
if isFiltering() {
song = filteredSongs[selectedIndex]
} else {
song = songs[selectedIndex]
}
destinationVC.songFileName = song.musicFileName
navigationController?.setNavigationBarHidden(true, animated: false)
}
}
}
extension SearchViewController: UITableViewDelegate,UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if isFiltering() {
resultView.setIsFilteringToShow(filteredItemCount: filteredSongs.count, of: songs.count)
return filteredSongs.count
}
resultView.setNotFiltering()
return songs.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "filteredCell", for: indexPath) as! FilteredSongCell
let song: SongListModel
if isFiltering() {
song = filteredSongs[indexPath.row]
} else {
song = songs[indexPath.row]
}
cell.listenSongButton.setBackgroundImage(UIImage(named: "playback"), for: .normal)
cell.filteredAuthorNameLabel.text = song.authorName
cell.filteredSongNameLabel.text = song.songName
cell.playGameButton.setTitle(song.playButton.rawValue, for: .normal)
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
performSegue(withIdentifier: "PlayTilesSegue", sender: indexPath)
tableView.deselectRow(at: indexPath, animated: true)
}
}
extension SearchViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
filterContentForSearchText(searchController.searchBar.text!)
}
}
Image memory leaks
Deactivating the search controller on removing VC from parent helps to avoid memory leak:
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
if parent == nil, searchController.isActive {
searchController.isActive = false
}
}
You need to set the UISearchController searchBar's delegate. Once you have done this, the addition of the delegate method searchBarCancelButtonClicked: will properly be called.
Here it is.

Unable to present a UISearchController

I have the following view hierarchy:
UINavigationController
||
\/
LibraryTableViewController: UITableViewController
||
\/
AlbumsCollectionViewController: UICollectionViewController
||
\/
SongsTableViewController: UITableViewController
I want to have a search bar in AlbumsCollectionViewController and a different one in SongsTableViewController that is shown in the navigationItem.titleView.
I have managed to add a working search bar in AlbumsCollectionViewController as follows:
class AlbumsCollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout, UISearchControllerDelegate, UISearchResultsUpdating, UISearchBarDelegate {
var searchController : UISearchController!
override func viewDidLoad() {
super.viewDidLoad()
initSearchBar()
initNavigationBar()
}
private func initSearchBar() {
self.searchController = UISearchController(searchResultsController: nil)
self.searchController.searchResultsUpdater = self
self.searchController.delegate = self
self.searchController.searchBar.delegate = self
self.searchController.hidesNavigationBarDuringPresentation = false
self.searchController.dimsBackgroundDuringPresentation = false
searchController.searchResultsController?.view.isHidden = false
searchController.hidesNavigationBarDuringPresentation = false
self.extendedLayoutIncludesOpaqueBars = true
self.definesPresentationContext = true
searchController.searchBar.backgroundColor = UIColor.black
UIBarButtonItem.appearance(whenContainedInInstancesOf: [UISearchBar.self]).setTitleTextAttributes([NSAttributedStringKey.foregroundColor : UIColor.white], for: .normal)
self.navigationItem.titleView = searchController.searchBar
navigationItem.titleView?.isHidden = true
}
private func initNavigationBar() {
searchButton.tintColor = UIColor.white
settingsButton.tintColor = UIColor.white
backButton.tintColor = UIColor.white
self.navigationItem.title = "Artists"
self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedStringKey.foregroundColor : UIColor.white]
}
#IBAction func SearchButtonTapped(_ sender: Any) {
showSearchBar()
}
private func showSearchBar(){
navigationItem.titleView?.isHidden = false
searchController.isActive = true
}
}
Note that the search bar is hidden on ViewDidLoad() and is presented when a button is pressed as shown in SearchButtonTapped method.
Now, I am trying to do the same in SongsTableViewController however, the search bar is not showing when tapping the the button (i.e. calling SearchButtonTapped) and I am getting the following message:
Warning: Attempt to present <UISearchController: 0x7f8158812b50> on <MyProject.AlbumsCollectionViewController: 0x7f81588023c0> whose view is not in the window hierarchy!
If I commented the line searchController.isActive = true then the search bar will show, however, it wont be active even if I tapped on it.
Edit
Sorry if I haven't been clear. I have a separate UISearchController in SongsTableViewController. I meant I am using the same logic in both controllers
Also Note if I pushed SongsTableViewController from the navigation controller (i.e the view hierarchy only has 2 controllers (UINavigationController => SongsTableViewController) the search bar works fine
This is most of the Code of SongsTableViewController (omitted non relevant stuff)
import UIKit
import os.log
import MediaPlayer
class SongsTableViewController: UITableViewController, UISearchControllerDelegate, UISearchResultsUpdating, UISearchBarDelegate ,PlayerDelegate, NowPlayingDelegate, SongCellDelegate, SongsOptionsDelegate {
// MARK: properties
var playerManager: PlayerManager? = nil
var dataManager: DataManager? = nil
var tabVC: TabBarController?
var selectedSong: Song?
lazy var optionsTransitionDelegate = PresentationManager()
lazy var playlistTransitionDelegate = PresentationManager()
var searchController : UISearchController!
#IBOutlet var backgroundView: UIView!
#IBOutlet weak var searchButton: UIBarButtonItem!
#IBOutlet weak var settingsButton: UIBarButtonItem!
var albumID: String?
var artistID: String?
var playlist: Playlist?
var songs = [Song]()
var songIndexMap = [String: Int]()
var filteredSongs = [Song]()
override func viewDidLoad() {
super.viewDidLoad()
self.dataManager = DataManager.getInstance()
self.playerManager = PlayerManager.getInstance()
playlistTransitionDelegate.screenRatio = 2.0 / 3.0
if(self.albumID != nil) {
self.songs = SQLiteManager.getAlbumSongs(albumID: self.albumID!)
} else if(self.artistID != nil) {
self.songs = SQLiteManager.getArtistSongs(artistID: self.artistID!)
} else if (self.playlist != nil) {
self.songs = SQLiteManager.getPlaylistSongs(playlist: self.playlist!)
} else {
dataManager?.songsTableViewController = self
}
for i in 0..<songs.count {
songIndexMap[songs[i].id] = i
}
initSearchBar()
initNavigationBar()
if(songs.count == 0 && (!fullListOfSongs() || fullListOfSongs() && dataManager?.getFullSongsCount() == 0)){
tableView.backgroundView = backgroundView
}
tableView.tableFooterView = UIView()
tabVC = tabBarController as? TabBarController
tabVC?.nowPlayingViewController?.delegate = self
}
private func shouldAutorotate() -> Bool {
return false
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return Util.SONG_CELL_HEIGHT
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if(isFiltering()) {
return self.filteredSongs.count
} else if (fullListOfSongs()) {
return dataManager!.getFullSongsCount()
}
return self.songs.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "SongTableViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? SongCell else {
fatalError("The dequeued cell is not an instance of SongCell.")
}
var song: Song?
if(fullListOfSongs()) {
if(isFiltering()){
song = self.filteredSongs[indexPath.row]
} else {
song = dataManager?.getSong(index: indexPath.row)
}
} else {
if(isFiltering()){
song = self.filteredSongs[indexPath.row]
} else {
song = self.songs[indexPath.row]
}
}
cell.setAttributes(song: song!)
cell.delegate = self
cell.preservesSuperviewLayoutMargins = false
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath){
self.tableView.deselectRow(at: indexPath, animated: false)
}
// MARK: - Search Bar
func updateSearchResults(for searchController: UISearchController) {
if (!searchController.isActive) {
hideSearchBar()
tableView.reloadData()
}
if(isSearchBarEmpty()) {
return
}
filterSongs(filter: searchController.searchBar.text!)
tableView.reloadData()
}
private func filterSongs(filter: String) {
if(self.albumID != nil) {
self.filteredSongs = SQLiteManager.getAlbumSongs(albumID: self.albumID!, filter: filter)
} else if(self.artistID != nil) {
self.filteredSongs = SQLiteManager.getArtistSongs(artistID: self.artistID!, filter: filter)
} else if(self.playlist != nil) {
self.filteredSongs = SQLiteManager.getPlaylistSongs(playlist: self.playlist!, filter: filter)
}else {
self.filteredSongs = SQLiteManager.getSongs(filter: filter)
}
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
if searchText == "" {
tableView.reloadData()
}
}
private func initSearchBar() {
self.searchController = UISearchController(searchResultsController: nil)
self.searchController.searchResultsUpdater = self
self.searchController.delegate = self
self.searchController.searchBar.delegate = self
self.searchController.hidesNavigationBarDuringPresentation = false
self.searchController.dimsBackgroundDuringPresentation = false
searchController.searchResultsController?.view.isHidden = false
searchController.hidesNavigationBarDuringPresentation = false
self.extendedLayoutIncludesOpaqueBars = true
self.definesPresentationContext = true
searchController.searchBar.backgroundColor = UIColor.black
UIBarButtonItem.appearance(whenContainedInInstancesOf: [UISearchBar.self]).setTitleTextAttributes([NSAttributedStringKey.foregroundColor : UIColor.white], for: .normal)
self.navigationItem.titleView = searchController.searchBar
navigationItem.titleView?.isHidden = true
}
private func initNavigationBar() {
searchButton.tintColor = UIColor.white
if (fullListOfSongs()) {
searchButton.isEnabled = false
dataManager?.buttons.append(searchButton)
}
settingsButton.tintColor = UIColor.white
self.navigationItem.title = "Songs"
self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedStringKey.foregroundColor : UIColor.white]
}
#IBAction func SearchButtonTapped(_ sender: Any) {
showSearchBar()
}
private func showSearchBar(){
self.navigationItem.titleView?.isHidden = false
self.searchController.isActive = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self.searchController.searchBar.becomeFirstResponder()
}
navigationItem.rightBarButtonItems![0].isEnabled = false
navigationItem.rightBarButtonItems![0].image = nil
navigationItem.rightBarButtonItems![1].isEnabled = false
navigationItem.rightBarButtonItems![1].image = nil
}
private func hideSearchBar() {
navigationItem.titleView?.isHidden = true
navigationItem.rightBarButtonItems![0].isEnabled = true
navigationItem.rightBarButtonItems![0].image = UIImage(named: "settings")
navigationItem.rightBarButtonItems![1].isEnabled = true
navigationItem.rightBarButtonItems![1].image = UIImage(named: "search")
}
func isFiltering() -> Bool {
if(searchController == nil){
return false
}
return searchController.isActive && !isSearchBarEmpty()
}
private func isSearchBarEmpty() -> Bool {
return searchController.searchBar.text?.isEmpty ?? true
}
private func fullListOfSongs() -> Bool {
return self.playlist == nil && self.albumID == nil && self.artistID == nil
}
}
This worked when I tested. I believe the problem is in SongsTableViewController: self.definesPresentationContext = false. This should be true for the pushed View Controller. (see docs here)
For SongsTableViewController (pushed view controller) add the following:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.definesPresentationContext = true
}
And add this to AlbumsCollectionViewController (initial view controller):
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.definesPresentationContext = false
}
When you are on SongsTableViewController, your AlbumsCollectionViewController is not in the window hierarchy.
What I can understand, you are calling showSearchBar method of AlbumsCollectionViewController from SongsTableViewController. And since you navigated from AlbumsCollectionViewController to SongsTableViewController, your AlbumsCollectionViewController is not in the window hierarchy hence wont able to present the search controller.
To fix try adding a separate searchbar controller in SongsTableViewController just as you previously did in AlbumsCollectionViewController.
Alternatively you can create a seperate viewcontroller, implement search functionality and then present it from both of your controllers.

Let my UISearchBar get focus automatically when the view is loaded

Here is my view with UISearchBar in my navigationbar:
I want it get focussed automatically when my view is loaded. I tried a few ways based on this question. But none is working. I have to click the searchbar to make it focussed.
This is one of my code:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.searchController.isActive = true
DispatchQueue.main.async { [unowned self] in
self.searchController.searchBar.becomeFirstResponder()
}
}
Somebody mentioned the searchController should be active after becomeFirstResponder. I tried this:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
DispatchQueue.main.async { [unowned self] in
self.searchController.searchBar.becomeFirstResponder()
self.searchController.isActive = true
}
}
The keybord did come out this time. But I can't key in anything in my search bar.
Any idea? Thanks.
I follow the suggestion and recreate my project. It works then. My code:
class SearchViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate {
......
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
searchController.isActive = true
}
func didPresentSearchController(_ searchController: UISearchController) {
DispatchQueue.main.async { [unowned self] in
self.searchController.searchBar.becomeFirstResponder()
}
}
Has to put becomeFirstRespnder in main queue, otherwise the keyboard will not be shown automatically.
I could focus the search bar whenever it is appear with the following code. Hope if would be helped.
And I have compared calling the become first responder inside viewWillAppear and viewDidAppear, it only worked when you call it inside viewWillAppear. But I am not quite understand why this happened. That maybe the reason why you can't type anything inside your searchbar.
PS: I think your DispatchQueue.main.async is not necessary inside viewDidAppear. It always be called in the main queue.
//
// TestSearchbarFocus.swift
// SwiftPlayground
//
// Created by Enix Yu on 31/10/2016.
// Copyright © 2016 RobotBros. All rights reserved.
//
import UIKit
class TestSearchbarFocus: UITableViewController, UISearchResultsUpdating {
var searchController : UISearchController!
let data = ["ABC", "BBC", "CCD", "Enix", "Peter", "Earth"]
var displayData = [String]()
override func viewDidLoad() {
super.viewDidLoad()
searchController = UISearchController(searchResultsController: nil)
searchController.searchResultsUpdater = self
searchController.dimsBackgroundDuringPresentation = false
searchController.hidesNavigationBarDuringPresentation = false
definesPresentationContext = true
navigationItem.titleView = searchController.searchBar
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
searchController.active = true
searchController.searchBar.becomeFirstResponder()
}
func filterDataForSearchText(text: String){
displayData = data.filter({
(item) -> Bool in
item.lowercaseString.containsString(text.lowercaseString)
})
tableView.reloadData()
}
func updateSearchResultsForSearchController(searchController: UISearchController) {
filterDataForSearchText(searchController.searchBar.text!)
}
// MARK : UITableViewDataSource/Delegate
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if searchController.active && searchController.searchBar.text != "" {
return displayData.count
}
return data.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
if searchController.active && searchController.searchBar.text != "" {
cell.textLabel?.text = displayData[indexPath.row]
} else {
cell.textLabel?.text = data[indexPath.row]
}
return cell
}
}
PS: I am using swift 2 + Xcode 7.3.1

Resources