The nav bar doesn't clip to the top of iphone x - ios

The Nav bar doesnt go to the top of the screen when rounding the edges on iphone x or up. Is there any way to fix this?
IMAGE(WHAT IT LOOKS LIKE NOW)
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.navigationBar.layer.cornerRadius = 20
self.navigationController?.navigationBar.clipsToBounds = true
self.navigationController?.navigationBar.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
}
}

A default navigation bar has a background view which will be not generated when you apply custom operations on a layer.
Solution
Create a view with a given color, radius corner and dimension
Convert the view to an image
Set the image view to a navigation bar background property
override func viewDidLoad() {
super.viewDidLoad()
let v = UIView(frame: CGRect(x: 0, y: -UIApplication.shared.statusBarFrame.height, width: view.frame.width, height: UIApplication.shared.statusBarFrame.height + (navigationController?.navigationBar.frame.height ?? 0)))
v.backgroundColor = .red
v.layer.cornerRadius = 20
v.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
v.clipsToBounds = true
let i = image(with: v)!
navigationController?.navigationBar.setBackgroundImage(i, for: .default)
}
func image(with view: UIView) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(view.bounds.size, false, 0.0)
defer { UIGraphicsEndImageContext() }
if let context = UIGraphicsGetCurrentContext() {
view.layer.render(in: context)
let image = UIGraphicsGetImageFromCurrentImageContext()
return image
}
return nil
}

You can use xib for add navigation bar for proper navigation bar design. Which is compatible for all devices.
var customNavView: Your_xib_File?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationViewStup()
}
func navigationViewStup(){
customNavView = Bundle.main.loadNibNamed("NavBar", owner: self, options: nil)?[0] as? NavBarView
customNavView?.frame = (self.navigationController?.navigationBar.bounds)!
customNavView?.tag = 1111
customNavView?.btnVoice.addTarget(self, action: #selector(btnVoicePressed), for: .touchUpInside)
self.navigationController?.navigationBar.addSubview(customNavView!)
}
Or try this,
func position(for bar: UIBarPositioning) -> UIBarPosition {
return .topAttached
}
navigationBar.isTranslucent = false

Related

Managing Custom UITabBar View

Following this repo, I created a custom "tabBar" via a separate uiview that sits behind the native uitabbarcontroller tabBar. The custom tabBar has rounded corners and a shadow.
Everything works great except in the instances I push/pop a new uiviewcontroller onto/from the embedded uinavigationcontroller stack, where I hide the tabBar.
My issue is smoothly toggling the custom tabBar to hide/show during these instances.
I've set .isHidden = true/false for the custom tabBar when a uiviewcontroller is pushed/popped, and either the custom tabBar disappears too early or appears too late relative to the native uitabbarcontroller tabBar.
Any guidance would be appreciated.
class TabBarViewController: UITabBarController {
let customTabBarView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = Constants.style.offWhite
view.layer.cornerRadius = 20
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
view.clipsToBounds = true
view.layer.masksToBounds = false
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOffset = CGSize(width: 0, height: -8.0)
view.layer.shadowOpacity = 0.12
view.layer.shadowRadius = 10.0
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
self.tabBar.isHidden = true
self.tabBar.backgroundColor = .clear
self.tabBar.barStyle = .default
self.tabBar.isTranslucent = true
addCustomTabBarView()
hideTabBarBorder()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
customTabBarView.frame = tabBar.frame
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
var newSafeArea = UIEdgeInsets()
newSafeArea.bottom += customTabBarView.bounds.size.height
self.children.forEach({$0.additionalSafeAreaInsets = newSafeArea})
}
//Function invoked when pushing/popping viewControllers onto/from the embedded navigation stack
func toggleCustomTabBarView(shouldHide: Bool) {
self.customTabBarView.isHidden = shouldHide
DispatchQueue.main.async {
UIView.transition(with: self.view, duration: TimeInterval(UINavigationController.hideShowBarDuration), options: .transitionCrossDissolve, animations: {
})
}
}
func addCustomTabBarView() {
customTabBarView.frame = tabBar.frame
view.addSubview(customTabBarView)
view.bringSubviewToFront(self.tabBar)
}
func hideTabBarBorder() {
let tabBar = self.tabBar
tabBar.backgroundImage = UIImage.from(color: .clear)
tabBar.shadowImage = UIImage()
tabBar.clipsToBounds = true
}
}
extension UIImage {
static func from(color: UIColor) -> UIImage {
let rect = CGRect(x: 0, y: 0, width: 1, height: 1)
UIGraphicsBeginImageContext(rect.size)
let context = UIGraphicsGetCurrentContext()
context!.setFillColor(color.cgColor)
context!.fill(rect)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return img!
}
}
https://stackoverflow.com/a/65285566/9466631
Couldn't find anything as a solve for the dummy view behind the tabBar, but this new solution worked for me that avoids using the dummy view altogether and adds a CAShapeLayer instead.

