I am using a UISearchController search bar as the title view of my navigation bar in a modal view controller. I have it set up like this:
var searchController: UISearchController!
override func viewDidLoad() {
super.viewDidLoad()
searchController = UISearchController(searchResultsController: nil)
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
searchController.hidesNavigationBarDuringPresentation = false
searchController.searchBar.showsCancelButton = false
navigationItem.titleView = searchController.searchBar
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// for whatever reason, it's necessary to make the search controller first responder
// on the main queue. reference: https://stackoverflow.com/a/41657181/2335677
DispatchQueue.main.async {
self.searchController.searchBar.becomeFirstResponder()
}
}
Everything is working great, except dismiss() and swipe-to-dismiss the modal aren't working when searchController.isActive = true
I can get around the dismiss() issue by setting it to inactive first:
#IBAction private func done(_ sender: UIBarButtonItem) {
searchController.isActive = false
dismiss(animated: true)
}
But I can't swipe down to dismiss the view controller as I mentioned. And I can't think of a workaround. I tried:
Setting isModalInPresentation = false (not an option because my app target is iOS12)
Playing with UIAdaptivePresentationControllerDelegate methods and trying to set searchController.isActive = false in some of those (didn't work)
Setting definesPresentationContext = true (doesn't make a difference)
Does anyone have any other ideas?
If you want a separate screen just for searching and you just need a search bar in your navigation bar title like I did, it's easier to use a UISearchBar instead:
var searchBar: UISearchBar!
override func viewDidLoad() {
super.viewDidLoad()
searchBar = UISearchBar()
searchBar.showsCancelButton = false
navigationItem.titleView = searchBar
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
searchBar.becomeFirstResponder()
}
It looks the same and you get all the default behavior now since UISearchController isn't hijacking your view controller.
Related
My intention is to initialize a view controller and set it to the window rootViewController, then pass the view controller as dependency to an AppCoordinator where the app decide what to do with the view controller (replace it with an onboarding scene or login page, or even home screen if the user is already logged in.
The problem is that whenever I do that, the window doesn't seems to get back its view controller, but instead a black screen.
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
self.window = UIWindow(windowScene: windowScene)
var viewController = UIViewController()
self.window?.rootViewController = viewController
let appCoordinator = AppCoordinator(viewController: viewController)
appCoordinator.start()
window?.makeKeyAndVisible()
}
AppCoordinator.swift
import UIKit
import SwiftUI
class AppCoordinator: Coordinator {
var viewController: UIViewController
init(viewController: UIViewController) {
self.viewController = viewController
}
func start() {
let onboardingView = OnboardingView()
self.viewController = UIHostingController(rootView: onboardingView)
}
}
If I bypass the AppCoordinator and set everything in the scene delegate, the screen loads correctly:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
self.window = UIWindow(windowScene: windowScene)
var viewController = UIViewController()
let onboardingView = OnboardingView()
self.window?.rootViewController = UIHostingController(rootView: onboardingView)
window?.makeKeyAndVisible()
}
What seems to be wrong when I pass the view controller as a dependency? Btw, I don't prefer to pass the window as dependency to the called coordinator, although this works, I don't prefer it.
Coordinator pattern is an old topic with many libraries trying to solve it and I am learning it in simple example app.
My current set up is 3 rootViewControlers: LoadingStateCoordinator, WelcomeCoordinator, TabBarCoordinator but missing connection between UIKit and coordinators. I am trying to implement it with a UINavigationController but the button is not being called. I need a way to connect to back button and a reusable coordinator that I could push to and dellocate accordingly (that is without RxSwift).*Set up Welcome screen as the parent/main navigation and always be able to come back to it.**
So after user selects a form from modal view (vertical flow) presented I show on a push a TabBarCoordinator (horizontal). All viewControllers have empty.storyboard, UIViewController and Coordinator exept the TabBar.Here I only have a coordinator due to the set up of child tab coordinators and the magic needs to happen on a back button tap. Currenly this only being called when user comes from LoadingStateCoordinator. There I need to send the user back to the Welcome screen so they can change the onboarding set up. Here is the first code for LoadingStateCoordinator:
final class LoadingStateCoordinator: NSObject, Coordinator {
*// MARK: - Inputs required*
var childCoordinators: [Coordinator]
var presenter: UINavigationController
private let window: UIWindow
*// MARK: - Initialization*
init(window: UIWindow) {
self.window = window
childCoordinators = []
presenter = UINavigationController()
}
*// MARK: - Coordinator*
func start() {
let controller: LoadingStateViewController = LoadingStateViewController.instantiate()
window.rootViewController = controller
controller.delegate = self
}
}
*// MARK: - LoadingViewControllerDelegate*
extension LoadingStateCoordinator : LoadingViewControllerDelegate {
func performScreenSwitch() {
if UserDefaults.standard.userWasHere == false {
let tabCoordinator: TabBarCoordinator = TabBarCoordinator(window: window, tabBarController: UITabBarController())
window.rootViewController = presenter
addChildCoordinator(tabCoordinator)
tabCoordinator.start()
presenter.pushViewController(tabCoordinator.tabBarController!, animated: true)
} else {
let welcomeCoordinator = WelcomeCoordinator(window: window, presenter: presenter)
window.rootViewController = welcomeCoordinator.presenter
addChildCoordinator(welcomeCoordinator)
welcomeCoordinator.start()
}
}
}
And here is the TabBarCoordinator that need to perform back to Welcome screen action. When I present popToRootfunction it pushes the Welcome screen but all the button there are disbled. I guess to be retain cycle issue. Do I need funadametally another set up? Is there a way to popToRoot(vc) in this set up? What I tryed ended with runtime error "poping to non existing controller".
TabBarCoordinator code that need to perform this:
final class TabBarCoordinator: NSObject, Coordinator {
internal var presenter: UINavigationController
internal var tabBarController: UITabBarController?
internal var childCoordinators: [Coordinator]
var parentCoordinator: LoadingStateCoordinator?
lazy var leftBtn: UIBarButtonItem = {
let button = UIButton(type: .system)
button.setImage(UIImage(systemName: "arrow.turn.up.left"), for: .normal)
button.sizeToFit()
button.addTarget(self,
action: #selector(self.popToRoot(_:)),
for: .touchUpInside)
return UIBarButtonItem(customView: button)
}()
init(window: UIWindow, tabBarController: UITabBarController) {
self.tabBarController = tabBarController
childCoordinators = []
self.presenter = UINavigationController()
}
func start() {
performGetTabBar()
self.presenter.delegate = self
}
private func performGetTabBar() {
let coordinators: [Coordinator] = generateTabCoordinators()
coordinators.forEach({ coordinator in
coordinator.start()
addChildCoordinator(coordinator)
})
let presenters: [UIViewController] = coordinators.map({ coordinator -> UIViewController in
return coordinator.presenter
})
leftBtn.style = .plain
tabBarController?.navigationItem.leftBarButtonItem = leftBtn
tabBarController?.setViewControllers(presenters, animated: false)
selectTab(type: SurfTripCoordinator.self)
}
private func generateTabCoordinators() -> [Coordinator] {
let calculatorCoordinator: CalculatorCoordinator = CalculatorCoordinator(presenter: UINavigationController())
let tripCoordinator: SurfTripCoordinator = SurfTripCoordinator(presenter: UINavigationController())
let sellCoordinator: SavedTripsCoordinator = SavedTripsCoordinator(presenter: UINavigationController())
return [calculatorCoordinator, tripCoordinator, sellCoordinator]
}
*//this is not being called when coming from vertical flow*
#objc func popToRoot(_ sender: UIBarButtonItem) {
let storyboard: UIStoryboard = UIStoryboard(name: Constants.Storyboards.welcomeViewCoordinator, bundle: nil)
let controller: WelcomeViewController = WelcomeViewController.instantiate(from: storyboard)
tabBarController?.navigationController?.pushViewController(controller, animated: true)
}
}
extension TabBarCoordinator: UINavigationControllerDelegate {
func selectTab<T: Coordinator>(type _: T.Type) {
guard let index = childCoordinators.firstIndex(where: { coordinator in
coordinator is T
}) else {
return
}
tabBarController?.selectedIndex = index
}
}
and here is the current WelcomeCoordinator set up
class WelcomeCoordinator: NSObject, Coordinator {
internal var presenter: UINavigationController
var childCoordinators: [Coordinator]
init(window: UIWindow, presenter: UINavigationController) {
self.presenter = presenter
childCoordinators = []
}
func start() {
let storyboard: UIStoryboard = UIStoryboard(name: Constants.Storyboards.welcomeViewCoordinator, bundle: nil)
let controller: WelcomeViewController = WelcomeViewController.instantiate(from: storyboard)
controller.delegate = self
presenter.pushViewController(controller, animated: true)
}
}
extension WelcomeCoordinator : WelcomeViewControllerDelegate {
func performAddLevel() {
let addLevelCoordinator: AddLevelViewCoordinator = AddLevelViewCoordinator(presenter: UINavigationController())
addLevelCoordinator.start()
addChildCoordinator(addLevelCoordinator)
addLevelCoordinator.presenter.modalPresentationStyle = .fullScreen
presenter.present(addLevelCoordinator.presenter, animated: true, completion: nil)
}
}
sorry for the long post I wish there was more reaktive native way to do this...
Ok so I found partlly a solution the back button solution for my case: not using pushViewController or show because it comes with back button. presenter.setViewControllers([tabCoordinator.tabBarController!], animated: true) and there setting the navBar to hidden. I made my own navItem button to navigate to rootVC. Next step to allocate and remove all child tabBar coordinators on back tap recognized.
I’m attaching a UISearchController to the navigationItem.searchController on iOS 13. This works fine: I can use the nice iOS 13-style search bar.
However, I’d like see the large titles and searchBar by default.
I set navigationItem.hidesSearchBarWhenScrolling = false because I want to see the search permanently on my screen, but the search bar replace large titles by default.
Does anyone know how is this is possible?
Check this out
navigationItem.searchController = UISearchController(searchResultsController: nil)
navigationItem.hidesSearchBarWhenScrolling = false
This is how it looks actually
This is how I need to implement(large title and search bar both visible)
For me it worked after adding following lines in the viewDidLoad() method:
searchController.hidesNavigationBarDuringPresentation = true
navigationController?.navigationBar.prefersLargeTitles = true
navigationController!.navigationBar.sizeToFit()
Try this, working fine in my side
private var search = UISearchController(searchResultsController: nil)
override func viewDidLoad() {
super.viewDidLoad()
search.searchBar.delegate = self
search.searchBar.sizeToFit()
search.obscuresBackgroundDuringPresentation = false
search.hidesNavigationBarDuringPresentation = true
self.definesPresentationContext = true
search.searchBar.placeholder = "Search here"
self.navigationItem.searchController = search
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationItem.hidesSearchBarWhenScrolling = false
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationItem.hidesSearchBarWhenScrolling = true
}
For large navigation bar use this
For full application navigation bar support please add this extension inside your code.
import UIKit
extension UIViewController {
open func showNavigationBar(_ large: Bool,
_ animated: Bool,
titleColor: UIColor,
barTintColor: UIColor,
fontSize: CGFloat) {
navigationController?.navigationBar.barTintColor = barTintColor
navigationController?.navigationBar.backgroundColor = barTintColor
navigationController?.navigationBar.isTranslucent = true
self.navigationController?.setNavigationBarHidden(false, animated: animated)
if large {
self.navigationController?.navigationBar.prefersLargeTitles = true
if #available(iOS 13.0, *) {
let appearance = UINavigationBarAppearance()
appearance.backgroundColor = barTintColor
appearance.titleTextAttributes = [.foregroundColor: titleColor]
appearance.largeTitleTextAttributes = [NSAttributedString.Key.foregroundColor: titleColor,
NSAttributedString.Key.font: UIFont(resource: R.font.robotoMedium, size: fontSize)!]
navigationController?.navigationBar.standardAppearance = appearance
navigationController?.navigationBar.compactAppearance = appearance
navigationController?.navigationBar.scrollEdgeAppearance = appearance
} else {
self.navigationController?.navigationBar.largeTitleTextAttributes = [NSAttributedString.Key.foregroundColor: titleColor,
NSAttributedString.Key.font: UIFont(resource: R.font.robotoMedium, size: fontSize)!]
}
} else {
self.navigationController?.navigationBar.prefersLargeTitles = false
self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: titleColor,
NSAttributedString.Key.font: UIFont(resource: R.font.robotoMedium, size: 20.0)!]
}
}
}
And Then call this method simply
self.showNavigationBar(true, true, titleColor: UIColor.blue, barTintColor: UIColor.red, fontSize: 32.0)
If then Also not work then use this
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
search.searchBar.becomeFirstResponder()
}
one more solution is that add one UIView with height 0 in storyboard and set-top with safe area and bottom with UIScrollView/UICollectionView/UITableVIew or something else scrollable view and Remove Direct Constraint between TopSafeArea And ScrollableView Top. I know maybe this is not a solution but I did in a storyboard.
I've been trying to achieve the same thing all day long for my app as well and I finally did it.
I wanted to add a searchBar on a UITableViewController and I did it this way.
let searchController: UISearchController = {
let searchController = UISearchController(searchResultsController: nil)
searchController.searchBar.placeholder = "New Search"
searchController.searchBar.searchBarStyle = .minimal
searchController.dimsBackgroundDuringPresentation = false
searchController.definesPresentationContext = true
return searchController
}()
You first create a new UISearchController using a closure, that way you are able to use it globally in your code and customize it easier in the future.
Afterwards in viewDidLoad, you set the searchSontroller.searchResultsUpdater = self and the navigationItem.searchController = searchController.
For me it works perfectly after a lot of trial and error since I'm doing everything programmatically.
This code should work
class NavigationController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
createCustomTabBar()
}
func createCustomTabBar() {
let firstVC = UINavigationController(rootViewController: HomeVC())
firstVC.title = "Home"
firstVC.tabBarItem.image = UIImage(systemName: "house.fill")
viewControllers = [firstVC]
}
class HomeVC: UIViewController {
let searchController = UISearchController(searchResultsController: nil)
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
navigationController?.navigationBar.prefersLargeTitles = true
navigationItem.title = "Home"
navigationItem.searchController = searchController
}
}
My code below is causing a run time error. This works If I code a lazy var as a scrollview. But this does not work if I am just trying to add a object to the view or superview. I have connected nothing from the storyboard and do not want to.
var FIRE: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
playSound()
view.addSubview(FIRE)
FIRE.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
FIRE.topAnchor.constraint(equalTo: view.topAnchor, constant: 20).isActive = true
FIRE.widthAnchor.constraint(equalToConstant: 400).isActive = true
FIRE.heightAnchor.constraint(equalToConstant: 80).isActive = true
}
Change
var FIRE: UIImageView!
To
let FIRE = UIImageView()
my app uses several libraryies. Bassically KYDrawerController. I want my App to use only potraite Orientation but for one ViewControllar i need both landscape and Potrait.
I have search about allmost every solution on the internet. every solution was to subclass the NavigationControllar and Override ShouldAutoRotate method. but non of them worked for me.
and here is the closer View of the entire StoryBoard
the gray color View is the KYDrawerControllar view which is an libry uses to work as NavigationDrawer like Android.
I have created a custom Class for Navigation Controllar and subclass it to the requierd ViewControllar's Navigation Controllar.
here is the Code
class SettingsNavigation: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
NSLog("visibleControllar", self.visibleViewController!.debugDescription)
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func shouldAutorotate() -> Bool {
//return self.visibleViewController!.shouldAutorotate()
return false
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.Portrait
}
override func preferredInterfaceOrientationForPresentation() -> UIInterfaceOrientation {
return UIInterfaceOrientation.Portrait
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
}
here is the ViewControllar
class Settings: UIViewController {
// MARK: - Outlets
#IBOutlet var profileImage: UIImageView!
#IBOutlet var profileName: UILabel!
#IBOutlet var profileRegistrationType: UILabel!
#IBOutlet var logOutButton: FUIButton!
#IBOutlet var subscriptionType: UILabel!
#IBOutlet var subscriptionRegistrationType: UILabel!
#IBOutlet var upgradeSubscriptionButton: FUIButton!
override func shouldAutorotate() -> Bool {
/* if (UIDevice.currentDevice().orientation == UIDeviceOrientation.LandscapeLeft ||
UIDevice.currentDevice().orientation == UIDeviceOrientation.LandscapeRight ||
UIDevice.currentDevice().orientation == UIDeviceOrientation.Unknown) {
return false;
}
else {
return true;
}*/
return false
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.Portrait
} }
and i am using StoryBoard segues to present ViewControllars.
please someone help me with this.
Here is my way:
in your appdelegae.swift:
var shouldSupportAllOrientation = false
func application(application: UIApplication, supportedInterfaceOrientationsForWindow window: UIWindow?) -> UIInterfaceOrientationMask {
if (shouldSupportAllOrientation == true){
return UIInterfaceOrientationMask.All
}
return UIInterfaceOrientationMask.Portrait
}
in your entry view which enter the all orientation view (change to support all orientation, here I use a button as an example):
#IBAction func next(sender: UIButton) {
let appdelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appdelegate.shouldSupportAllOrientation = true
self.performSegueWithIdentifier("next", sender: self)
}
in your entry view which enter the all orientation view (change the orientation to support only Portrait):
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
let appdelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appdelegate.shouldSupportAllOrientation = false
}
finally, you may find this works on all iPhone device and iPad ipad2 except iPad air iPad pro; you should check the "requires full screen" in your project general info to ensure you all orientation view can enter landscape.