I am trying to create universal alert that appears on the top most view controller while the bottom viewcontroller is still clickable.
This alert is just a 20 points height status line that inform user about network reachability. How can I make UIViewController not user interactable?
Please note that I do not use Storyboard or XIB
Also if you are targeting iOS11 and above you would need to use safeAreaLayoutGuide while using autolayout code
The solution is two folds.
First, create a Base View Controller and have all your view controllers that need to show the alert to extend from that Base View Controller.
Then create a new swift file, a subclass of NSObject. Lets say NetworkAlerter.swift and copy paste the code below (as appropriate)
import UIKit
class NetworkAlerter: NSObject {
var window :UIWindow? = UIApplication.shared.keyWindow
var alertShowingConstraint : NSLayoutConstraint?
var alertHidingConstraint : NSLayoutConstraint?
var closeTimer : Timer? = nil
public lazy var networkIndicatorLabel : UILabel = {
let label : UILabel = UILabel(frame: .zero)
label.translatesAutoresizingMaskIntoConstraints = false
label.textAlignment = NSTextAlignment.center
return label
}()
override init() {
super.init()
createSubviews()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
print("Time to deinit")
networkIndicatorLabel.removeFromSuperview()
}
func createSubviews() {
guard let window = window else {
print("Some thing wrong with Window initialization!!")
return
}
window.addSubview(networkIndicatorLabel)
addAutolayout()
}
func addAutolayout() {
guard let window = window else {
print("Some thing wrong with Window initialization!!")
return
}
alertShowingConstraint = networkIndicatorLabel.topAnchor.constraint(equalTo: window.topAnchor)
alertHidingConstraint = networkIndicatorLabel.bottomAnchor.constraint(equalTo: window.topAnchor)
alertHidingConstraint?.isActive = true
networkIndicatorLabel.heightAnchor.constraint(equalToConstant: 20).isActive = true
networkIndicatorLabel.leadingAnchor.constraint(equalTo: window.leadingAnchor).isActive = true
networkIndicatorLabel.trailingAnchor.constraint(equalTo: window.trailingAnchor).isActive = true
}
func showNetworkAlerter(networkAvailable: Bool) {
guard let window = window else {
print("Some thing wrong with Window initialization!!")
return
}
invalidateAndKillTimer()
closeTimer = Timer.scheduledTimer(timeInterval: 3.0, target: self, selector: #selector(dismissNetworkAlerter), userInfo: nil, repeats: false)
if networkAvailable {
networkIndicatorLabel.text = "Available"
networkIndicatorLabel.backgroundColor = UIColor.green
} else {
networkIndicatorLabel.text = "Not Available"
networkIndicatorLabel.backgroundColor = UIColor.red
}
window.layoutIfNeeded()
UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 1.0, options: .curveEaseOut, animations: {
if (self.alertHidingConstraint?.isActive)! {
self.alertHidingConstraint?.isActive = false
}
if !(self.alertShowingConstraint?.isActive)! {
self.alertShowingConstraint?.isActive = true
}
window.layoutIfNeeded()
}, completion: { _ in
})
}
#objc func dismissNetworkAlerter() {
invalidateAndKillTimer()
guard let window = window else {
print("Some thing wrong with Window initialization!!")
return
}
window.layoutIfNeeded()
UIView.animate(withDuration: 0.5, animations: {
if (self.alertShowingConstraint?.isActive)! {
self.alertShowingConstraint?.isActive = false
}
if !(self.alertHidingConstraint?.isActive)! {
self.alertHidingConstraint?.isActive = true
}
window.layoutIfNeeded()
}) { (done) in
}
}
// MARK:- Timer Related
private func invalidateAndKillTimer() -> Void {
if (closeTimer != nil) {
closeTimer?.invalidate()
closeTimer = nil
}
}
}
Then move back Base View Controller. Right on top copy paste the following
var networkAlertLauncher : NetworkAlerter? = nil
and then find an appropriate place in Base View Controller and paste the following:
func showAlertBar(networkAvailabilityStatus: Bool) -> Void {
if networkAlertLauncher != nil {
networkAlertLauncher = nil
}
networkAlertLauncher = NetworkAlerter()
networkAlertLauncher?.showNetworkAlerter(networkAvailable: networkAvailabilityStatus)
}
Now the function showAlertBar will be accessible from all the view controllers that you have extended from Base View Controller. You can invoke it like so:
self.showAlertBar(networkAvailabilityStatus: false) or self.showAlertBar(networkAvailabilityStatus: true)
Related
I use UIView as alert view in my app, and i want to show it as banner on top of screen, when device is not connected to internet. So my issue that this view appears under my nav bar, how can i bring it to front ? I've tried to us UIApplication.shared.keyWindow! and add my backgroundView as subview to it, but it causes other issues.
This is my alert view class: I'll provide all class, but my realisation is in show() method.
import Foundation
import UIKit
import SnapKit
class ConnectionAlertView: UIView, UIGestureRecognizerDelegate {
internal var backgroundView: UIView = {
let view = UIView()
view.backgroundColor = Theme.Color.alertLabelBackgroundColor
view.alpha = 0
view.layer.cornerRadius = 15
return view
}()
internal var dismissButton: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(named: "close_icon")
imageView.layer.cornerRadius = 15
imageView.isUserInteractionEnabled = true
return imageView
}()
internal var descriptionTitleLabel: UILabel = {
let label = UILabel()
label.text = "Відсутнє підключення до Інтернету"
label.font = Theme.Font.fontBodyLarge
label.textColor = .white
return label
}()
internal var descriptionLabel: UILabel = {
let label = UILabel()
label.text = "Перевірте налаштування мережі"
label.font = Theme.Font.fontBodyMedium
label.textColor = .white
return label
}()
// MARK: - Private Methods -
internal func layout() {
backgroundView.addSubview(descriptionTitleLabel)
backgroundView.addSubview(descriptionLabel)
backgroundView.addSubview(dismissButton)
descriptionTitleLabel.snp.makeConstraints { make in
make.trailing.equalTo(backgroundView).offset(54)
make.leading.equalTo(backgroundView).offset(16)
make.top.equalTo(backgroundView).offset(12)
}
descriptionLabel.snp.makeConstraints { make in
make.leading.equalTo(descriptionTitleLabel.snp.leading)
make.top.equalTo(descriptionTitleLabel.snp.bottom).offset(4)
}
dismissButton.snp.makeConstraints { make in
make.width.height.equalTo(30)
make.centerY.equalTo(backgroundView)
make.trailing.equalTo(backgroundView).offset(-16)
}
}
internal func configure() {
let tap = UITapGestureRecognizer(target: self, action: #selector(dismiss(sender:)))
tap.delegate = self
dismissButton.addGestureRecognizer(tap)
}
// MARK: - Public Methods -
func show(viewController: UIViewController) {
guard let targetView = viewController.view else { return }
backgroundView.frame = CGRect(x: 10, y: 50, width: targetView.frame.width - 20 , height: 67)
targetView.addSubview(targetView)
layout()
configure()
// show view
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
UIView.transition(with: self.backgroundView, duration: 0.6,
options: .transitionCrossDissolve,
animations: {
self.backgroundView.alpha = 1
})
}
// hide view after 5 sec delay
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
UIView.transition(with: self.backgroundView, duration: 1,
options: .transitionCrossDissolve,
animations: {
self.backgroundView.alpha = 0
})
}
}
// MARK: - Objc Methods -
#objc internal func dismiss(sender: UITapGestureRecognizer) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
UIView.transition(with: self.backgroundView, duration: 1,
options: .transitionCrossDissolve,
animations: {
self.backgroundView.alpha = 0
})
}
}
}
My viewController:
class PhoneNumViewController: UIViewController {
let alert = ConnectionAlertView()
private func checkInternetConnection() {
if !NetworkingMonitor.isConnectedToInternet {
log.error("No internet connection!")
alert.show(viewController: self)
}
}
}
Since you have a navigation controller and do not wish to add this view to the window directly, I can offer the following idea which could work.
Your UIViewController is contained with the UINavigationController so if you add the alert to your UIViewController, you will notice it below the UINavigationBar.
You could instead show the alert from your UINavigationController instead with the following changes.
1.
In the func show(viewController: UIViewController) in your class ConnectionAlertView: UIView I changed the following line:
targetView.addSubview(targetView)
to
targetView.addSubview(backgroundView)
This does not directly relate to your issue but seems to be a bug and causes a crash as it seems like you want to add the background view on the target view.
2.
In your class ViewController: UIViewController, when you want to show your alert view, pass the UINavigationController instead like this:
if let navigationController = self.navigationController
{
alert.show(viewController: navigationController)
}
This should give you the desired result I believe (The image and font looks different as I do not have these files but should work fine at your end):
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()
}
}
}
}
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()
}
}
}
}
override func viewDidLoad() {
let tap = UITapGestureRecognizer(target: self, action: #selector(touchHandled))
view.addGestureRecognizer(tap)
}
#objc func touchHandled() {
tabBarController?.hideTabBarAnimated(hide: true)
}
extension UITabBarController {
func hideTabBarAnimated(hide:Bool) {
UIView.animate(withDuration: 2, animations: {
if hide {
self.tabBar.transform = CGAffineTransform(translationX: 0, y: 100)
} else {
self.tabBar.transform = CGAffineTransform(translationX: 0, y: -100)
}
})
}
}
I can only hide the tab bar but I can't make it show when you tap again. I tried to look for answers on stack overflow but the answers seems to only work if you're using a button or a storyboard.
Have a variable isTabBarHidden in class which stores if the tabBar has been animated to hide. (You could have used tabBar.isHidden, but that would complicate the logic a little bit when animate hiding and showing)
class ViewController {
var isTabBarHidden = false // set the default value as required
override func viewDidLoad() {
super.viewDidLoad()
let tap = UITapGestureRecognizer(target: self, action: #selector(touchHandled))
view.addGestureRecognizer(tap)
}
#objc func touchHandled() {
guard let tabBarControllerFound = tabBarController else {
return
}
tabBarController?.hideTabBarAnimated(hide: !isTabBarHidden)
isTabBarHidden = !isTabBarHidden
}
}
Generalised solution with protocol which will work in all the screens
Create UIViewController named BaseViewController and make it base class of all of your view controllers
Now Define protocol
protocol ProtocolHideTabbar:class {
func hideTabbar ()
}
protocol ProtocolShowTabbar:class {
func showTabbar ()
}
extension ProtocolHideTabbar where Self : UIViewController {
func hideTabbar () {
self.tabBarController?.tabBar.isHidden = true
}
}
extension ProtocolShowTabbar where Self : UIViewController {
func showTabbar () {
self.tabBarController?.tabBar.isHidden = false
}
}
By default we want show tabbar in every view controller so
extension UIViewController : ProtocolShowTabbar {}
In your BaseView Controller
in view will appear method add following code to show hide based on protocol
if self is ProtocolHideTabbar {
( self as! ProtocolHideTabbar).hideTabbar()
} else if self is ProtocolShowTabbar{
( self as ProtocolShowTabbar).showTabbar()
}
How to use
Simply
class YourViewControllerWithTabBarHidden:BaseViewController,ProtocolHideTabbar {
}
Hope it is helpful
Tested 100% working
Please try below code for that in UITabBarController subclass
var isTabBarHidden:Bool = false
func setTabBarHidden(_ tabBarHidden: Bool, animated: Bool,completion:((Void) -> Void)? = nil) {
if tabBarHidden == isTabBarHidden {
self.view.setNeedsDisplay()
self.view.layoutIfNeeded()
//check tab bar is visible and view and window height is same then it should be 49 + window Heigth
if (tabBarHidden == true && UIScreen.main.bounds.height == self.view.frame.height) {
let offset = self.tabBar.frame.size.height
self.view.frame = CGRect(x:0, y:0, width:self.view.frame.width, height:self.view.frame.height + offset)
}
if let block = completion {
block()
}
return
}
let offset: CGFloat? = tabBarHidden ? self.tabBar.frame.size.height : -self.tabBar.frame.size.height
UIView.animate(withDuration: animated ? 0.250 : 0.0, delay: 0.1, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5, options: [.curveEaseIn, .layoutSubviews], animations: {() -> Void in
self.tabBar.center = CGPoint(x: CGFloat(self.tabBar.center.x), y: CGFloat(self.tabBar.center.y + offset!))
//Check if View is already at bottom so we don't want to move view more up (it will show black screen on bottom ) Scnario : When present mail app
if (Int(offset!) <= 0 && UIScreen.main.bounds.height == self.view.frame.height) == false {
self.view.frame = CGRect(x:0, y:0, width:self.view.frame.width, height:self.view.frame.height + offset!)
}
self.view.setNeedsDisplay()
self.view.layoutIfNeeded()
}, completion: { _ in
if let block = completion {
block()
}
})
isTabBarHidden = tabBarHidden
}
Hope it is helpful
I want to add a UILabel to the view which slides down when an error occurs to send the error message to user and after 3 seconds it will slide up to disappear. The prototype of it is like the one Facebook or Instagram shows. I need errorLabel in many ViewControllers, so I tried to subclass UILabel. Here is my subclass ErrorLabel:
class ErrorLabel: UILabel {
var errorString: String?
func sendErrorMessage() {
self.text = errorString
showErrorLabel()
let timer = NSTimer.scheduledTimerWithTimeInterval(3, target: self, selector: "hideErrorLabel", userInfo: nil, repeats: false)
}
func animateFrameChange() {
UIView.animateWithDuration(1, animations: { self.layoutIfNeeded() }, completion: nil)
}
func showErrorLabel() {
let oldFrame = self.frame
let newFrame = CGRectMake(oldFrame.origin.x, oldFrame.origin.y, oldFrame.height + 30, oldFrame.width)
self.frame = newFrame
self.animateFrameChange()
}
func hideErrorLabel() {
let oldFrame = self.frame
let newFrame = CGRectMake(oldFrame.origin.x, oldFrame.origin.y, oldFrame.height - 30, oldFrame.width)
self.frame = newFrame
self.animateFrameChange()
}
}
Then, I tried to add the errorLabel to one of my ViewController like following:
class ViewController: UIViewController {
var errorLabel = ErrorLabel()
override func viewDidLoad() {
super.viewDidLoad()
let errorLabelFrame = CGRectMake(0, 20, self.view.frame.width, 0)
self.errorLabel.frame = errorLabelFrame
self.errorLabel.backgroundColor = translucentTurquoise
self.errorLabel.font = UIFont.systemFontOfSize(18)
self.errorLabel.textColor = UIColor.whiteColor()
self.errorLabel.textAlignment = NSTextAlignment.Center
self.view.addSubview(errorLabel)
self.view.bringSubviewToFront(errorLabel)
}
func aFunc(errorString: String) {
self.errorLabel.errorString = errorString
self.errorLabel.sendErrorMessage()
}
}
When I run it in iOS Simulator, it doesn't work as expected:
errorLabel shows on the left horizontally and in the middle vertically with only I... which should be Invalid parameters.
After 1 second, it goes to the position as expected but its width is still not self.view.frame.width.
After that, nothing happens but it should slide up after 3 seconds.
Can you tell me what's wrong and how to fix the error?
I might have partial solution to your issues. Hope it helps.
The I... happens when the string is longer than the view. For this you'll need to increase the size of UILabel.
For aligning text inside a UILable refer to this.
To animate away use the same code in the completion block of the UIView.animateWithDuration. Refer to this link
I suggest you to consider using Extensions to accomplish what you are trying to do.
Rather than subclassing UILabel I would subclass UIViewController, which maybe you have aldready done? Let's call out subclass - BaseViewController and let all our UIViewControllers subclass this class.
I would then programatically create an UIView which contains a vertically and horizontally centered UILabel inside this BaseViewController class. The important part here is to create NSLayoutConstraints for it. I would then hide and show it by changing the values of the constraints.
I would use the excellent pod named Cartography to create constraints, which makes it super easy and clean!
With this solution you should be able to show or hide an error message in any of your UIViewControllers
This is untested code but hopefully very near a solution to your problem.
import Cartography /* Requires that you have included Cartography in your Podfile */
class BaseViewController: UIViewController {
private var yPositionForErrorViewWhenVisible: Int { return 0 }
private var yPositionForErrorViewWhenInvisible: Int { return -50 }
private let hideDelay: NSTimeInterval = 3
private var timer: NSTimer!
var yConstraintForErrorView: NSLayoutConstraint!
var errorView: UIView!
var errorLabel: UILabel!
//MARK: - Initialization
required init(aDecoder: NSCoder) {
super.init(aDecoder)
setup()
}
//MARK: - Private Methods
private func setup() {
setupErrorView()
}
private func setupErrorView() {
errorView = UIView()
errorLabel = UILabel()
errorView.addSubview(errorLabel)
view.addSubview(errorView)
/* Set constraints between viewController and errorView and errorLabel */
layout(view, errorView, errorLabel) {
parent, errorView, errorLabel in
errorView.width == parent.width
errorView.centerX == parent.centerX
errorView.height == 50
/* Capture the y constraint, which defaults to be 50 points out of screen, so that it is not visible */
self.yConstraintForErrorView = (errorView.top == parent.top - self.yPositionForErrorViewWhenInvisible)
errorLabel.height = 30
errorLabel.width == errorView.width
errorLabel.centerX == errorView.centerX
errorLabel.centerY = errorView.centerY
}
}
private func hideOrShowErrorMessage(hide: Bool, animated: Bool) {
if hide {
yConstraintForErrorView.constant = yPositionForErrorViewWhenInvisible
} else {
yConstraintForErrorView.constant = yPositionForErrorViewWhenVisible
}
let automaticallyHideErrorViewClosure: () -> Void = {
/* Only scheduling hiding of error message, if we just showed it. */
if show {
automaticallyHideErrorMessage()
}
}
if animated {
view.animateConstraintChange(completion: {
(finished: Bool) -> Void in
automaticallyHideErrorViewClosure()
})
} else {
view.layoutIfNeeded()
automaticallyHideErrorViewClosure()
}
}
private func automaticallyHideErrorMessage() {
if timer != nil {
if timer.valid {
timer.invalidate()
}
timer = nil
}
timer = NSTimer.scheduledTimerWithTimeInterval(hideDelay, target: self, selector: "hideErrorMessage", userInfo: nil, repeats: false)
}
//MARK: - Internal Methods
func showErrorMessage(message: String, animated: Bool = true) {
errorLabel.text = message
hideOrShowErrorMessage(false, animated: animated)
}
//MARK: - Selector Methods
func hideErrorMessage(animated: Bool = true) {
hideOrShowErrorMessage(true, animated: animated)
}
}
extension UIView {
static var standardDuration: NSTimeInterval { return 0.3 }
func animateConstraintChange(duration: NSTimeInterval = standardDuration, completion: ((Bool) -> Void)? = nil) {
UIView.animate(durationUsed: duration, animations: {
() -> Void in
self.layoutIfNeeded()
}, completion: completion)
}
}