How to overlap status bar when adding UIView on top of UINavigationBar?

I am using UINavigationController as a rootController with UIViewController. In UIViewController, i am adding custom UIView into the navigationBar(because i want to use CAGradientLayer to my custom UIView).
Everything's working fine except that my custom UIView does not overlap the status bar, which means my custom UIView is padding from top of the navigationBar.
Is there any solution and way to make it overlap statusBar? Here i included UI of what i want to achieve.
Status bar is always there so if you don't want it when you add your view you have to override a property prefersStatusBarHidden of the ViewController where your new View is added like so:-
Declare a new variable say:
var myNewViewIsVisible: Bool = false {
didSet {
DispatchQueue.main.async { [weak self] in
guard let self = self else {return}
self.setNeedsStatusBarAppearanceUpdate()
}
}
}
Then override the property like so: -
override var prefersStatusBarHidden: Bool {
return myNewViewIsVisible
}
Explanation:
First you declare your variable and observe it for changes. If it is changed ie.. when you add your new view, change it to true then force the ViewController to update statatusBar, that will cause it to hide because your myNewViewIsVisible will be true.
let gradient = CAGradientLayer()
gradient.frame = CGRect(x: 0, y: 0, width: navigationBar.frame.width, height: 100)
let leftColor = UIColor.red
let rightColor = UIColor.purple
gradient.colors = [leftColor.cgColor, rightColor.cgColor]
gradient.startPoint = CGPoint(x: 0, y: 0.5)
gradient.endPoint = CGPoint(x: 1.0, y: 0.5)
navigationBar.setBackgroundImage(image(fromLayer: gradient), for: .default)
func image(fromLayer layer: CALayer) -> UIImage {
UIGraphicsBeginImageContext(layer.frame.size)
layer.render(in: UIGraphicsGetCurrentContext()!)
let outputImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return outputImage!
}
You need to hide the navigation bar :
self.navigationController?.setNavigationBarHidden(true, animated: false)
Then you need to show status bar :
override var prefersStatusBarHidden: Bool{
return false
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
Also then you have to set the background of the status bar view :
let statusBarView = UIView(frame: CGRect(x: 0, y: 0, width:UIScreen.main.bounds.width, height: UIApplication.shared.statusBarFrame.height))
statusBarView.backgroundColor = UIColor.gray
self.navigationController?.view.addSubview(statusBarView)

Globally apply a gradient to the navigation bar and handle orientation changes

I need to apply a gradient globally to my status and navigation bars and have it adjust properly to orientation changes. Because I want this to be global, I'm trying to use UIAppearance. Surprisingly, UIAppearance doesn't make this very easy.
It looks great in Portrait, but the gradient is too tall when in Landscape so you can't see the whole thing:
Here's my code to this point:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let navigationBarAppearance = UINavigationBar.appearance()
navigationBarAppearance.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
navigationBarAppearance.isTranslucent = false
navigationBarAppearance.tintColor = UIColor.white
let status_height = UIApplication.shared.statusBarFrame.size.height
let gradientLayer = CAGradientLayer(frame: CGRect(x: 0, y: 0, width: 64, height: status_height + 44), colors: [UIColor.init(hex: "005382"), UIColor.init(hex: "00294B")])
let layerImage = gradientLayer.createGradientImage()
navigationBarAppearance.barTintColor = UIColor(patternImage: layerImage ?? UIImage())
}
and I'm using this extension:
extension CAGradientLayer {
convenience init(frame: CGRect, colors: [UIColor]) {
self.init()
self.frame = frame
self.colors = []
for color in colors {
self.colors?.append(color.cgColor)
}
startPoint = CGPoint(x: 0, y: 0)
endPoint = CGPoint(x: 0, y: 1)
}
func createGradientImage() -> UIImage? {
var image: UIImage? = nil
UIGraphicsBeginImageContext(bounds.size)
if let context = UIGraphicsGetCurrentContext() {
render(in: context)
image = UIGraphicsGetImageFromCurrentImageContext()
}
UIGraphicsEndImageContext()
return image
}
}
I know I could check the orientation and then change the gradient accordingly but I'd need to do that on every view controller so that would defeat the purpose of using UIAppearance and being able to do it in one place.
Most of the SO threads I've found provide solutions for making the top bar's gradient at the view controller level, but not the global level.
EDIT:
Tried answer from #Pan Surakami on my UITabBarController but I still have white navigation bars:
Here's my storybaord setup:
And code:
class MenuTabBarController: UITabBarController {
var notificationsVM = NotificationsVModel()
var hasNewAlerts: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
setTabs()
styleUI()
notificationsVM.fetchData { (success, newNotifications) in
if success {
self.hasNewAlerts = newNotifications.count > 0 ? true : false
DispatchQueue.main.async {
if let tabBarItems = self.tabBar.items {
for (_, each) in tabBarItems.enumerated() {
if each.tag == 999 { //only update the alerts tabBarItem tag == '999'
self.updateAlertBadgeIcon(self.hasNewAlerts, each)
}
}
}
}
}
}
}
fileprivate func setTabs() {
let tab1 = GradientNavigationController(rootViewController: FeedViewController())
let tab2 = GradientNavigationController(rootViewController: NotificationsTableViewController())
let tab3 = GradientNavigationController(rootViewController: SearchViewController())
let tab4 = GradientNavigationController(rootViewController: ComposeDiscussionViewController())
UITabBarController().setViewControllers([tab1, tab2, tab3, tab4], animated: false)
}
func updateAlertBadgeIcon(_ hasAlerts: Bool, _ item: UITabBarItem) {
if hasAlerts {
item.image = UIImage(named: "alert-unselected-hasAlerts")?.withRenderingMode(UIImage.RenderingMode.alwaysOriginal)
item.selectedImage = UIImage(named: "alert-selected-hasAlerts")?.withRenderingMode(UIImage.RenderingMode.alwaysOriginal)
} else {
hasNewAlerts = false
item.image = UIImage(named: "alert-unselected-noAlerts")?.withRenderingMode(UIImage.RenderingMode.alwaysOriginal)
item.selectedImage = UIImage(named: "alert-selected-noAlerts")?.withRenderingMode(UIImage.RenderingMode.alwaysOriginal)
}
}
// UITabBarDelegate
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
if item.tag == 999 { //alerts tabBarItem tag == '999'
updateAlertBadgeIcon(hasNewAlerts, item)
}
if item.tag == 0 { //Feed Item clicked
if let feedNav = children[0] as? UINavigationController, let feedVC = feedNav.viewControllers[0] as? FeedViewController {
feedVC.tableView.reloadData()
}
}
}
func styleUI() {
UITabBar.appearance().backgroundImage = UIImage.colorForNavBar(color:.lightGrey4)
UITabBar.appearance().shadowImage = UIImage.colorForNavBar(color:.clear)
tabBar.layer.shadowOffset = CGSize.zero
tabBar.layer.shadowRadius = 2.0
tabBar.layer.shadowColor = UIColor.black.cgColor
tabBar.layer.shadowOpacity = 0.30
UITabBarItem.appearance().setTitleTextAttributes([NSAttributedString.Key.foregroundColor : UIColor.grey2,
NSAttributedString.Key.font: UIFont(name: "AvenirNext-DemiBold", size: 12) as Any],
for: .normal)
UITabBarItem.appearance().setTitleTextAttributes([NSAttributedString.Key.foregroundColor : UIColor.darkSapphire,
NSAttributedString.Key.font: UIFont(name: "AvenirNext-DemiBold", size: 12) as Any],
for: .selected)
}
}
One way to implement it is to subclass UINavigationController.
Create a new subclass.
class GradientNavigationController: UINavigationController {}
Override traitCollectionDidChange method.
class GradientNavigationController: UINavigationController {
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
let status_height = UIApplication.shared.statusBarFrame.size.height
let gradientLayer = CAGradientLayer(frame: CGRect(x: 0, y: 0, width: 64, height: status_height + 44), colors: [[UIColor.init(hex: "005382"), UIColor.init(hex: "00294B")])
let layerImage = gradientLayer.createGradientImage()
self.navigationBar.barTintColor = UIColor(patternImage: layerImage ?? UIImage())
}
}
Use this subclass instead of UINavigationController. Either change custom subclass on storiboards or use it in code.
EDIT:
Your UITabBarController is configured by the storyboard. So setTabs() used this way has no sense. It just creates another copy of UITabBarController and then removes it. I showed it just as an example of embedding controllers.
Remove the method setTabs().
Open each storyboard which is linked to your tabs.
(I cannot see how they are actually configured they are behind storyboard references.)
Make sure that an initial controller is GradientNavigationController.
enter image description here

