Swift 5 | UINavigationController not appearing until going to DetailsViewController - ios

I'm working on rebuilding my app programmatically instead of using storyboards and running into issues with my UINavigationController. When the app first loads the root ViewController, the bar containing "carrier" and the time is a light version of the color that I've assigned and the search bar exists instead of a normal navigation bar.
When I tap/click a cell on my UITableView to get to the details screen then go back, my UINavigationBar appears correctly but the search bar disappears. I'm sure I've confused something but I'm not sure where I messed up or if I am missing something entirely. Any and all help would be greatly appreciated. I've also included an image as an example of whats going on.
Nonexistant UINavigationController > Detail screen > Back.gif
I have setup a TabBarController in the SceneDelegate
SceneDelegate
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// MARK: - Properties
var window: UIWindow?
let tabBarDelegate = TabBarDelegate()
let userAuthToken = UserDefaults.standard.string(forKey: "token")
let userKeyToken = UserDefaults.standard.string(forKey: "key")
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Disable dark mode
if #available(iOS 13.0, *) {
window?.overrideUserInterfaceStyle = .light
}
// IF USER IS LOGGED IN
if let windowScene = (scene as? UIWindowScene) {
if let _ = userAuthToken {
self.window = UIWindow(windowScene: windowScene)
// CREATE TAB BAR //
let tabController = UITabBarController()
tabController.tabBar.backgroundColor = .white
// Instantiate the storyboards
let cannabisStoryboard = UIStoryboard(name: "Cannabis", bundle: nil)
let profileStoryboard = UIStoryboard(name: "Profile", bundle: nil)
// Instantiate the view controllers to storyboards
let cannabisVC = cannabisStoryboard.instantiateViewController(withIdentifier: "Cannabis") as! CannabisViewController
let profileVC = profileStoryboard.instantiateViewController(withIdentifier: "Profile") as! ProfileViewController
// Displays the items in below order in tab bar
let vcData: [(UIViewController, UIImage, UIImage)] = [
(cannabisVC, UIImage(named: "Cannabis_icon")!, UIImage(named: "Cannabis_icon_selected")!),
(profileVC, UIImage(named: "Profile_icon")!, UIImage(named: "Profile_icon_selected")!),
]
let vcs = vcData.map { (vc, defaultImage, selectedImage) -> UINavigationController in
let nav = UINavigationController(rootViewController: vc)
nav.tabBarItem.image = defaultImage
nav.tabBarItem.selectedImage = selectedImage
return nav
}
// Assign to tab bar controller
tabController.viewControllers = vcs
tabController.tabBar.isTranslucent = false
tabController.delegate = tabBarDelegate
// Disables rendering for tab bar images
if let items = tabController.tabBar.items {
for item in items {
if let image = item.image {
item.image = image.withRenderingMode(UIImage.RenderingMode.alwaysOriginal)
}
if let selectedImage = item.selectedImage {
item.selectedImage = selectedImage.withRenderingMode(UIImage.RenderingMode.alwaysOriginal)
}
// Hides title
item.imageInsets = UIEdgeInsets(top: 6, left: 0, bottom: -6, right: 0)
}
}
// Customize Navigation bar
UINavigationBar.appearance().backgroundColor = UIColor(rgb: 0x00ffcc)
// Disable dark mode
window!.overrideUserInterfaceStyle = .light
window?.rootViewController = tabController
window?.makeKeyAndVisible()
} else {
// let loginStoryboard = UIStoryboard(name: "Login", bundle: nil)
// let loginViewController = loginStoryboard.instantiateViewController(withIdentifier: "Login") as! LoginViewController
// Disable dark mode
window!.overrideUserInterfaceStyle = .light
// window?.rootViewController = loginViewController
window?.rootViewController = LoginViewController()
window?.makeKeyAndVisible()
}
window?.windowScene = windowScene
}
}
Root ViewController (stripped down to relevant info)
class CannabisViewController: UIViewController {
// MARK:- Outlets
let tableView = UITableView()
// MARK:- Properties
var cannabisDetailViewController: CannabisDetailsViewController? = nil
// Search
let searchController = UISearchController(searchResultsController: nil)
var isSearchBarEmpty: Bool { return searchController.searchBar.text?.isEmpty ?? true }
var filteredStrains = [Cannabis]()
var isFiltering: Bool { return searchController.isActive && !isSearchBarEmpty }
// MARK: - ViewWillLayoutSubViews
override func viewWillLayoutSubviews() {
let navigationBar: UINavigationBar = UINavigationBar(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: 44))
// navigationController?.setViewControllers([CannabisViewController()], animated: true)
self.view.addSubview(navigationBar)
}
// MARK: - ViewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
configurePage()
// MARK: Self sizing table view cell
tableView.estimatedRowHeight = CGFloat(88.0)
tableView.rowHeight = UITableView.automaticDimension
// MARK: DataSource/Delegate
tableView.dataSource = self
tableView.delegate = self
// Removes default lines from table views
tableView.tableFooterView = UIView()
tableView.separatorStyle = .none
// MARK: Navigation: logo in center
let logoHeader = UIImageView(image: UIImage(named: "logoHeader"))
self.navigationItem.titleView = logoHeader
// MARK: API
getCannabisList()
// MARK: Search bar controller
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Search for a strain!"
navigationItem.searchController = searchController
definesPresentationContext = true
}
// Configure TableView
func configurePage() {
// Configure Tableview
view.addSubview(tableView)
tableView.anchor(top: view.topAnchor, left: view.leftAnchor,
bottom: view.bottomAnchor, right: view.rightAnchor)
}
}
SearchController (Still in the root ViewController)
func updateSearchResults(for searchController: UISearchController) {
let searchBar = searchController.searchBar
let userSearch = searchBar.text!.trimmingCharacters(in: .whitespaces)
search(searchText: userSearch)
}

