UISearchBar width and position changes upon selection - ios

I'm having a really frustrating problem that I'm sure has a simple solution but for the life of me, I can't figure it out.
I have a UITableView within a UIViewController. On the toolbar, I have a button that can show/hide a Search Bar. Everything works great except for the annoying fact that the search bar, upon selection, shifts up 8 pixels (the original margin between the UITableView and the SuperView) and expands in width to equal the full superview.
I have kind of fixed the width issue with the function searchBarFrame(), however, it cuts the "Cancel" button in half, so it isn't perfect (See Below). I'd really appreciate any thoughts on these two problems. I have tried every combination of Extend Edges and Scroll View Insets based on other solutions I've found, but nothing is working for me. I really don't want to use the navigation bar as the search bar nor do I want to convert completely to a UITableViewController. There must be a way to make this work!
Here is my (relevant?) code:
class ListVC: UIViewController UISearchControllerDelegate, UISearchBarDelegate, UISearchResultsUpdating {
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var searchBtn: UIBarButtonItem!
let searchController: UISearchController = UISearchController(searchResultsController: nil)
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
}
func searchBarFrame() {
var searchBarFrame = searchController.searchBar.frame
searchBarFrame.size.width = tableView.frame.size.width
searchController.searchBar.frame = searchBarFrame
}
func showSearchController() {
searchController.isActive = true
searchBarFrame()
searchController.searchResultsUpdater = self
searchController.searchBar.delegate = self
searchController.dimsBackgroundDuringPresentation = false
searchController.hidesNavigationBarDuringPresentation = false
definesPresentationContext = true
searchController.searchBar.placeholder = "Search Places"
searchController.searchBar.roundCorners(corners: [.topLeft, .topRight, .bottomLeft, .bottomRight], radius: 5.0)
searchController.searchBar.barTintColor = UIColor.blurColor
tableView.tableHeaderView = searchController.searchBar
}
func hideSearchController() {
tableView.tableHeaderView = nil
searchController.isActive = false
}
#IBAction func onSearchBtnPress(sender: UIBarButtonItem) {
if !searchController.isActive {
showSearchController()
} else {
hideSearchController()
}
}

Following up, in case anyone else experienced this issue. After a lot of time and effort, I never got my original setup to work. Instead, I started from scratch and approached it differently.
In storyboard (you can do this programmatically too but I went the easier route because I was fed up), I put a UISearchBar inside a UIView inside a UIStackView. I set the Stackview's leading and trailing constraints to the uitableview, the bottom to the top of the uitableview and the top to the bottom of the top layout view. The UIView's only constraint is a height of 56 (the typical search bar height) with a priority of 999 (if you want to show and hide).
This fixed everything and the code was really simple too.
class MyVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
searchBar.delegate = self
searchView.isHidden = true
}
#IBAction func onSearchBtnPress(sender: UIBarButtonItem) {
if searchView.isHidden {
searchView.isHidden = false
} else {
searchView.isHidden = true
}
}
}
extension MyVC: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
// do something
}

I imagine that the use of NSLayoutConstraint to position and size your views would solve this issue.
For example:
private let margin: CGFloat = 15.0
tableView.translatesAutoresizingMaskIntoConstraints = false
searchBar.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: margin),
tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: margin),
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
searchBar.widthAnchor.constraint(equalTo: tableView.widthAnchor)
])

Related

UIKit layouts subviews even though it was not asked to

I was messing with layoutSubviews method in UIViewControllers. I assumed that when you override layoutSubviews on the view, it doesn't layout its subviews, but that wasn't the case, the view and its subviews were correctly laid out.
class Vieww: UIView {
override func didMoveToSuperview() {
super.didMoveToSuperview()
let centered = UIView()
addSubview(centered)
centered.translatesAutoresizingMaskIntoConstraints = false
backgroundColor = .green
centered.backgroundColor = .red
centered.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 1/2).isActive = true
centered.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 1/2).isActive = true
centered.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
centered.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
}
override func layoutSubviews() {
print("layoutSubviews")
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.layout(Vieww()).center().leading(20).trailing(20).height(400)
}
}
I expected that because I was not calling layoutSubviews, my views wouldn't be laid out, but I get layout like if I did override this method.
Read the docs: https://developer.apple.com/documentation/uikit/uiview/1622482-layoutsubviews
"the default implementation uses any constraints you have set to determine the size and position of any subviews. Subclasses can override this method as needed to perform more precise layout of their subviews."