How to build a context menu like Facebook / Slack on iOS?

I was just looking at Context menu of Facebook and or slack and wanted to create something similar in my App.
I have tried two methods.
First method. Having a in View Table View and sliding it from bottom to create as if it is animated on to the view. But the problem with this is that The navigation controller and Tab bar controller are not hidden and a white patch is shown over the Black (Alpha 30 %).
The second method I tried was showing a new View controller over the current view controller and presenting as a Modal presentation.
let vc = CustomActionTableViewController(nibName: "CustomActionTableViewController", bundle: nil)
vc.modalPresentationStyle = .overFullScreen
self.present(vc, animated: false, completion: nil)
This works okay but the method is too slow as I have to work with lot of Notifications (To send selected index to my main View and then perform action). It is painfully slow.
Could anyone help me with how I can improve the implementation so that I can get the Action sheet similar to Facebook which is smooth and very very fluid
Check this example : Bottom pop Up
Currently I am using this in my app and it's work fine.
Since you mentioned Slack, they actually have open sourced their bottom sheet implementation, PanModal.
Using UIPresentationController and UIPanGestureRecognizer
1- create BottomMenu presentation Controller which will handle the height of your View Controller and blur
class BottomMenuPresentationController: UIPresentationController {
// MARK: - Properties
var blurEffectView: UIVisualEffectView?
var tapGestureRecognizer = UITapGestureRecognizer()
private var topHeightRatio: Float
private var bottomHeightRatio: Float
init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?, topHeightRatio: Float, bottomHeightRatio: Float) {
let blurEffect = UIBlurEffect(style: .systemThickMaterialDark)
blurEffectView = UIVisualEffectView(effect: blurEffect)
self.topHeightRatio = topHeightRatio
self.bottomHeightRatio = bottomHeightRatio
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
blurEffectView?.autoresizingMask = [.flexibleWidth, .flexibleHeight]
tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissController))
self.blurEffectView?.isUserInteractionEnabled = true
self.blurEffectView?.addGestureRecognizer(tapGestureRecognizer)
}
override var frameOfPresentedViewInContainerView: CGRect {
CGRect(origin: CGPoint(x: 0, y: self.containerView!.frame.height * CGFloat(topHeightRatio)),
size: CGSize(width: self.containerView!.frame.width, height: self.containerView!.frame.height * CGFloat(bottomHeightRatio)))
}
override func presentationTransitionWillBegin() {
self.blurEffectView?.alpha = 0
if let blurEffectView = blurEffectView {
self.containerView?.addSubview(blurEffectView)
}
self.presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (_) in
self.blurEffectView?.alpha = 0.66
}, completion: { (_) in })
}
override func dismissalTransitionWillBegin() {
self.presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (_) in
self.blurEffectView?.alpha = 0
}, completion: { (_) in
self.blurEffectView?.removeFromSuperview()
})
}
override func containerViewWillLayoutSubviews() {
super.containerViewWillLayoutSubviews()
presentedView!.roundCorners([.topLeft, .topRight], radius: 14)
}
override func containerViewDidLayoutSubviews() {
super.containerViewDidLayoutSubviews()
presentedView?.frame = frameOfPresentedViewInContainerView
blurEffectView?.frame = containerView!.bounds
}
#objc func dismissController() {
self.presentedViewController.dismiss(animated: true, completion: nil)
}
}
2- create Your ViewController
class BottomMenuVC: UIViewController {
// MARK: - Instances
var hasSetPointOrigin = false
var pointOrigin: CGPoint?
// MARK: - Properties
let topDarkLine: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexString: "#E1E1E1")
view.layer.cornerRadius = 2
return view
}()
let cancelButn: UIButton = {
let button = UIButton(type: .custom)
button.setAttributedTitle(NSAttributedString(string: "Cancel", attributes: [NSAttributedString.Key.font: UIFont.LatoMedium(size: 17),
NSAttributedString.Key.foregroundColor: UIColor(hexString: "#515151")
]), for: .normal)
button.backgroundColor = UIColor(hexString: "#F1F3F4")
button.layer.cornerRadius = 5.0
button.addTarget(self, action: #selector(cancelButnPressed), for: .touchUpInside)
return button
}()
// MARK: - viewLifeCycle
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.isUserInteractionEnabled = true
setupMenuView()
}
override func viewDidLayoutSubviews() {
if !hasSetPointOrigin {
hasSetPointOrigin = true
pointOrigin = self.view.frame.origin
}
}
// MARK: - SetupView
func setupMenuView() {
self.view.addSubview(topDarkLine)
self.view.addSubview(cancelButn)
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panGestureRecognizerAction(_:)))
view.addGestureRecognizer(panGesture)
topDarkLine.constrainHeight(constant: 4)
topDarkLine.constrainWidth(constant: view.frame.size.width * 0.10)
topDarkLine.centerXInSuperview()
topDarkLine.anchor(top: view.topAnchor, leading: nil, bottom: nil, trailing: nil, padding: .init(top: 8, left: 0, bottom: 0, right: 0))
cancelButn.anchor(top:view.topAnchor, leading: view.leadingAnchor, bottom: nil, trailing: view.trailingAnchor,
padding: .init(top: 16, left: 16, bottom: 0, right: 16))
cancelButn.constrainHeight(constant: 44)
}
// MARK: - Actions
#objc func panGestureRecognizerAction(_ sender: UIPanGestureRecognizer) {
let translation = sender.translation(in: view)
// Not allowing the user to drag the view upward
guard translation.y >= 0 else { return }
// setting x as 0 because we don't want users to move the frame side ways!! Only want straight up or down in the y-axis
view.frame.origin = CGPoint(x: 0, y: self.pointOrigin!.y + translation.y)
if sender.state == .ended {
let dragVelocity = sender.velocity(in: view)
if dragVelocity.y >= 1300 {
// Velocity fast enough to dismiss the uiview
self.dismiss(animated: true, completion: nil)
} else {
// If the dragging isn’t too fast, resetting the view back to it’s original point
UIView.animate(withDuration: 0.3) {
self.view.frame.origin = self.pointOrigin ?? CGPoint(x: 0, y: 400)
}
}
}
}
#objc func cancelButnPressed() {
dismiss(animated: true, completion: nil)
}
}
3- make the viewController that contain the button that will present your menu conforms to UIViewControllerTransitioningDelegate
extension viewController: UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
BottomMenuPresentationController(presentedViewController: presented, presenting: presenting, topHeightRatio: 0.6, bottomHeightRatio: 0.4)
}
}
4- set the transitioning delegate to self and present your custom presentation Controller
func showBottomMenu() {
let menu = BottomMenuVC()
menu.coordinator = self
menu.modalPresentationStyle = .custom
menu.transitioningDelegate = self
present(menu, animated: true, completion: nil)
}
check this PanGesture Slidable View article

