My VC starts with stackView attached with Align Bottom to Safe Area .
I have tabBar, but in the beginning is hidden tabBar.isHidden = true.
Later when the tabBar appears, it hides the stackView
So I need function that refresh constraints after tabBar.isHidden = false
When I start the app with tabBar.isHidden = false the stackView is shown properly.
Tried with every function like: stackView.needsUpdateConstraints() , updateConstraints() , setNeedsUpdateConstraints() without success.
Now I'm changing the bottom programatically, but when I switch the tabBarIndex and return to that one with changed bottom constraints it detects the tabBar and lifts the stackView under another view (which is not attached with constraints). Like is refreshing again the constraints. I'm hiding and showing this stackView with constrains on/off screen.
I need to refresh constraints after tabBar.isHidden = false, but the constraints don't detect the appearance of the tabBar.
As I mention switching between tabBars fixes the issue, so some code executes to detecting tabBar after the switch. Is anyone know this code? I tried with calling the methods viewDidLayoutSubviews and viewWillLayoutSubviews without success... Any suggestions?
This amateur approach fixed my bug... :D
tabBarController!.selectedIndex = 1
tabBarController!.selectedIndex = 0
Or with an extension
extension UITabBarController {
// Basically just toggles the tabs to fix layout issues
func forceConstraintRefresh() {
// Get the indices we need
let prevIndex = selectedIndex
var newIndex = 0
// Find an unused index
let items = viewControllers ?? []
find: for i in 0..<items.count {
if (i != prevIndex) {
newIndex = i
break find
}
}
// Toggle the tabs
selectedIndex = newIndex
selectedIndex = prevIndex
}
}
Usage (called when switching dark / light mode):
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
tabBarController?.forceConstraintRefresh()
}
If you want to update view's layout, you can try layoutIfNeeded() function.
after updating stackView constraints call this method:
stackView.superview?.layoutIfNeeded()
Apple's Human Interface Guidelines indicate that one should not mess around with the Tab Bar, which is why (I'm guessing) setting tabBar.isHidden doesn't properly update the rest of the view hierarchy.
Quick searching comes up with various UITabBarController extensions for showing / hiding the tab bar... but they all appear to push the tabBar down off-screen, rather than setting its .isHidden property. May or may not be suitable for your use.
I'm assuming from your comments that your VC in tab index 0 has a button (or some other action) to show / hide the tabBar?
If so, here is an approach that may do the job....
Add this enum in your project:
enum TabBarState {
case toggle, show, hide
}
and put this func in that view controller:
func showOrHideTabBar(state: TabBarState? = .toggle) {
if let tbc = self.tabBarController {
let b: Bool = (state == .toggle) ? !tbc.tabBar.isHidden : state == .hide
guard b != tbc.tabBar.isHidden else {
return
}
tbc.tabBar.isHidden = b
view.frame.size.height -= 0.1
view.setNeedsLayout()
view.frame.size.height += 0.1
}
}
You can call it with:
// default: toggles isHidden
showOrHideTabBar()
// toggles isHidden
showOrHideTabBar(state: .toggle)
// SHOW tabBar (if it's hidden)
showOrHideTabBar(state: .show)
// HIDE tabBar (if it's showing)
showOrHideTabBar(state: .hide)
I would expect that simply pairing .setNeedsLayout() with .layoutIfNeeded() after setting the tabBar's .isHidden property should do the job, but apparently not.
The quick frame height change (combined with .setNeedsLayout()) does trigger auto-layout, though, and the height change is not visible.
NOTE: This is the result of very brief testing, on one device and one iOS version. I expect it will work across devices and versions, but I have not done complete testing.
Related
I'd like to implement a nav style like what is found in the "Add Reminder" view controller from Apple's Reminders (iOS 14). I've tried hooking into the scrollview delegate methods but I'm not sure how to change the alpha of the default nav bar background/shadow image.
I've tried changing the nav bar style on scroll and, while that works, it doesn't fade in/out like in the example. That makes me think the answer lies manipulating the alpha value. Thanks in advance!
I've found a (hacky) solution that works in iOS 14 (untested in other versions). It makes an assumption about the private view structure of UINavigationBar, so there's no guarantee that it will work in future iOS versions, but it's unlikely to crash - the worst that should happen is that the bar will fail to hide, or only partially hide.
Assuming that you are placing the code inside a UIViewController subclass that it acting as the delegate for a UITableView, UICollectionView or UIScrollView, the following should work:
override func viewDidLoad() {
super.viewDidLoad()
// this hides the bar initially
self.navigationController?.navigationBar.subviews.first?.alpha = 0
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let navigationController = self.navigationController else { return }
let navBarHeight = navigationController.navigationBar.frame.height
let threshold: CGFloat = 20 // distance from bar where fade-in begins
let alpha = (scrollView.contentOffset.y + navBarHeight + threshold) / threshold
navigationController.navigationBar.subviews.first?.alpha = alpha
}
The magic threshold value is a little hard to explain, but it's basically the distance from the bar at which the fade in will start. A value of 20 means the bar starts to fade in when the scrollView content is 20 points away. A value of 0 would mean the bar snaps straight from fully transparent to fully opaque the moment the scrollView content touches it.
After reading a few articles about custom UITabBarControllers, I am left more confused than before I even started doing the research in the first place.
My goal is to create a custom TabBar with 3 important properties:
No text, just icons
The active icon is marked by a circle filled with a color behind it, and therefore needs a different icon color
Here's what I am trying to achieve:
I've been able to remove the text and center the icon by following another StackOverflow answer (Remove tab bar item text, show only image), although the solution seems like a hack to me.
How would I go about creating a circle behind the item and change the active item's color?
Also, would someone mind explaining the difference between the XCode inspector sections "Tab Bar Item" and "Bar Item", which appear directly under each other?
The first step is simple: leaving the title property of the UITabbarItem empty should hide the label.
Your second step can actually be broken down into two steps: changing the color of the icon and adding a circle behind it.
The first step here is simple again: you can set a different icon to use for the currently selected ViewController (I use Storyboards, this process is pretty straightforward). What you'd do is add a white version of the icon to be shown when that menu option is selected.
The final step is displaying the circle. To do this, we'll need the following information:
Which item is currently selected?
What is the position of the icon on the screen?
The first of these two is pretty easy to find out, but the second poses a problem: the icons in a UITabBar aren't spaced around the screen equally, so we can't just divide the width of the tabbar by the amount of items in it, and then take half of that to find the center of the icons. Instead, we will subclass UITabBarController.
Note: the tabBar property of a UITabBarController does have a .selectionIndicatorImage property. You can assign an image to this and it will be shown behind your icon. However, you can't easily control the placement of this image, and that is why we still resort to subclassing UITabBarController.
class CircledTabBarController: UITabBarController {
var circle: UIView?
override func viewDidLoad() {
super.viewDidLoad()
let numberOfItems = CGFloat(tabBar.items!.count)
let tabBarItemSize = CGSize(width: (tabBar.frame.width / numberOfItems) - 20, height: tabBar.frame.height)
circle = UIView(frame: CGRect(x: 0, y: 0, width: tabBarItemSize.height, height: tabBarItemSize.height))
circle?.backgroundColor = .darkGray
circle?.layer.cornerRadius = circle!.frame.width/2
circle?.alpha = 0
tabBar.addSubview(circle!)
tabBar.sendSubview(toBack: circle!)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let index = -(tabBar.items?.index(of: tabBar.selectedItem!)?.distance(to: 0))!
let frame = frameForTabAtIndex(index: index)
circle?.center.x = frame.origin.x + frame.width/2
circle?.alpha = 1
}
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
let index = -(tabBar.items?.index(of: item)?.distance(to: 0))!
let frame = frameForTabAtIndex(index: index)
self.circle?.center.x = frame.origin.x + frame.width/2
}
func frameForTabAtIndex(index: Int) -> CGRect {
var frames = tabBar.subviews.compactMap { (view:UIView) -> CGRect? in
if let view = view as? UIControl {
for item in view.subviews {
if let image = item as? UIImageView {
return image.superview!.convert(image.frame, to: tabBar)
}
}
return view.frame
}
return nil
}
frames.sort { $0.origin.x < $1.origin.x }
if frames.count > index {
return frames[index]
}
return frames.last ?? CGRect.zero
}
}
Now use this subclass of UITabBarController instead of the base class.
So why this approach over simply changing the icon to a circled one? Because you can do many different things with this. I wrote an article about animating the UITabBarController in a similar manner, and if you like, you can easily use above implementation to add animation to yours too.
The easiest and actually cleanest way to do it is to design your icons and import them as images to the .xcassets folder. Then you can just set the different icons for the different states for each of the viewControllers with:
ViewController.tabBarItem = UITabBarItem(title: "", image: yourImage.withRenderingMode(.alwaysOriginal), selectedImage: yourImage)
your selected image will be the one with the circle and the image will be without. It is way easier than manipulating the images in xcode and it is also less expensive since the compiler only has to render the images and doesn't have to manipulate them.
About the other question UIBarItem is
An abstract superclass for items that can be added to a bar that appears at the bottom of the screen.
UITabBarItem is a subclass of UIBarItem to provide extra funtionality.
I'm working with Cocoa and I create my views in code (no IB) and I'm hitting an issue with NSSplitView.
I have a NSSplitView that I configure in the following way in my view controller, in Swift:
override func viewDidLoad() {
super.viewDidLoad()
let splitView = NSSplitView()
splitView.isVertical = true
splitView.addArrangedSubview(self.createLeftPanel())
splitView.addArrangedSubview(self.createRightPanel())
splitView.adjustSubviews()
self.view.addSubview(splitView)
...
}
The resulting view shows the two subviews and the divider for the NSSplitView, and one view is wider than the other. When I drag the diver to change the width, as soon as I release the mouse, the divider goes back to its original position, as if pulled back by a "spring".
I can't resize the two subviews; the right one always keeps a fixed size. However, nowhere in the code I fix the width of that subview, or any of its content, to a constant.
What I would like to achieve instead is that the right view size is not fixed, and that if I drag the divider at halfway through, the subviews will resize accordingly and end up with the same width.
This is a screen recording of the problem:
Edit: here is how I set the constraints. I'm using Carthography, because otherwise setting constraints in code is extremely verbose beyond the most simple cases.
private func createLeftPanel() -> NSView {
let view = NSView()
let table = self.createTable()
view.addSubview(table)
constrain(view, table) { view, table in // Cartography magic.
table.edges == view.edges // this just constraints table.trailing to
// view.trailing, table.top to view.top, etc.
}
return view
}
private func createRightPanel() -> NSView {
let view = NSView()
let label = NSTextField(labelWithString: "Name of item")
view.addSubview(label)
constrain(view, label) { view, label in
label.edges == view.edges
}
return view
}
My app has quite a few buttons on each screen as well as a UIBarButtonItem back button and I have problems with people being able to multi click buttons. I need only 1 button to be clickable at a time.
Does anyone know how to make a UIBarButtonItem back button exclusive to touch?
I've managed to disable multi clicking the UIButtons by setting each one's view to isExclusiveTouch = true but this doesn't seem to count for the back button in the navigation bar.
The back button doesn't seem to adhere to isExclusiveTouch.
Does anyone have a simple work around that doesn't involve coding each and every buttons send events?
Many Thanks,
Krivvenz.
you can enable exclusive touch simply this will stop multiple touch until first touch is not done
buttton.exclusiveTouch = true
You could write an extension for UIBarButtonItem to add isExclusiveTouch?
you can simply disable the multi-touch property of the super view. You can also find this property in the storyboard.
you could try this for the scene where you want to disable multiple touch.
let skView = self.view as! SKView
skView.isMultipleTouchEnabled = false
I have found a solution to this. isExclusiveTouch is the solution in the end, but the reason why this property didn't do anything is because you need to set it also on all of the subviews of each button that you want to set as isExclusiveTouch = true. Then it works as expected :)
It's working fine in Swift
self.view.isMultipleTouchEnabled = false
buttonHistory.isExclusiveTouch = true
In addition of #Luky LĂzal answer, you need pay attention that all subviews of a view you want disable multi-touching (exactly all in hierarchy, actually subviews of subviews) must be set as isExclusiveTouch = true.
You can run through all of them recursively like that:
extension UIView
{
func allSubViews() -> [UIView] {
var all: [UIView] = []
func getSubview(view: UIView) {
all.append(view)
guard view.subviews.count > 0 else { return }
view.subviews.forEach{ getSubview(view: $0) }
}
getSubview(view: self)
return all
}
}
// Call this method when all views in your parent view were set
func disableMultipleTouching() {
self.isMultipleTouchEnabled = false
self.allSubViews().forEach { $0.isExclusiveTouch = true }
}
I'm developing my app to work with iOS7.
I have a UINavigationController I'm pushing a UIViewController that has a ScrollView inside it. Inside the scrollView I have a tableView.
Is it possible to achieve that when I scroll the tableView inside the scrollView the list will appear behind that Status bar. Same why it would be if I had a UINavigationController and a UIViewController with a tableView in it.
So this it the hierarchy :
UINavigationController -> UIViewController -> UIScrollView -> UITableView .
and I want that when a user scroll the table,the cells in the top will be visible under the status bar.
If there is no UIScrollView it happens automatically in iOS7.
Thanks.
Just set automaticallyAdjustsScrollViewInsets to NO in the viewController init method.
In Storyboard, you can switch the property directly in the property panel when the UIViewController is selected.
If you use xib, just set it like this:
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self.automaticallyAdjustsScrollViewInsets = NO;
}
Note: this is right since iOS7 and still in iOS8.
none of the above workd for me, until I noticed that I had to set Content Insets from Automatically to Never in the interfacebuilder:
Starting with iOS 11 you can use this new property with a fallback (Swift 4):
if #available(iOS 11.0, *) {
scrollView.contentInsetAdjustmentBehavior = .never
} else {
self.automaticallyAdjustsScrollViewInsets = false
}
The answer from Skoua might work in some situations, but does have certain side-effects on iOS11 and later. Most notably, the scroll view will start propagating safe areas to its children, which can mess up your layout while scrolling if you use the safe areas or layout margins.
Apple explains this very well and in detail in this WWDC session and also mentions explicitly that contentInsetAdjustmentBehavior = .never can have side-effects and is not what you should use in most cases.
To get a scroll view that does not mess up our layout, but shows its content below the status bar (or navigation bar), you should observe the safe area of your scroll view and adjust your custom content insets accordingly:
private var scrollViewSafeAreaObserver: NSKeyValueObservation!
override func viewDidLoad() {
...
if #available(iOS 11.0, *) {
self.scrollViewSafeAreaObserver = self.scrollView.observe(\.safeAreaInsets) { [weak self] (_, _) in
self?.scrollViewSafeAreaInsetsDidChange()
}
} else {
self.automaticallyAdjustsScrollViewInsets = false
}
}
#available(iOS 11.0, *)
func scrollViewSafeAreaInsetsDidChange() {
self.scrollView.contentInset.top = -self.scrollView.safeAreaInsets.top
}
deinit {
self.scrollViewSafeAreaObserver?.invalidate()
self.scrollViewSafeAreaObserver = nil
}
Why does this work? Because we leave contentInsetAdjustmentBehavior = .automatic. This will give us normal behaviour when it comes to scrolling and non-scrolling axis, but the UIScrollView will also "convert" any safe areas to content insets. Since we don't want this for our top edge, we simply set the negative top safe area as our custom insets, which will counter any insets set by the scroll view.
Thats just dumb from Apple. One more weird behaviour to worry about. Anyhow, I ended up setting the scroll view content inset for top to -20 (from IB).
I found the solution! Just set:
self.automaticallyAdjustsScrollViewInsets = false
on the view controller that has the UIScrollView.
You probably has seen this recommendation a thousand times but, check the 'The Status Bar' section on the iOS 7 transition guide(can't post the direct link here).
Blunt resuming, on ios7 the status bar is part of the view. This means that almost anything you put on your view, will be under the bar, unless you say the contrary. A work around i found for that problem was making the status bar opaque.
Another way could be disabling the status bar on that specific view. Like on this thread.
I have had a similar sort of problem, and I found that to ensure my scrollview content doesn't go under the status bar, I applied a setContentInset.
So in your situation, if you are using an inset, I would use suggest using UIScrollViewDelegate or scrollViewDidScroll tailored to your tableview. If a scroll is occurring, disregard the scrollview inset.
don't hide the statusBar, the scrollView won't jump