I'm attempting to recreate a UI similar to that of the one found in the Maps app—namely my focus is on the bottom drawer. I've created a container view controller DrawerViewController that is added to the root view controller ViewController. The DrawerViewController handles the positioning and interaction with the drawer itself, which then receives a child view controller as its content—in this case, by default, MapSearchViewController. I'm trying to add a UISearchController to the MapSearchController, which simply contains the UISearchBar from the search controller and a UITableView. I'm having the issue that when the search bar is tapped, both the search bar and table view are disappearing, and from the UI inspector, I can see a UITransitionView has been added.
After some extensive research, I was able to find quite a few SO answers where although the situation wasn't the exact same, the general trend among them was that definesPresentationContext must be true on the view controller containing the search controller. I added this, but alas, no dice. This did however, lead me to a temporary solution: in the DrawerViewController, I had been adding the MapSearchViewController as a child by doing:
let mapSearchViewController = MapSearchViewController()
contentView.addSubview(mapSearchViewController.view)
addChild(mapSearchViewController)
mapSearchViewController.didMove(toParent: self)
I found that by removing the addChild(_:) call fixes the issue of the search bar and table view disappearing when the search bar is tapped, but since the view controller hasn't been added as a child, all of the logic in the MapSearchViewController doesn't get executed. I assume its an issue with the view controller hierarchy as such or how I'm trying to add the search controller to the MapSearchController, but I have no idea what to even begin to look for to fix this.
import UIKit
class MapSearchViewController: UIViewController {
weak var delegate: MapSearchViewControllerDelegate?
var searchController: UISearchController!
var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
definesPresentationContext = true
view.backgroundColor = .clear
view.translatesAutoresizingMaskIntoConstraints = false
searchController = UISearchController(searchResultsController: nil)
searchController.searchResultsUpdater = self
searchController.dimsBackgroundDuringPresentation = false
searchController.obscuresBackgroundDuringPresentation = false
let searchBar = searchController.searchBar
searchBar.searchBarStyle = .minimal
searchBar.placeholder = "Search for a building or place"
view.addSubview(searchBar)
searchBar.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
searchBar.topAnchor.constraint(equalTo: view.topAnchor),
searchBar.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 2.0),
searchBar.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -2.0),
searchBar.heightAnchor.constraint(equalToConstant: 56.0)
])
tableView = UITableView(frame: .zero)
tableView.dataSource = self
tableView.delegate = self
tableView.backgroundColor = .clear
tableView.layer.borderColor = UIColor.lightGray.withAlphaComponent(0.7).cgColor
tableView.layer.borderWidth = 1.0 / 3.0
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: searchBar.bottomAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
}
// Both these extensions just have protocol stubs as of now, but I'm including them to show they exist.
extension MapSearchViewController: UISearchResultsUpdating {
...
}
extension MapSearchViewController: UITableViewDataSource {
...
}
I've foregone including the source for the DrawerViewController here, as I believe the only relevant part is the section included above where the MapSearchViewController is created and added. I haven't changed any properties of the DrawerViewController itself, only properties of the drawer's view. However, if anyone thinks they might know the issue and need to see the source, please leave me a comment and I can edit the question to include it.
Related
This is how I setup my UISearchController (I am using a new controller BackupSearchResultsVC to show the search result)
class BackupViewViewController: UIViewController {
private lazy var searchController: UISearchController = {
let backupSearchResultsVC = BackupSearchResultsVC.instanceFromNib()
backupSearchResultsVC.postInit(nsBackup)
let searchController = UISearchController(searchResultsController: backupSearchResultsVC)
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = true
searchController.searchBar.placeholder = "search_notes".localized
searchController.searchBar.delegate = self
return searchController
}()
override func viewDidLoad() {
super.viewDidLoad()
/** Specify that this view controller determines how the search controller is presented.
The search controller should be presented modally and match the physical size of this view controller.
*/
definesPresentationContext = true
navigationItem.searchController = searchController
}
This is the UI before search begins.
Once the search begins (The search bar will move upward to block the title bar when the search text field is in focus), there is a wide gap between the search result (red background) and the search text bar.
Is there any way to avoid the gap, yet able to use a new controller to show the result?
Thanks.
By default, the presentation is full screen and the searchResultsController goes under the nav bar which has the search bar.
I believe this has something to do with the autolayout constraints / background color set up of your view controller set up to be the searchResultsController
For example, I have set up the like this:
class BackupSearchResultsVC: UIViewController, UISearchResultsUpdating
{
let resultsView = UIView()
override func viewDidLoad()
{
super.viewDidLoad()
configureResultsView()
}
private func configureResultsView()
{
resultsView.translatesAutoresizingMaskIntoConstraints = false
resultsView.backgroundColor = .red
view.addSubview(resultsView)
// Add constraints with top anchor of 200
view.addConstraints([
resultsView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
resultsView.topAnchor.constraint(equalTo: view.topAnchor, constant: 200),
resultsView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
resultsView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
}
Because of the top anchor, it gives me the same results you see:
If you want to keep the red view's position as it is, add a background color to your view controller's main view
override func viewDidLoad()
{
super.viewDidLoad()
// Give a background color
view.backgroundColor = .white
configureResultsView()
}
Or you could just remove the constraint to the top
// Add constraints without top anchor of 200
view.addConstraints([
resultsView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
resultsView.topAnchor.constraint(equalTo: view.topAnchor),
resultsView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
resultsView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
If this still does not work, try backupSearchResultsVC.modalPresentationStyle = .fullScreen during your set up
Give one of these a go and see if it solves your problem
Update based on Mr Cheng's
It seems giving the UIViewController used as the searchResultsController seems to make the UINavigationBar go transparent when active and the full view controller is shown behind:
This is different to what I see running the same code:
This leads me to believe this is iOS related as my device is iOS 14 and the result you see is probably iOS 15.
Digging a bit deeper, I came across this post and this which suggests by default, the bar tint is transparent on iOS 15.
So I believe you might need to set a custom background color for the nav bar in iOS 15 as outlined here perhaps
I am trying to get my UITableView to be positioned within the safe area but it doesn't seem to be working and I do not know why. I trying to do this programatically.
class MenuTableViewController: UITableViewController{
var margin: UILayoutGuide!
var tableDataSource: [userFolderObject]!
let cellId = "cellId"
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
private func setup(){
margin = view.layoutMarginsGuide
tableDataSource = MockData.UITableDateSource
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.topAnchor.constraint(equalTo: margin.topAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: margin.bottomAnchor).isActive = true
tableView.leadingAnchor.constraint(equalTo: margin.leadingAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: margin.trailingAnchor).isActive = true
/* I have also tried the below code
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
*/
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellId)
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let label = UILabel()
label.backgroundColor = UIColor.lightGray
if section == 0{
label.text = "Search PubMed"
}else{
label.text = "My Folders"
}
return label
}
}
First, remove all of your setup code that attempts to mess with the margins of the table view. That is all done for you by default in a UITableViewController.
Since your issue is only with the layout of your custom section header views, you need to fix how you have implemented those views.
Like cells, you should use reusable header/footer views and your header/footer view should extend UITableViewHeaderFooterView. This will ensure proper margins by defaults and it already provides a standard textLabel you can set. No need to create your own UILabel.
As shown in the documentation for UITableViewHeaderFooterView you should register a class. Then in viewForHeader you should dequeque the header view and then set its textLabel as needed.
If you don't actually need anything but a plain old section label, then don't implement viewForHeader. Instead, implement titleForHeader. Much simpler.
I have a tableView:
tableView = UITableView()
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
let constraints = [tableView.topAnchor.constraint(equalTo: view.topAnchor), tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), tableView.heightAnchor.constraint(equalToConstant: view.frame.height * 0.6)]
NSLayoutConstraint.activate(constraints)
And my searchController looks like:
func setupSearchController() {
searchController = UISearchController(searchResultsController: nil)
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Search for a candy"
navigationItem.searchController = searchController
definesPresentationContext = true
}
When it loads for the first time everything is fine. But when I tap on the search bar to type there, my search bar moves to the top, navigation bar becomes smaller and it breaks my layout and white space appears between tableView and searchBar:
How can I improve my constraints to move up and down my tableView when the navigation bar sizes are changed?
You have to change your top constraint of the tableview.
Try replacing this 2 constraint
tableView.topAnchor.constraint(equalTo: searchController.searchBar.bottomAnchor),
tableView.topAnchor.constraint(equalTo: view.topAnchor)
With this
tableView.topAnchor.constraint(equalTo: view.topAnchor)
The automatic content inset adjustment will do the rest.
Note also that in your table view constraints list you doesn't have a leading/horizontal position constraint. I suggest to add also this constraint
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor)
There's this large titles feature in iOS 11 that shows large title when the UITableViewController's table is scrolled to top, and gets collapsed to standard small title when the user scrolls the table away from top. This is standard behavior. I need the navigation controller to behave a bit differently - I need to always show the large title. How to achieve this?
Following code does not help, it still collapses when scrolled.
navigationController?.navigationBar.prefersLargeTitles = true
navigationItem.largeTitleDisplayMode = .always
I've achieved it unintentionally when embedded UITableViewController inside UIViewController.
I'm not sure whether it is an Apple's bug or intended behavior.
So stack is as simple as UINavigationController -> UIViewController(used as container) -> UITableViewController
Here is sample of view controller with embedded UITableViewController fullscreen
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var vc = UITableViewController(style: .plain)
var array: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
vc.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(vc.view)
view.addConstraint(view.leadingAnchor.constraint(equalTo: vc.view.leadingAnchor))
view.addConstraint(view.rightAnchor.constraint(equalTo: vc.view.rightAnchor))
view.addConstraint(view.safeAreaLayoutGuide.topAnchor.constraint(equalTo: vc.view.topAnchor))
view.addConstraint(view.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor))
vc.tableView.delegate = self
vc.tableView.dataSource = self
array = "0123456789".characters.map(String.init)
vc.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "identifier")
title = "Title"
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return array.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "identifier", for: indexPath)
cell.textLabel?.text = array[indexPath.row]
return cell
}
}
Here is the result
Hope it helps.
P.S. Surprisingly, my current problem is that I don't know how to get collapsing behavior with such architecture :)
What I did was to add another view between navigationBar and TableView with a height of 1.
let tableViewSeperator: UIView = {
let view = UIView()
// remove the color, so it wont be visible.
view.backgroundColor = UIColor.systemBlue
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
One thing which is important is add this seperator view as a subview of your viewcontroller's view before tableView, otherwise it won't work
view.addSubview(tableViewSeperator)
view.addSubview(tableView)
or if you want to save one line of code, you can also do it like this.
[tableViewSeperator, tableView].forEach({view.addSubview($0)})
Then set its constraints like this.
tableViewSeperator.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
tableViewSeperator.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0).isActive = true
tableViewSeperator.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true
tableViewSeperator.heightAnchor.constraint(equalToConstant: 1).isActive = true
The last thing is change the tableView TopAnchor to be the BottomAnchor of sperator View.
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0).isActive = true
tableView.topAnchor.constraint(equalTo: tableViewSeperator.bottomAnchor, constant: 0).isActive = true
tableView.bottomAnchor.constraint(equalTo: createItemBtn.topAnchor, constant: 0).isActive = true
Now when you scroll the the NavigationBar will stay as Large.
You need to add UIView(it's can be width=0, height=0) before add UITableView.
example
Then this code will work
navigationController?.navigationBar.prefersLargeTitles = true
navigationItem.largeTitleDisplayMode = .always
I have a filterbar, which is a UIView as well as a tableView in my controller like so:
override func viewWillLayoutSubviews() {
sortSalonsByDistanceAndReload()
}
func setupViews() {
view.addSubview(filterBar)
view.addSubview(tableView)
filterBar.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
filterBar.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
filterBar.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
filterBarHeightLC = filterBar.heightAnchor.constraint(equalToConstant: 44)
filterBarHeightLC?.isActive = true
tableView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
tableView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
tableView.topAnchor.constraint(equalTo: filterBar.bottomAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
}
I want to make the filterBar dissapear when the user scrolls down on the tableView, and then reappear when the user scrolls up
You can use Search Controller. Take a look at How to implement UISearchController in UITableView - SWIFT
if it doesn't work out for you , you could add as a section or part of header view
The delegate of your tableView should implement the UIScrollViewDelegate method scrollViewDidScroll: and query the contentOffset.y property on the scrollView (your tableView). Then it depends on when you want the bar to appear: only on the top, or also whenever the user scroll up inside the tableView. You could sync the contentOffset.y difference with the filterBars top constraint's constant (ranging from -44 to 0). It might be better to inset your tableView and update that inset as well (from 0 to 44).
If your filterBar could be implemented with a navigation bar from a UINavigationController you might just do navigationController?.hidesBarsOnSwipe = true, but I don't know your view controller setup.