I can see a couple of issues here, but first to initialize your window in the SceneDelegate you would use the UIWindowScene:
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: windowScene)
self.window = window
let rootViewController = RootViewController()
window.rootViewController = UINavigationController(rootViewController: rootViewController)
window.makeKeyAndVisible()
}
If you want to disable Dark Mode globally in your app, simply add a key UIUserInterfaceStyle to your Info.plist and set its value to Dark (or Light). By doing this, you won't need to update each view controller because it will overwrite the global app default style. I highly encourage you to add support for Dark Mode though!
If you want to change your navigation bar appearance:
if #available(iOS 13.0, *) {
let navigationAppearance = UINavigationBarAppearance()
navigationAppearance.configureWithOpaqueBackground()
navigationAppearance.backgroundColor = .white
navigationAppearance.titleTextAttributes = // ...
navigationAppearance.largeTitleTextAttributes = // ...
UINavigationBar.appearance().tintColor = .systemBlue
UINavigationBar.appearance().barTintColor = .white
UINavigationBar.appearance().standardAppearance = navigationAppearance
UINavigationBar.appearance().scrollEdgeAppearance = navigationAppearance
} else {
UINavigationBar.appearance().backgroundColor = .white
UINavigationBar.appearance().barTintColor = .white
UINavigationBar.appearance().tintColor = .systemBlue
UINavigationBar.appearance().titleTextAttributes = // ...
UINavigationBar.appearance().largeTitleTextAttributes = // ...
}
I made a minimal project for you to see a working example with the search bar, in which the flickering of the status bar doesn't happen, and the UISearchBar's hide/show animation works properly when pushing/popping your DetailViewController:
RootViewController:
import UIKit
class RootViewController: UIViewController {
private let reuseIdentifier = "reuseIdentifier"
lazy var tableView: UITableView = {
$0.delegate = self
$0.dataSource = self
$0.register(UITableViewCell.self, forCellReuseIdentifier: reuseIdentifier)
return $0
}(UITableView(frame: .zero, style: .grouped))
private lazy var searchController: UISearchController = {
$0.searchResultsUpdater = self
$0.delegate = self
$0.searchBar.delegate = self
$0.obscuresBackgroundDuringPresentation = false
$0.hidesNavigationBarDuringPresentation = false
$0.searchBar.backgroundColor = .white
$0.searchBar.tintColor = .systemBlue
return $0
}(UISearchController(searchResultsController: nil))
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
setupConstraints()
}
func setupViews() {
title = "Source"
view.backgroundColor = .white
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
view.addSubview(tableView)
// ...
}
func setupConstraints() {
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
tableView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
tableView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
}
}
extension RootViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let detailViewController = DetailViewController()
navigationController?.pushViewController(detailViewController, animated: true)
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
return nil
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return CGFloat.leastNonzeroMagnitude
}
}
extension RootViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath)
cell.textLabel?.text = "Cell at indexPath \(indexPath)"
return cell
}
}
extension RootViewController: UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate {
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {}
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {}
func updateSearchResults(for searchController: UISearchController) {}
}
DetailViewController:
class DetailViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
title = "Detail"
view.backgroundColor = .systemGray6
}
}