UISearchBar with UISearchController at the top of the UIViewController breaks when tapping on it

I'm using UISearchController with its UISearchBar.
I'd like to put UISearchBar at the top of my UIViewController's view using AutoLayout:
import UIKit
class ViewController: UIViewController {
var searchController = UISearchController(searchResultsController: nil)
let myCustomSearchBar = UISearchBar()
var searchBar: UISearchBar {
// return searchController.searchBar
return myCustomSearchBar
}
override func viewDidLoad() {
super.viewDidLoad()
searchController.dimsBackgroundDuringPresentation = false
searchController.obscuresBackgroundDuringPresentation = false
searchController.hidesNavigationBarDuringPresentation = false
view.addSubview(searchBar)
configureSearchBarLayout()
searchBar.translatesAutoresizingMaskIntoConstraints = false
}
private func configureSearchBarLayout() {
[
searchBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
searchBar.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
searchBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
searchBar.heightAnchor.constraint(equalToConstant: 88)
]
.forEach{
$0.isActive = true
}
}
}
Which results in the following view being displayed:
However, as soon as I tap on the searhBar, it moves out of the screen completely to never reappear again.
If I modify the code to include a basic UISearchBar, not created by the UISearchController, it works fine:
Why does the UISearchController's SearchBar doesn't work correctly when I try to snap it to the top with AutoLayout and how to fix it?

Swift Add Button to UISearchBar / UITableView

I would like to add a button to the left of the search bar. However, I don't want the the scope titles to shift over as well, leaving me with a gap somewhere. I believe my options to be:
remove scope bar, use UISegmentedControl below, and add a button in
modify the UISearchBarClass with a button
?? container view as header, includes button and search controller ??
Here is my code, refactored to use a UIViewController for easy modification. Note that I am using the SnapKit libary for constraints (table cell logic removed). How can I accomplish this?
class TeamSearchController: UIViewController {
let tableView = UITableView()
var searchBar = UISearchBar()
override func viewDidLoad() {
super.viewDidLoad()
initUI()
}
func initUI() {
self.view.addSubview(tableView)
initSearchController()
initTableView()
}
func initSearchController() {
// searchController.dimsBackgroundDuringPresentation = false
searchBar.placeholder = "Search for a team here..."
searchBar.delegate = nil
searchBar.sizeToFit()
searchBar.showsScopeBar = true
searchBar.scopeButtonTitles = ["Cross Country", "Track"]
// searchController.definesPresentationContext = true
}
func initTableView() {
tableView.dataSource = nil
tableView.delegate = nil
tableView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
tableView.tableHeaderView = searchBar
tableView.estimatedRowHeight = 40
tableView.rowHeight = UITableView.automaticDimension
}
}
Current Status:

Implementing search bar in Navigationbar using UISearchController