Cannot change navigation bar item height in iOS 11

After customizing the navigation bar height bigger than the default value (44pt), I want to change the height of my right side navigation bar item button, but it's limited in 44pt. How can I make it taller? I know that in iOS 11, the button now is inside a UIBarButtonStackView, it seems we cannot change the stack view frame?
I use this code to change the width and height of the button:
button.widthAnchor.constraint(equalToConstant: 40).isActive = true
button.heightAnchor.constraint(equalToConstant: 60).isActive = true
button.translatesAutoresizingMaskIntoConstraints = false
button.setImage(image, for: .normal)
let barButton = UIBarButtonItem(customView: button)
self.navigationItem.rightBarButtonItem = barButton
Thank you!
You can change the width of navigation bar button item by using this code -
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
var frame: CGRect? = navigationItem.leftBarButtonItem?.customView?.frame
frame?.size.width = 5 // change the width of your item bar button
self.navigationItem.leftBarButtonItem?.customView?.frame = frame!
}
override var prefersStatusBarHidden : Bool {
return true
}
Or from storyboard -
Make sure your Assets.xcassets image is set as Render As - Original Image Just like -
Using subclass of UInavigationcontroller class and NavigationBar class you can achieve this.
I am sharing some code of snipt:
class ARVNavigationController {
init(rootViewController: UIViewController) {
super.init(navigationBarClass: AVNavigationBar.self, toolbarClass: nil)
viewControllers = [rootViewController] }}
class AVNavigationBar {
let AVNavigationBarHeight: CGFloat = 80.0
init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
init(frame: CGRect) {
super.init(frame: frame ?? CGRect.zero)
initialize()
}
func initialize() {
transform = CGAffineTransform(translationX: 0, y: +AVNavigationBarHeight)
}
func layoutSubviews() {
super.layoutSubviews()
let classNamesToReposition = ["_UINavigationBarBackground", "UINavigationItemView", "UINavigationButton"]
for view: UIView? in subviews() {
if classNamesToReposition.contains(NSStringFromClass(view.self)) {
let bounds: CGRect = self.bounds()
let frame: CGRect? = view?.frame
frame?.origin.y = bounds.origin.y + CGFloat(AVNavigationBarHeight)
frame?.size.height = bounds.size.height - 20.0
view?.frame = frame ?? CGRect.zero
}
}
}
func position(for bar: UIBarPositioning) -> UIBarPosition {
return .topAttached
}
}

Resources