Related

Activating a search controller whose navigation item is not at the top of the stack | Swift

The error:
[Assert] Surprise! Activating a search controller whose navigation item is not at the top of the stack. This case needs examination in UIKit. items = (null),
search hosting item = <UINavigationItem: 0x1068473a0> title='PARTS' style=navigator leftBarButtonItems=0x282bfb8d0 rightBarButtonItems=0x282bfb890 searchController=0x110024400 hidesSearchBarWhenScrolling
Why am I getting this error and how do I fix it? This question is similar to another post, but there was only one response to it and the response was not detailed at all (therefore not helpful).
import UIKit
import SPStorkController
struct Part {
var title: String?
var location: String?
var selected: Bool? = false
}
class InspectorViewController: UIViewController, UINavigationControllerDelegate, UITableViewDataSource, UITableViewDelegate, UISearchResultsUpdating, UISearchBarDelegate {
private let initials = InspectorPartsList.getInitials() // model
private var parts = InspectorPartsList.getDamageUnrelatedParts() // model
var filteredParts: [Part] = [] // model
var searching = false
var searchController = UISearchController(searchResultsController: nil)
lazy var searchBar: UISearchBar = UISearchBar()
var isSearchBarEmpty: Bool {
return searchController.searchBar.text?.isEmpty ?? true
}
var navBar = UINavigationBar()
let inspectorTableView = UITableView() // tableView
var darkTheme = Bool()
override func viewDidLoad() {
super.viewDidLoad()
// setup the navigation bar
setupNavBar()
// add the table view
setupInspectorTableView()
// add the search controller to the navigation bar
setupSearchController()
}
func setupNavBar() {
navBar = UINavigationBar(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 100))
view.addSubview(navBar)
let navItem = UINavigationItem(title: "PARTS")
let doneItem = UIBarButtonItem(barButtonSystemItem: .done, target: nil, action: #selector(self.addBtnTapped))
let cancelItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: nil, action: #selector(self.cancelBtnTapped))
navItem.rightBarButtonItem = doneItem
navItem.leftBarButtonItem = cancelItem
navItem.searchController = searchController
navBar.setItems([navItem], animated: false)
}
#objc func cancelBtnTapped() {
// dismiss the storkView
SPStorkController.dismissWithConfirmation(controller: self, completion: nil)
}
#objc func addBtnTapped() {
// get all of the selected rows
// Update the InspectionData model with the selected items... this will allow us to update the InspectionTableView in the other view
// create an empty array for the selected parts
var selectedParts = [Part]()
// loop through every selected index and append it to the selectedParts array
for part in parts {
if part.selected! {
selectedParts.append(part)
}
}
// update the InspectionData model
if !selectedParts.isEmpty { // not empty
InspectionData.sharedInstance.partsData?.append(contentsOf: selectedParts)
// update the inspectionTableView
updateInspectionTableView()
}
// dismiss the storkView
SPStorkController.dismissWithConfirmation(controller: self, completion: nil)
}
func cancelAddPart() {
// dismiss the storkView
SPStorkController.dismissWithConfirmation(controller: self, completion: nil)
}
private func setupInspectorTableView() {
// set the data source
inspectorTableView.dataSource = self
// set the delegate
inspectorTableView.delegate = self
// add tableview to main view
view.addSubview(inspectorTableView)
// set constraints for tableview
inspectorTableView.translatesAutoresizingMaskIntoConstraints = false
// inspectorTableView.topAnchor.constraint(equalTo: fakeNavBar.bottomAnchor).isActive = true
inspectorTableView.topAnchor.constraint(equalTo: navBar.bottomAnchor).isActive = true
inspectorTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
inspectorTableView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
inspectorTableView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
// allow multiple selection
inspectorTableView.allowsMultipleSelection = true
inspectorTableView.allowsMultipleSelectionDuringEditing = true
// register the inspectorCell
inspectorTableView.register(CheckableTableViewCell.self, forCellReuseIdentifier: "inspectorCell")
}
func setupSearchController() {
// add the bar
searchController.searchResultsUpdater = self
searchController.searchBar.delegate = self
searchController.hidesNavigationBarDuringPresentation = false
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Search by part name or location"
definesPresentationContext = true
searchController.searchBar.sizeToFit()
self.inspectorTableView.reloadData()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if searching {
return filteredParts.count
} else {
return parts.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let inspectorCell = tableView.dequeueReusableCell(withIdentifier: "inspectorCell", for: indexPath)
var content = inspectorCell.defaultContentConfiguration()
var part = Part()
if searching {
// showing the filteredParts array
part = filteredParts[indexPath.row]
if filteredParts[indexPath.row].selected! {
// selected - show checkmark
inspectorCell.accessoryType = .checkmark
} else {
// not selected
inspectorCell.accessoryType = .none
}
} else {
// showing the parts array
part = parts[indexPath.row]
if part.selected! {
// cell selected - show checkmark
inspectorCell.accessoryType = .checkmark
} else {
// not selected
inspectorCell.accessoryType = .none
}
}
content.text = part.title
content.secondaryText = part.location
inspectorCell.contentConfiguration = content
return inspectorCell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// Note: When you select or unselect a part in the filteredParts array, you must also do so in the parts array
if searching { // using filteredParts array
if filteredParts[indexPath.row].selected! { // selected
filteredParts[indexPath.row].selected = false // unselect the part
// search the parts array for the part by both the title and location, so we for sure get the correct part (there could be parts with identical titles with different locations)
if let part = parts.enumerated().first(where: { $0.element.title == filteredParts[indexPath.row].title && $0.element.location == filteredParts[indexPath.row].location}) { // exact part (with same title & location) found
parts[part.offset].selected = false // unselect the part
}
} else { // not selected
filteredParts[indexPath.row].selected = true // select the part
if let part = parts.enumerated().first(where: { $0.element.title == filteredParts[indexPath.row].title && $0.element.location == filteredParts[indexPath.row].location}) { // exact part (with same title & location) found
parts[part.offset].selected = true // select the part
}
}
} else { // using parts array
if parts[indexPath.row].selected! { // selected
parts[indexPath.row].selected = false // unselect the part
} else { // not selected
parts[indexPath.row].selected = true // select the part
}
}
inspectorTableView.reloadRows(at: [indexPath], with: .none) // reload the tableView
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
filteredParts = parts.filter { ($0.title?.lowercased().prefix(searchText.count))! == searchText.lowercased() }
searching = true
inspectorTableView.reloadData()
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
searching = false
searchBar.text = ""
inspectorTableView.reloadData()
}
func updateSearchResults(for searchController: UISearchController) {
}
private func updateInspectionTableView() {
NotificationCenter.default.post(name: NSNotification.Name("updateInspectionTable"), object: nil)
}
}
// CHECKABLE UITABLEVIEWCELL
class CheckableTableViewCell: UITableViewCell {
}

