I tend to hide the status bar, animated in the following way.
var statusBarHidden: Bool = false {
didSet {
UIView.animate(withDuration: Constants.config_shortAnimTime) { () -> Void in
self.setNeedsStatusBarAppearanceUpdate()
}
}
}
override var prefersStatusBarHidden: Bool {
return statusBarHidden
}
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation{
return .slide
}
extension ViewController: SideMenuNavigationControllerDelegate {
func sideMenuWillAppear(menu: SideMenuNavigationController, animated: Bool) {
statusBarHidden = true
}
func sideMenuDidAppear(menu: SideMenuNavigationController, animated: Bool) {
}
func sideMenuWillDisappear(menu: SideMenuNavigationController, animated: Bool) {
}
func sideMenuDidDisappear(menu: SideMenuNavigationController, animated: Bool) {
statusBarHidden = false
}
}
However, I would also like to preserve the space occupied by status bar, so that when status bar appears, the entire app will not be "pushed up"
May I know how I can achieve so?
Thank you.
You can use additionalSafeAreaInsets to add a placeholder height, substituting the status bar.
But for devices with a notch like the iPhone 12, the space is automatically preserved, so you don't need to add any additional height.
class ViewController: UIViewController {
var statusBarHidden: Bool = false /// no more computed property, otherwise reading safe area would be too late
override var prefersStatusBarHidden: Bool {
return statusBarHidden
}
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation{
return .slide
}
#IBAction func showButtonPressed(_ sender: Any) {
statusBarHidden.toggle()
if statusBarHidden {
sideMenuWillAppear()
} else {
sideMenuWillDisappear()
}
}
lazy var overlayViewController: UIViewController = {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
return storyboard.instantiateViewController(withIdentifier: "OverlayViewController")
}()
var additionalHeight: CGFloat {
if view.window?.safeAreaInsets.top ?? 0 > 20 { /// is iPhone X or other device with notch
return 0 /// add 0 height
} else {
/// the height of the status bar
return view.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0.0
}
}
}
extension ViewController {
/// add placeholder height to substitute status bar
func addAdditionalHeight(_ add: Bool) {
if add {
if let navigationController = self.navigationController {
/// set insets of navigation controller if you're using navigation controller
navigationController.additionalSafeAreaInsets.top = additionalHeight
} else {
/// set insets of self if not using navigation controller
self.additionalSafeAreaInsets.top = additionalHeight
}
} else {
if let navigationController = self.navigationController {
/// set insets of navigation controller if you're using navigation controller
navigationController.additionalSafeAreaInsets.top = 0
} else {
/// set insets of self if not using navigation controller
self.additionalSafeAreaInsets.top = 0
}
}
}
func sideMenuWillAppear() {
addChild(overlayViewController)
view.addSubview(overlayViewController.view)
overlayViewController.view.frame = view.bounds
overlayViewController.view.frame.origin.x = -400
overlayViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
overlayViewController.didMove(toParent: self)
addAdditionalHeight(true) /// add placeholder height
UIView.animate(withDuration: 1) {
self.overlayViewController.view.frame.origin.x = -100
self.setNeedsStatusBarAppearanceUpdate() /// hide status bar
}
}
func sideMenuDidAppear() {}
func sideMenuWillDisappear() {
addAdditionalHeight(false) /// remove placeholder height
UIView.animate(withDuration: 1) {
self.overlayViewController.view.frame.origin.x = -400
self.setNeedsStatusBarAppearanceUpdate() /// show status bar
} completion: { _ in
self.overlayViewController.willMove(toParent: nil)
self.overlayViewController.view.removeFromSuperview()
self.overlayViewController.removeFromParent()
}
}
func sideMenuDidDisappear() {}
}
Result (Tested on iPhone 12, iPhone 8, iPad Pro 4th gen):
iPhone 12 (notch)
iPhone 8 (no notch)
iPhone 12 + navigation bar
iPhone 8 + navigation bar
Demo GitHub repo
First of all, it is not currently possible to make UINavigationController behave this way. However you can wrap your UINavigationController instance in a Container View Controller.
This will give you control over managing the top space from where the UINavigationController view layout starts. Inside this container class, you could manage it like following -
class ContainerViewController: UIViewController {
private lazy var statusBarBackgroundView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = .clear
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var statusBarBackgroundViewHeightConstraint: NSLayoutConstraint = {
statusBarBackgroundView.heightAnchor.constraint(equalToConstant: 0)
}()
var statusBarHeight: CGFloat {
if #available(iOS 13.0, *) {
guard let statusBarMananger = self.view.window?.windowScene?.statusBarManager
else { return 0 }
return statusBarMananger.statusBarFrame.height
} else {
return UIApplication.shared.statusBarFrame.height
}
}
var statusBarHidden: Bool = false {
didSet {
self.statusBarBackgroundViewHeightConstraint.constant = self.statusBarHidden ? self.lastKnownStatusBarHeight : 0
self.view.layoutIfNeeded()
}
}
private var lastKnownStatusBarHeight: CGFloat = 0
override func viewDidLoad() {
super.viewDidLoad()
let topView = self.statusBarBackgroundView
self.view.addSubview(topView)
NSLayoutConstraint.activate([
topView.topAnchor.constraint(equalTo: self.view.topAnchor),
topView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
statusBarBackgroundViewHeightConstraint,
topView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
])
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let height = self.statusBarHeight
if height > 0 {
self.lastKnownStatusBarHeight = height
}
}
func setUpNavigationController(_ navCtrl: UINavigationController) {
self.addChild(navCtrl)
navCtrl.didMove(toParent: self)
self.view.addSubview(navCtrl.view)
navCtrl.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
navCtrl.view.topAnchor.constraint(equalTo: statusBarBackgroundView.bottomAnchor),
navCtrl.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
navCtrl.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
navCtrl.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
])
self.view.layoutIfNeeded()
}
}
Now from your call site, you can do following -
class ViewController: UIViewController {
var statusBarHidden: Bool = false {
didSet {
UIView.animate(withDuration: Constants.config_shortAnimTime) { () -> Void in
/// Forward the call to ContainerViewController to act on this update
(self.navigationController?.parent as? ContainerViewController)?.statusBarHidden = self.statusBarHidden
/// Keep doing whatever you are doing now
self.setNeedsStatusBarAppearanceUpdate()
}
}
}
}
Related
I tend to hide the status bar, animated in the following way.
var statusBarHidden: Bool = false {
didSet {
UIView.animate(withDuration: Constants.config_shortAnimTime) { () -> Void in
self.setNeedsStatusBarAppearanceUpdate()
}
}
}
override var prefersStatusBarHidden: Bool {
return statusBarHidden
}
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation{
return .slide
}
extension ViewController: SideMenuNavigationControllerDelegate {
func sideMenuWillAppear(menu: SideMenuNavigationController, animated: Bool) {
statusBarHidden = true
}
func sideMenuDidAppear(menu: SideMenuNavigationController, animated: Bool) {
}
func sideMenuWillDisappear(menu: SideMenuNavigationController, animated: Bool) {
}
func sideMenuDidDisappear(menu: SideMenuNavigationController, animated: Bool) {
statusBarHidden = false
}
}
However, I would also like to preserve the space occupied by status bar, so that when status bar appears, the entire app will not be "pushed up"
May I know how I can achieve so?
Thank you.
You can use additionalSafeAreaInsets to add a placeholder height, substituting the status bar.
But for devices with a notch like the iPhone 12, the space is automatically preserved, so you don't need to add any additional height.
class ViewController: UIViewController {
var statusBarHidden: Bool = false /// no more computed property, otherwise reading safe area would be too late
override var prefersStatusBarHidden: Bool {
return statusBarHidden
}
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation{
return .slide
}
#IBAction func showButtonPressed(_ sender: Any) {
statusBarHidden.toggle()
if statusBarHidden {
sideMenuWillAppear()
} else {
sideMenuWillDisappear()
}
}
lazy var overlayViewController: UIViewController = {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
return storyboard.instantiateViewController(withIdentifier: "OverlayViewController")
}()
var additionalHeight: CGFloat {
if view.window?.safeAreaInsets.top ?? 0 > 20 { /// is iPhone X or other device with notch
return 0 /// add 0 height
} else {
/// the height of the status bar
return view.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0.0
}
}
}
extension ViewController {
/// add placeholder height to substitute status bar
func addAdditionalHeight(_ add: Bool) {
if add {
if let navigationController = self.navigationController {
/// set insets of navigation controller if you're using navigation controller
navigationController.additionalSafeAreaInsets.top = additionalHeight
} else {
/// set insets of self if not using navigation controller
self.additionalSafeAreaInsets.top = additionalHeight
}
} else {
if let navigationController = self.navigationController {
/// set insets of navigation controller if you're using navigation controller
navigationController.additionalSafeAreaInsets.top = 0
} else {
/// set insets of self if not using navigation controller
self.additionalSafeAreaInsets.top = 0
}
}
}
func sideMenuWillAppear() {
addChild(overlayViewController)
view.addSubview(overlayViewController.view)
overlayViewController.view.frame = view.bounds
overlayViewController.view.frame.origin.x = -400
overlayViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
overlayViewController.didMove(toParent: self)
addAdditionalHeight(true) /// add placeholder height
UIView.animate(withDuration: 1) {
self.overlayViewController.view.frame.origin.x = -100
self.setNeedsStatusBarAppearanceUpdate() /// hide status bar
}
}
func sideMenuDidAppear() {}
func sideMenuWillDisappear() {
addAdditionalHeight(false) /// remove placeholder height
UIView.animate(withDuration: 1) {
self.overlayViewController.view.frame.origin.x = -400
self.setNeedsStatusBarAppearanceUpdate() /// show status bar
} completion: { _ in
self.overlayViewController.willMove(toParent: nil)
self.overlayViewController.view.removeFromSuperview()
self.overlayViewController.removeFromParent()
}
}
func sideMenuDidDisappear() {}
}
Result (Tested on iPhone 12, iPhone 8, iPad Pro 4th gen):
iPhone 12 (notch)
iPhone 8 (no notch)
iPhone 12 + navigation bar
iPhone 8 + navigation bar
Demo GitHub repo
First of all, it is not currently possible to make UINavigationController behave this way. However you can wrap your UINavigationController instance in a Container View Controller.
This will give you control over managing the top space from where the UINavigationController view layout starts. Inside this container class, you could manage it like following -
class ContainerViewController: UIViewController {
private lazy var statusBarBackgroundView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = .clear
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var statusBarBackgroundViewHeightConstraint: NSLayoutConstraint = {
statusBarBackgroundView.heightAnchor.constraint(equalToConstant: 0)
}()
var statusBarHeight: CGFloat {
if #available(iOS 13.0, *) {
guard let statusBarMananger = self.view.window?.windowScene?.statusBarManager
else { return 0 }
return statusBarMananger.statusBarFrame.height
} else {
return UIApplication.shared.statusBarFrame.height
}
}
var statusBarHidden: Bool = false {
didSet {
self.statusBarBackgroundViewHeightConstraint.constant = self.statusBarHidden ? self.lastKnownStatusBarHeight : 0
self.view.layoutIfNeeded()
}
}
private var lastKnownStatusBarHeight: CGFloat = 0
override func viewDidLoad() {
super.viewDidLoad()
let topView = self.statusBarBackgroundView
self.view.addSubview(topView)
NSLayoutConstraint.activate([
topView.topAnchor.constraint(equalTo: self.view.topAnchor),
topView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
statusBarBackgroundViewHeightConstraint,
topView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
])
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let height = self.statusBarHeight
if height > 0 {
self.lastKnownStatusBarHeight = height
}
}
func setUpNavigationController(_ navCtrl: UINavigationController) {
self.addChild(navCtrl)
navCtrl.didMove(toParent: self)
self.view.addSubview(navCtrl.view)
navCtrl.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
navCtrl.view.topAnchor.constraint(equalTo: statusBarBackgroundView.bottomAnchor),
navCtrl.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
navCtrl.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
navCtrl.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
])
self.view.layoutIfNeeded()
}
}
Now from your call site, you can do following -
class ViewController: UIViewController {
var statusBarHidden: Bool = false {
didSet {
UIView.animate(withDuration: Constants.config_shortAnimTime) { () -> Void in
/// Forward the call to ContainerViewController to act on this update
(self.navigationController?.parent as? ContainerViewController)?.statusBarHidden = self.statusBarHidden
/// Keep doing whatever you are doing now
self.setNeedsStatusBarAppearanceUpdate()
}
}
}
}
Problem
When tapping the skip button on page i (which calls setViewControllers(_:animated:) and transitions the user to the last page in the page view controller), and then swiping back to page i again, the page control disappears.
Wanted result
I want to programmatically add and show a custom page control on the bottom of each view controller in a page view controller when said page view controller contains different types of view controllers.
Efforts so far to resolve the issue
Adding the page control to the base view controller each time it appears.
Calling loadView() on the view controller that contains the missing page control.
Code
I have a WalkthroughRootViewController that contains a UIPageViewController. The type of the view controllers in the page view controller are two subclasses of type WalkthroughBaseViewController, the first n-1 of one type, and the last of the other. I have not included code of the last type, as that's working as far as I can see.
I have this code in WalkthroughBaseViewController:
lazy var pageControl: UIPageControl = {
let pageControl = UIPageControl(frame: .zero)
pageControl.translatesAutoresizingMaskIntoConstraints = false
pageControl.numberOfPages = numberOfPages
pageControl.sizeToFit()
pageControl.pageIndicatorTintColor = Colors.brown
pageControl.currentPageIndicatorTintColor = Colors.silver
pageControl.isUserInteractionEnabled = false
pageControl.isEnabled = false
return pageControl
}()
The page control is added to the view in viewDidLoad():
view.addSubview(pageControl)
NSLayoutConstraint.activate([
pageControl.bottomAnchor.constraint(equalTo: view.bottomAnchor),
pageControl.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
If the user is on any of the first n-1 view controllers, there is a skip button the user can tap to skip forward to the last view controller. The code for this is
func skipWalkthrough() {
guard let viewController = walkthroughPageViewControllerDataSource.viewController(at: lastIndex, storyboard: storyboard!) else { return }
walkthroughPageViewController.setViewControllers([viewController], direction: .forward, animated: true)
}
Reference
I have highlighted the code I believe is important, but here are all files related to the walkthrough of the application.
WalkthroughRootViewController
import UIKit
class WalkthroughRootViewController: UIViewController {
// MARK: Regular Properties
var walkthroughPageViewController: UIPageViewController!
var walkthroughImages = [
Images.w1,
Images.w2
]
var walkthroughStrings: [String] = [
.localized(.walkthroughTitle1),
.localized(.walkthroughZipCodeTitle)
]
// MARK: Lazy Properties
lazy var walkthroughPageViewControllerDataSource: WalkthroughPageViewControllerDataSource = {
var dataSource = WalkthroughPageViewControllerDataSource()
dataSource.walkthroughRootViewController = self
return dataSource
}()
// MARK: Computed Properties
var lastIndex: Int {
return walkthroughImages.count - 1
}
var temporaryUserInput: String?
var temporarySwitchPosition = false
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
}
// MARK: View Controller Life Cycle
extension WalkthroughRootViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Stop gray shadow from appearing under transition.
navigationController?.view.backgroundColor = .white
configurePageViewController()
}
}
// MARK: Helper Methods
extension WalkthroughRootViewController {
func configurePageViewController() {
walkthroughPageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
walkthroughPageViewController.dataSource = walkthroughPageViewControllerDataSource
walkthroughPageViewController.delegate = walkthroughPageViewControllerDataSource
let startingViewController = storyboard!.instantiateViewController(withIdentifier: Strings.ViewControllerIdentifiers.walkthroughImage) as! WalkthroughImageViewController
let startIndex = 0
startingViewController.delegate = self
startingViewController.pageIndex = startIndex
startingViewController.text = walkthroughStrings[startIndex]
startingViewController.image = walkthroughImages[startIndex]
startingViewController.numberOfPages = walkthroughImages.count
walkthroughPageViewController.setViewControllers([startingViewController], direction: .forward, animated: true)
walkthroughPageViewController.view.frame = view.bounds
add(walkthroughPageViewController)
}
}
extension WalkthroughRootViewController: WalkthroughDelegate {
func skipWalkthrough() {
guard let viewController = walkthroughPageViewControllerDataSource.viewController(at: lastIndex, storyboard: storyboard!) else { return }
walkthroughPageViewController.setViewControllers([viewController], direction: .forward, animated: true)
}
}
extension WalkthroughRootViewController: WalkthrouZipCodeViewControllerDelegate {
func walkththroughZipCodeViewController(_ viewController: WalkthroughZipCodeViewController, userEnteredText enteredText: String) {
temporaryUserInput = enteredText
}
func walkthroughZipCodeViewController(_ viewController: WalkthroughZipCodeViewController, userChangedSwitchPosition position: Bool) {
temporarySwitchPosition = position
}
}
WalkthroughBaseViewController
import UIKit
protocol WalkthroughDelegate: class {
func skipWalkthrough()
}
class WalkthroughBaseViewController: UIViewController {
// MARK: Regular Properties
var pageIndex = 0
var text = ""
var delegate: WalkthroughDelegate?
var numberOfPages = 0
// Lazy Properties
lazy var pageControl: UIPageControl = {
let pageControl = UIPageControl(frame: .zero)
pageControl.translatesAutoresizingMaskIntoConstraints = false
pageControl.numberOfPages = numberOfPages
pageControl.sizeToFit()
pageControl.pageIndicatorTintColor = Colors.brown
pageControl.currentPageIndicatorTintColor = Colors.silver
pageControl.isUserInteractionEnabled = false
pageControl.isEnabled = false
return pageControl
}()
}
// MARK: View Controller Life Cycle
extension WalkthroughBaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = Colors.silver
view.addSubview(pageControl)
NSLayoutConstraint.activate([
pageControl.bottomAnchor.constraint(equalTo: view.bottomAnchor),
pageControl.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
view.accessibilityIdentifier = Strings.AccessibilityIdentifiers.walkthrough
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
pageControl.currentPage = pageIndex
}
}
WalkthroughImageViewController
import UIKit
class WalkthroughImageViewController: WalkthroughBaseViewController {
// MARK: #IBOutlets
#IBOutlet weak var titleLabel: UILabel! {
didSet {
titleLabel.adjustsFontSizeToFitWidth = true
titleLabel.textColor = Colors.silver
titleLabel.numberOfLines = 0
}
}
#IBOutlet weak var skipWalkthroughButton: UIButton! {
didSet {
skipWalkthroughButton.setTitleColor(Colors.silver, for: .normal)
skipWalkthroughButton.titleLabel?.font = UIFont.preferredBoldFont(for: .body)
skipWalkthroughButton.setTitle(.localized(.skip), for: .normal)
}
}
#IBOutlet weak var imageView: UIImageView! {
didSet {
imageView.layer.shadowColor = Colors.brown.cgColor
imageView.layer.shadowOffset = CGSize(width: 0, height: 1)
imageView.layer.shadowOpacity = 1
imageView.layer.shadowRadius = 1.0
imageView.clipsToBounds = false
imageView.contentMode = .scaleAspectFill
}
}
// MARK: Regular Properties
var image: UIImage?
// MARK: View Controller Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
imageView.image = image
titleLabel.text = text
}
}
// MARK: #IBActions
extension WalkthroughImageViewController {
#IBAction func skipWalkthrough(_ sender: UIButton) {
delegate?.skipWalkthrough()
}
}
WalkthroughPageViewControllerDataSource
import UIKit
class WalkthroughPageViewControllerDataSource: NSObject {
// MARK: Regular Properties
var walkthroughRootViewController: WalkthroughRootViewController!
}
extension WalkthroughPageViewControllerDataSource: UIPageViewControllerDataSource {
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
var index = indexOfViewController(viewController as! WalkthroughBaseViewController)
if index == NSNotFound || index == 0 {
return nil
}
index -= 1
return self.viewController(at: index, storyboard: walkthroughRootViewController.storyboard!)
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
var index = indexOfViewController(viewController as! WalkthroughBaseViewController)
if index == NSNotFound {
return nil
}
index += 1
if index == walkthroughRootViewController.walkthroughImages.count {
return nil
}
return self.viewController(at: index, storyboard: walkthroughRootViewController.storyboard!)
}
}
extension WalkthroughPageViewControllerDataSource {
func viewController(at index: Int, storyboard: UIStoryboard) -> WalkthroughBaseViewController? {
if walkthroughRootViewController.walkthroughImages.count == 0 || index >= walkthroughRootViewController.walkthroughImages.count {
return nil
}
var viewController: WalkthroughBaseViewController?
if index == walkthroughRootViewController.lastIndex {
viewController = storyboard.instantiateViewController(withIdentifier: Strings.ViewControllerIdentifiers.walkthroughZipCode) as? WalkthroughZipCodeViewController
if let viewController = viewController as? WalkthroughZipCodeViewController {
viewController.pageIndex = index
viewController.walkthroughZipCodeDelegate = walkthroughRootViewController
viewController.temporaryUserInput = walkthroughRootViewController.temporaryUserInput
viewController.temporarySwitchPosition = walkthroughRootViewController.temporarySwitchPosition
viewController.numberOfPages = walkthroughRootViewController.walkthroughImages.count
viewController.image = walkthroughRootViewController.walkthroughImages[index]
}
} else {
viewController = storyboard.instantiateViewController(withIdentifier: Strings.ViewControllerIdentifiers.walkthroughImage) as? WalkthroughImageViewController
if let viewController = viewController as? WalkthroughImageViewController {
viewController.delegate = walkthroughRootViewController
viewController.pageIndex = index
viewController.image = walkthroughRootViewController.walkthroughImages[index]
viewController.text = walkthroughRootViewController.walkthroughStrings[index]
}
}
return viewController
}
func indexOfViewController(_ viewController: WalkthroughBaseViewController) -> Int {
return viewController.pageIndex
}
}
extension WalkthroughPageViewControllerDataSource: UIPageViewControllerDelegate {
}
Create a single UIPageControl that you put in the WalkthroughRootViewController and update it when you navigate the pages - don't create a page control for each child.
Try not to use extensions to override methods - it can cause you trouble - see this blog entry.
I have two view controllers. MainViewController and SecondViewController (this one is embedded in a Navigation Controller).
MainViewController has a UIButton that will modally present SecondViewController, while SecondViewController has a UIButton that will dismiss itself.
Each of them have the following code:
var statusBarHidden = false {
didSet {
UIView.animate(withDuration: 0.5) { () -> Void in
self.setNeedsStatusBarAppearanceUpdate()
}
}
}
override var prefersStatusBarHidden: Bool {
return statusBarHidden
}
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .slide
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
statusBarHidden = true
}
The slide animation of the status bar works great in the simulator but not on the actual device, what am i doing wrong ?
I'm using xCode 8.2.1 and Swift 3
What i ended up doing was this. I created a variable that links to the view of the status bar and added functions so i can do what i need.
extension UIApplication {
var statusBarView: UIView? {
return value(forKey: "statusBar") as? UIView
}
func changeStatusBar(alpha: CGFloat) {
statusBarView?.alpha = alpha
}
func hideStatusBar() {
UIView.animate(withDuration: 0.3) {
self.statusBarView?.alpha = 0
}
}
func showStatusBar() {
UIView.animate(withDuration: 0.3) {
self.statusBarView?.alpha = 1
}
}
}
A typical use would be:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let alpha = tableView.contentOffset.y / 100
UIApplication.shared.changeStatusBar(alpha: alpha)
}
I upgraded to XCode 8.0 / iOS 10 and now the color change animation of my navigation bar is not working anymore, it changes the color directly without any animation.
UIView.animateWithDuration(0.2, animations: {
self.navigationController?.navigationBar.barTintColor = currentSection.color!
})
Anyone knows how to fix this?
To animate navigationBar’s color change in iOS10 you need to call layoutIfNeeded after setting color inside animation block.
Example code:
UIView.animateWithDuration(0.5) {
self.navigationController?.navigationBar.barTintColor = UIColor.redColor()
self.navigationController?.navigationBar.layoutIfNeeded()
}
Also I want to inform that Apple doesn’t officialy support animations in such properties like barTintColor, so that method can break at any time.
If you call -layoutIfNeeded on the navigation bar during the animation
block it should update its background properties, but given the nature
of what these properties do, there really hasn't ever been any kind of
guarantee that you could animate any of them.
Interactive animation
Define a protocol:
/// Navigation bar colors for `ColorableNavigationController`, called on `push` & `pop` actions
public protocol NavigationBarColorable: UIViewController {
var navigationTintColor: UIColor? { get }
var navigationBarTintColor: UIColor? { get }
}
public extension NavigationBarColorable {
var navigationTintColor: UIColor? { return nil }
}
Define a custom NavigationController subclass:
class AppNavigationController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
navigationBar.shadowImage = UIImage()
if let colors = rootViewController as? NavigationBarColorable {
setNavigationBarColors(colors)
}
}
private var previousViewController: UIViewController? {
guard viewControllers.count > 1 else {
return nil
}
return viewControllers[viewControllers.count - 2]
}
override open func pushViewController(_ viewController: UIViewController, animated: Bool) {
if let colors = viewController as? NavigationBarColorable {
setNavigationBarColors(colors)
}
super.pushViewController(viewController, animated: animated)
}
override open func popViewController(animated: Bool) -> UIViewController? {
if let colors = previousViewController as? NavigationBarColorable {
setNavigationBarColors(colors)
}
// Let's start pop action or we can't get transitionCoordinator()
let popViewController = super.popViewController(animated: animated)
// Secure situation if user cancelled transition
transitionCoordinator?.animate(alongsideTransition: nil, completion: { [weak self] context in
guard let `self` = self else { return }
guard let colors = self.topViewController as? NavigationBarColorable else { return }
self.setNavigationBarColors(colors)
})
return popViewController
}
override func popToRootViewController(animated: Bool) -> [UIViewController]? {
if let colors = rootViewController as? NavigationBarColorable {
setNavigationBarColors(colors)
}
let controllers = super.popToRootViewController(animated: animated)
return controllers
}
private func setNavigationBarColors(_ colors: NavigationBarColorable) {
if let tintColor = colors.navigationTintColor {
navigationBar.titleTextAttributes = [
.foregroundColor : tintColor
]
navigationBar.tintColor = tintColor
}
navigationBar.barTintColor = colors.navigationBarTintColor
}
}
Now you can conform to NavigationBarColorable in any controller inside the AppNavigationController and give it any color you want.
extension FirstViewController: NavigationBarColorable {
public var navigationBarTintColor: UIColor? { UIColor.red }
public var navigationTintColor: UIColor? { UIColor.white }
}
extension SecondViewController: NavigationBarColorable {
public var navigationBarTintColor: UIColor? { UIColor.blue }
public var navigationTintColor: UIColor? { UIColor.orange }
}
Don't forget to implement this useful extension:
extension UINavigationController {
var rootViewController: UIViewController? {
return viewControllers.first
}
}
How to move the UIToolBar to top (stick to the UINavigationBar)?
I m struggle with this thing for a long time and I've try some stuff like:
Custom UIToolBar that conforms to UIToolbarDelegate and (UIBarPosition)positionForBar:(id <UIBarPositioning>)bar get called
and I return UIBarPositionTop but the toolbar stays at bottom.
Change the toolbar frame: self.navigationController.toolbar.frame = CGRectMake(0, NAV_BAR_Y, self.view.bounds.size.width, NAV_BAR_HEIGHT);
Custom UINaviagtionController which has this delegate function: (UIBarPosition)positionForBar:(id <UIBarPositioning>)bar {
return UIBarPositionTop;
}
None of the struggles goes well, same look:
Any Help will be great.
(I would like to have navigation look as Apple App store navigation)
There are 2 options that I'm aware of.
1) Related to Move UINavigationController's toolbar to the top to lie underneath navigation bar
You can subclass UINavigationController and change the Y-axis position of the toolbar when the value is set.
import UIKit
private var context = 0
class NavigationController: UINavigationController {
private var inToolbarFrameChange = false
var observerBag: [NSKeyValueObservation] = []
override func awakeFromNib() {
super.awakeFromNib()
self.inToolbarFrameChange = false
}
override func viewDidLoad() {
super.viewDidLoad()
observerBag.append(
toolbar.observe(\.center, options: .new) { toolbar, _ in
if !self.inToolbarFrameChange {
self.inToolbarFrameChange = true
toolbar.frame = CGRect(
x: 0,
y: self.navigationBar.frame.height + UIApplication.shared.statusBarFrame.height,
width: toolbar.frame.width,
height: toolbar.frame.height
)
self.inToolbarFrameChange = false
}
}
)
}
override func setToolbarHidden(_ hidden: Bool, animated: Bool) {
super.setToolbarHidden(hidden, animated: false)
var rectTB = self.toolbar.frame
rectTB = .zero
}
}
2) You can create your own UIToolbar and add it to view of the UIViewController. Then, you add the constraints to the leading, trailing and the top of the safe area.
import UIKit
final class ViewController: UIViewController {
private let toolbar = UIToolbar()
private let segmentedControl: UISegmentedControl = {
let control = UISegmentedControl(items: ["Op 1", "Op 2"])
control.isEnabled = false
return control
}()
override func loadView() {
super.loadView()
setupToolbar()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.navigationBar.hideBorderLine()
}
private func setupToolbar() {
let barItem = UIBarButtonItem(customView: segmentedControl)
toolbar.setItems([barItem], animated: false)
toolbar.isTranslucent = false
toolbar.isOpaque = false
view.addSubview(toolbar)
toolbar.translatesAutoresizingMaskIntoConstraints = false
toolbar.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
toolbar.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
toolbar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
}
}
private extension UINavigationBar {
func showBorderLine() {
findBorderLine().isHidden = false
}
func hideBorderLine() {
findBorderLine().isHidden = true
}
private func findBorderLine() -> UIImageView! {
return self.subviews
.flatMap { $0.subviews }
.compactMap { $0 as? UIImageView }
.filter { $0.bounds.size.width == self.bounds.size.width }
.filter { $0.bounds.size.height <= 2 }
.first
}
}
Try this solution
#interface ViewController () <UIToolbarDelegate>
{
UIToolbar * lpToolbar;
}
#end
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
lpToolbar = [[UIToolbar alloc] initWithFrame :CGRectZero];
lpToolbar.delegate = self;
self.navigationItem.title = #"Title";
}
-(void) viewWillAppear :(BOOL)animated
{
[super viewWillAppear:animated];
[self.navigationController.view addSubview :lpToolbar];
CGRect rFrame = self.navigationController.navigationBar.frame;
lpToolbar.frame = CGRectMake( 0.0, rFrame.origin.y + rFrame.size.height, rFrame.size.width, 50.0 );
}
-(void) viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[lpToolbar removeFromSuperview];
}
-(UIBarPosition) positionForBar:(id <UIBarPositioning>)bar
{
return UIBarPositionTop;
}