I have implemented searchBar using UISearchController using following code -
var searchController = UISearchController(searchResultsController: nil)
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Search here..."
definesPresentationContext = true
searchController.searchBar.delegate = self
searchController.searchBar.sizeToFit()
if #available(iOS 11.0, *) {
self.navigationItem.searchController = searchController
} else {
// Fallback on earlier versions
navigationItem.titleView = searchController.searchBar
navigationItem.titleView?.layoutSubviews()
}
Now I have two issues-
SearchBar comes below the navigationBar(See the image attached), how do I get the searchBar on top of NavigationBar that used to come when we implement searchBar with UISearch bar.
The cancel button is not coming on the right side of search bar.
I don't think you can do this natively. But you can activate the search bar when you open the menu (dont forget to set searchController.hidesNavigationBarDuringPresentation to true):
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
searchController.isActive = true
}
But it will hide the UINavigationBar so this is not what you really want. So, maybe better, you can create a custom navigation bar and hide the native one. Here is a quick example:
1 - Create a swift a xib file NavigationBarView with an horizontal UIStackView, a back UIButton with a fixed width and a UISearchBar:
class NavigationBarView: UIView {
var backAction: (()->Void)?
#IBOutlet weak var searchBarView: UISearchBar!
override func awakeFromNib() {
super.awakeFromNib()
// Customize your search bar
self.searchBarView.showsCancelButton = true
}
#IBAction func backButtonPressed(_ sender: Any) {
self.backAction?()
}
}
2 - Instead of using a UITableViewController, create a UIViewController with a vertical UIStackView which contains a view with a fixed height of 64 and a UITableView:
class TableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate {
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var containerView: UIView!
let navigationBarView: NavigationBarView = NavigationBarView.viewFromNib() // Custom helper to instantiate a view, see below
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.navigationBar.isHidden = true // hide the native UINavigationBar
self.navigationBarView.backAction = {
self.navigationController?.popViewController(animated: true)
}
self.navigationBarView.searchBarView.delegate = self
self.navigationBarView.add(in: self.containerView) // Custom helper to put a view in a container view, see below
// Other stuff
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
}
Here is my helpers:
extension UIView {
static public func viewFromNib <GenericView: UIView> () -> GenericView {
let className = String(describing: self)
guard let instance = UINib(nibName: className, bundle: nil)
.instantiate(withOwner: nil, options: nil).first as? GenericView else {
// If this happens, it means the xcodeproj is broken
fatalError("Ho no its broken!")
}
return instance
}
func add(in superView: UIView) {
self.translatesAutoresizingMaskIntoConstraints = false
superView.addSubview(self)
self.topAnchor.constraint(equalTo: superView.topAnchor).isActive = true
self.bottomAnchor.constraint(equalTo: superView.bottomAnchor).isActive = true
self.leftAnchor.constraint(equalTo: superView.leftAnchor).isActive = true
self.rightAnchor.constraint(equalTo: superView.rightAnchor).isActive = true
}
}
Yo can try below code and please let me know if you are facing any issue.
if self.searchController != nil {
self.searchController.isActive = false
}
isSearching = true
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
self.navigationItem.titleView = searchController.searchBar
self.definesPresentationContext = false
self.searchController.searchBar.returnKeyType = .done
There is a property for this
searchController.hidesNavigationBarDuringPresentation = true
There is a gap, so it might be a white text Canel button. ou can know it for sure in Debugger Navigator (Cmd+7) -> View UI Hierarcy. White button text might be caused by custom navigation bar style

tableView.setContentOffset not working on UITableView embedded in UIViewcontroller

I had this code in a UITableViewController and it worked perfectly.
func setupSearchBar() {
let searchBar: UISearchBar = searchController.searchBar
tableView.tableHeaderView = searchBar
let point = CGPoint(x: 0, y: searchBar.frame.size.height)
tableView.setContentOffset(point, animated: true
}
Now I'm refactoring my code to fit more of an MVC style architecture. What I did is create a UITableView in the View class:
class View: UIView {
lazy var tableView: UITableView = {
let table = UITableView()
table.translatesAutoresizingMaskIntoConstraints = false
return table
}()
func configureView() {
// tableView
addSubview(tableView)
tableView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
tableView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
tableView.widthAnchor.constraint(equalTo: widthAnchor).isActive = true
tableView.heightAnchor.constraint(equalTo: heightAnchor).isActive = true
}
}
and then use the View class in my ViewController:
class ViewController: UIViewController {
var newView: View! { return self.view as! View }
override func loadView() {
view = View(frame: UIScreen.main.bounds)
newView.configureView()
}
override func viewDidLoad() {
super.viewDidLoad()
setupSearchBar()
}
func setupSearchBar() {
let searchBar: UISearchBar = searchController.searchBar
newView.tableView.tableHeaderView = searchBar
let point = CGPoint(x: 0, y: searchBar.frame.size.height)
newView.tableView.setContentOffset(point, animated: true)
}
The tableView shows up no problem and everything else is fine. The only thing that's not working is the setContentOffset is being called, but it's not offsetting the content. I want the searchbar to be hidden by default when the user first opens this viewController (similar to iMessage), but after I moved the code from a UITableViewController to separate files (UIView + UIViewController) like in this example, the searchbar always shows by default.
I'm not sure why it's not working. Any help would be greatly appreciated.
It's probably a timing problem relative to layout. Instead of calling setUpSearchBar in viewDidLoad, do it later, in viewDidLayoutSubviews, when initial layout has actually taken place. This method can be called many times, so use a flag to prevent it from being called more than once:
var didSetUp = false
override func viewDidLayoutSubviews() {
if !didSetUp {
didSetUp = true
setUpSearchBar()
}
}
Also: Your animated value is wrong:
newView.tableView.setContentOffset(point, animated: true)
You mean false. You don't want this movement to be visible. The table view should just appear with the search bar out of sight.

Resources