Swift: Update Button.setTitle and current UIPageView with NSNotification

Swift 4: I'm using NSNotification post and observe an object between two view controller. My issue is updating the viewController that observes given the received object.
There are two things that needs updating 1. setTitle of a button, 2. the current page on UIPageViewController. How do i update these two things?
I've worked on this the whole day today and yesterday, with no results.
I've tried using struct to do the updating and normal variables.
**YellowViewController - Observes -> Issue here**
let searchedReturnedKey = "Searched"
class YellowViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
let pageView = PageView()
let searched = Notification.Name(rawValue: searchedReturnedKey)
private var isAnimating = false
struct structure {
static var SearchedIndex = Int()
}
override func viewDidLoad() {
super.viewDidLoad()
dataSource = self
delegate = self
setupUIPageView()
setupNavigationBarItem()
createObserver()
}
func createObserver(){
NotificationCenter.default.addObserver(self, selector: #selector(YellowViewController.updateVerseView(notification:)), name: searched, object: nil)
}
#objc func updateVerseView(notification: NSNotification) {
print("Observed")
structure.SearchedIndex = notification.object as! Int
let chapterIndexLabel = doneModel.ChapterIndex
let verseList = doneModel.chapterVerses
let pageView = PageView()
// HERE IS MY ISSUE - booklabel.text and pageview.verses
bookLabel.text = chapterIndexLabel[structure.SearchedIndex]
pageView.verses = verseList[structure.SearchedIndex]
print(structure.SearchedIndex, "Index")
}
func setupUIPageView() {
let verseList = bibleModel.chapterVerses
let chapterIndexLabel = bibleModel.ChapterIndex
let pageViewControllers = [pageView] // Important! UIPageViewController sets ViewController in a list to enable swiping. - Understand this.
pageView.verses = verseList.first! // Important! Setups verses for first view.
bookLabel.text = chapterIndexLabel.first!
setViewControllers(pageViewControllers, direction: .forward, animated: true, completion: nil)
}
var bookLabel = UILabel()
func setupLeftNavItems() {
let bookButton = UIButton(type: .system)
bookButton.setTitle(bookLabel.text! + " 1", for: .normal)
bookButton.setTitleColor(goldColor, for: .normal)
bookButton.titleLabel?.font = UIFont(name: "AvenirNext-DemiBold", size: 18)
bookButton.addTarget(self, action: #selector(handleSearch), for: .touchUpInside)
// bookButton.frame = CGRect(x: 30, y: 0, width: 54, height: 34)
self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: bookButton)
}
#objc func handleSearch() { // Handles openeing SearchViewController
let searchViewController = SearchViewController()
let navController = UINavigationController(rootViewController: searchViewController)
present(navController, animated: true, completion: nil)
}
func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
isAnimating = true
}
// Handles swiping right - To next chapter.
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
if isAnimating {
return nil
}
let verseList = bibleModel.chapterVerses
let currentVerseView = (viewController as! PageView).verses
let currentIndex = verseList.index(of: currentVerseView)
let chapterIndexLabel = doneModel.ChapterIndex
if currentIndex! < verseList.count - 1 {
let pageView = PageView()
pageView.verses = verseList[currentIndex! + 1 ]
bookLabel.text = chapterIndexLabel[currentIndex! + 1]
print("Swiped right")
setupLeftNavItems()
createObserver()
return pageView
}
return nil
}
// Handles swiping left - To previous chapter
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
if isAnimating {
return nil
}
let verseList = bibleModel.chapterVerses
let currentVerseView = (viewController as! PageView).verses
let currentIndex = verseList.index(of: currentVerseView)
let chapterIndexLabel = doneModel.ChapterIndex
if currentIndex! > 0 {
let pageView = PageView()
pageView.verses = verseList[currentIndex! - 1]
bookLabel.text = chapterIndexLabel[currentIndex! - 1]
setupLeftNavItems()
print("Swiped left")
createObserver()
return pageView
}
return nil
}
}
SearchViewController - Post
class SearchViewController: UITableViewController, UISearchBarDelegate {
let cellId = "cellId"
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.titleView = navSearchBar
setupView()
}
func setupView() {
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellId)
tableView.delegate = self
tableView.dataSource = self
navSearchBar.delegate = self
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return Books.count
}
**I post the data from this function**
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath)
let cellLabelContent = cell!.textLabel!.text
let cellLabelIndex = Books.firstIndex(of: cellLabelContent!)
print("Book name:", cellLabelContent!, "index:", cellLabelIndex!)
let notificationName = Notification.Name(rawValue: searchedReturnedKey)
NotificationCenter.default.post(name: notificationName, object: cellLabelIndex)
dismiss(animated: true, completion: nil)
}
}
Actual result -> No update
Expected result -> Update given object
To be able to update the current view of your UIPageViewController, you would need to do this inside your updateVerseView. The setViewControllers method is correct and efficient method to update a view inside a UIPageViewController.
let pageViewControllers = [pageView]
pageView.verses = verseList[structure.searchedIndex]
setViewControllers(pageViewControllers, direction: .forward, animated: true, completion: nil)
To update your bookLabel.txt you would just need to call your setupLeftNavItems() function.
Here you need toaddObserver with your selector #selector(YellowViewController.updateVerseView(notification:)
I think you set wrong ViewController name

iOS - How to dismiss keyboard from the navigationItem.searchController when tap anywhere on the UIView?

I have implemented the new SearchController with its searchBar and the searchResultsController.
Here is how I implemented it :
The resultViewController:
lazy var resultViewController: SearchResultViewController = {
let storyboard = UIStoryboard.init(name: "Main", bundle: nil)
let searchResultViewController = storyboard.instantiateViewController(withIdentifier: "SearchResultViewController") as! SearchResultViewController
searchResultViewController.delegate = self
return searchResultViewController
}()
And this is the SearchController:
lazy var searchController: UISearchController = {
let searchController = UISearchController(searchResultsController: resultViewController)
searchController.searchBar.delegate = self
searchController.obscuresBackgroundDuringPresentation = true
searchController.searchResultsUpdater = self
searchController.searchBar.placeholder = "Search.city.label".localizable()
searchController.searchBar.tintColor = UIColor.white
searchController.searchBar.barTintColor = UIColor.white
UITextField.appearance(whenContainedInInstancesOf: [type(of: searchController.searchBar)]).tintColor = UIColor(red:0.00, green:0.47, blue:0.78, alpha:1.0)
if let textfield = searchController.searchBar.value(forKey: "searchField") as? UITextField {
if let backgroundview = textfield.subviews.first {
// Background color
backgroundview.backgroundColor = UIColor.white
// Rounded corner
backgroundview.layer.cornerRadius = 10;
backgroundview.clipsToBounds = true;
}
}
definesPresentationContext = true
return searchController
}()
In my viewWillAppear I set the navigationItem.searchController :
self.searchController.isActive = true
if #available(iOS 11.0, *) {
self.navigationItem.searchController = searchController
self.navigationItem.hidesSearchBarWhenScrolling = true
} else {
// Fallback on earlier versions
}
I have been able to handle the cancelButtonClicked :
extension HomeViewController: UISearchBarDelegate {
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
searchBar.endEditing(true)
self.searchController.isActive = false
}
}
This is doing the "cancel" animation, hiding keyboard + inactive state on searchBar/searchController. Both at the same time, with 1 tap on cancel Button.
But I am unable to achieve this when the user tap anywhere on the view.
I tried with tap gesture but it requires me 2 tap to achieve the same behavior.
NB:
I got an UICollectionView in my UIViewController, which takes all the place in the UIView.
Here is what I have tried :
override func viewDidLoad() {
super.viewDidLoad()
handleTapAnywhereToRemoveKeyboard()
}
func handleTapAnywhereToRemoveKeyboard() {
let singleTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.singleTap(sender:)))
//singleTapGestureRecognizer.numberOfTapsRequired = 1
singleTapGestureRecognizer.cancelsTouchesInView = false
self.view.addGestureRecognizer(singleTapGestureRecognizer)
}
#objc func singleTap(sender: UITapGestureRecognizer) {
self.searchController.isActive = false
self.searchController.searchBar.resignFirstResponder()
self.searchController.searchBar.endEditing(true)
}
EDIT:
I was thinking, maybe it's because my searchBar and searchController aren't in the UIViewController's view hierarchy, but more in the NavigationController one.
So I also tried with :
navigationController?.view.endEditing(true)
I then was thinking, maybe it's because the UIScrollView within my UICollectionView is catching the tap.
So I tried to link the tap gesture on the UICollectionView instead of the UIView, but without success.
There is no need to add UITapGestureRecognizer as proposed above. UIViewContoller already conforms to UIResponder interface (legacy from Objective C), so you can override this method like this:
extension UIViewController {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.view.window?.endEditing(true)
super.touchesEnded(touches, with: event)
}
}
Please create UIViewController for dismiss keyboard througout application.
extension UIViewController {
func hideKeyboardWhenTappedAround() {
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(UIViewController.dismissKeyboard))
tap.cancelsTouchesInView = false
view.addGestureRecognizer(tap)
}
#objc func dismissKeyboard() {
view.endEditing(true)
}
}
Use above code in your UIViewController file as below.
override func viewDidLoad() {
super.viewDidLoad()
self.hideKeyboardWhenTappedAround()
}
Update:
You can also use below code to dismiss keyboard from any class.
UIApplication.shared.sendAction(#selector(UIResponder.resign‌​FirstResponder), to: nil, from: nil, for: nil)
Try the solution from Dimple Desai but add the gesture recognizer to the TableView or CollectionView or whatever is laying on top of your UIView. Then it should work.
override func viewDidLoad(){
let tapView = UITapGestureRecognizer(target: self, action: #selector(self.hideKeyboard))
self.view.addGestureRecognizer(tapView)
}
#objc func hideKeyboard(tap: UITapGestureRecognizer){
self.view.endEditing(true)
}

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

Using Page View Controller inside Tab Swift

MY WORK SO FAR:
So I have a Tab bar that looks like this:
When I click on "Canteen" I want to be directed to a Page View Controller where I can swipe between different pages but stay on the same tab.
I have this somewhat working:
I have the Storyboard setup like this:
As you can see that segue above is coming from the Tab Bar Controller.
The third view (Can Page Item Controller, ID: "CanItemController) is used for all pages in the page view.
The second view above (Page View Controller, ID: "CanPageController) is used for controlling the Pages (duh)
The first view (CanteenViewController) contains all the code and makes all the connections. This is where everything goes on. The code inside this class is here:
import UIKit
class CanteenViewController: UIViewController, UIPageViewControllerDataSource {
// MARK: - Variables
private var pageViewController: UIPageViewController?
private let contentImages = ["Radar-512.png",
"dartp.png",
"roomp.png",
"abnews.png",
"canteenp.png"];
override func viewDidLoad() {
super.viewDidLoad()
createPageViewController()
setupPageControl()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
private func createPageViewController() {
let pageController = self.storyboard!.instantiateViewControllerWithIdentifier("CanPageController") as! UIPageViewController
pageController.dataSource = self
if contentImages.count > 0 {
let firstController = getItemController(0)!
let startingViewControllers: NSArray = [firstController]
pageController.setViewControllers(startingViewControllers as [AnyObject], direction: UIPageViewControllerNavigationDirection.Forward, animated: false, completion: nil)
}
pageViewController = pageController
addChildViewController(pageViewController!)
self.view.addSubview(pageViewController!.view)
pageViewController!.didMoveToParentViewController(self)
}
private func setupPageControl() {
let appearance = UIPageControl.appearance()
appearance.pageIndicatorTintColor = UIColor.grayColor()
appearance.currentPageIndicatorTintColor = UIColor.whiteColor()
appearance.backgroundColor = UIColor.darkGrayColor()
}
// MARK: - UIPageViewControllerDataSource
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
let itemController = viewController as! CanPageItemController
if itemController.itemIndex > 0 {
return getItemController(itemController.itemIndex-1)
}
return nil
}
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
let itemController = viewController as! CanPageItemController
if itemController.itemIndex+1 < contentImages.count {
return getItemController(itemController.itemIndex+1)
}
return nil
}
private func getItemController(itemIndex: Int) -> CanPageItemController? {
if itemIndex < contentImages.count {
let pageItemController = self.storyboard!.instantiateViewControllerWithIdentifier("CanItemController") as! CanPageItemController
pageItemController.itemIndex = itemIndex
pageItemController.imageName = contentImages[itemIndex]
return pageItemController
}
return nil
}
// MARK: - Page Indicator
func presentationCountForPageViewController(pageViewController: UIPageViewController) -> Int {
return contentImages.count
}
func presentationIndexForPageViewController(pageViewController: UIPageViewController) -> Int {
return 0
}
}
I HAVE 2 PROBLEMS:
I can't see any page indicators at all.
This comes from the following code:
private func setupPageControl() {
let appearance = UIPageControl.appearance()
appearance.pageIndicatorTintColor = UIColor.grayColor()
appearance.currentPageIndicatorTintColor = UIColor.whiteColor()
appearance.backgroundColor = UIColor.darkGrayColor()
}
Is there a way I can add a page indicator in the storyboard and reference that programatically. That way maybe I could add constraints and have more control. I think the page indicator might be hidden behind the Tab Bar. Though constraints are also giving me issues, which leads me to problem 2
As you can see in the Item Controller, I have a UIImageView and the constraints are all set right. But when I run the app the image appears for a second (completely out of proportion) and then disappears. i.e - my constraints simply don't work properly
Question
Is my approach in general just wrong? Or is there a few little changes I can make to fix the above problems. I I've been following a tutorial (on Ray Wenderlich I think), and it all worked fine until I tried to integrate it with my Tab Bar.
Leave all above thing, just do as following.
Edited : As per Swift 5
class CanteenViewController: UIViewController, UIScrollViewDelegate {
#IBOutlet var scrHelp: UIScrollView!
#IBOutlet var pageControl: UIPageControl!
var page = 0
let arrContent: [[String: Any]] = [["name" : "Title1", "icon" : "Radar-512"],
["name" : "Title2", "icon" : "dartp"],
["name" : "Title3", "icon" : "roomp"],
["name" : "Title4", "icon" : "abnews"],
["name" : "Title5", "icon" : "canteenp"]]
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Canteen"
// Do any additional setup after loading the view, typically from a nib.
self.createHelpView()
self.pageControl.backgroundColor = UIColor.clear
self.pageControl.pageIndicatorTintColor = UIColor.lightGray
self.pageControl.currentPageIndicatorTintColor = UIColor(red: 251/255, green: 108/255, blue: 108/255, alpha: 1.0)
self.pageControl.tintAdjustmentMode = UIView.TintAdjustmentMode.dimmed
self.pageControl.numberOfPages = self.arrContent.count
self.pageControl.currentPage = 0
}
func createHelpView() {
var x = 50
var i = 0
for item in self.arrContent {
let lblTitle = UILabel(frame: CGRect(origin: CGPoint(x: CGFloat(x), y: 10), size: CGSize(width: CGFloat(self.scrHelp.frame.width-100), height: 25)))
lblTitle.autoresizingMask = UIView.AutoresizingMask.flexibleBottomMargin
lblTitle.backgroundColor = UIColor.clear
lblTitle.font = UIFont.systemFont(ofSize: 17)
lblTitle.textAlignment = NSTextAlignment.center
lblTitle.textColor = UIColor.black
lblTitle.text = item["name"] as? String //self.arrTitle[i]
self.scrHelp.addSubview(lblTitle)
let imgView = UIImageView(frame: CGRect(origin: CGPoint(x: CGFloat(x), y: 50), size: CGSize(width: CGFloat(self.scrHelp.frame.width-100), height: CGFloat(self.scrHelp.frame.height-150))))
imgView.autoresizingMask = UIView.AutoresizingMask.flexibleBottomMargin
imgView.backgroundColor = UIColor.clear
imgView.image = UIImage(named: (item["icon"] as! String))
imgView.contentMode = UIView.ContentMode.scaleAspectFit
self.scrHelp.addSubview(imgView)
x = x + Int(self.scrHelp.frame.width)
i = i + 1
}
self.scrHelp.contentSize = CGSize(width: (CGFloat(self.arrContent.count) * self.view.frame.width), height: 0)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let pageWidth = CGFloat(self.scrHelp.frame.width)
let fractionalPage = self.scrHelp.contentOffset.x / pageWidth
self.page = lround(CDouble(fractionalPage))
self.pageControl.currentPage = self.page
}
}
At last, add UIScrollView and UIPageControl to you storyboard and set respective outlet and constraint.

Resources