Reveal UISearchBar when UITableView is dragged from the first row - swift

My search bar is at the top of the screen, hiding underneath the navigation bar. I'd like to show the search bar when the user is panning, only when the table view is at the top (at row 0). Can someone explain this?
I've tried using the pan gesture, but then the search bar will show every time I pan downwards.
func showSearchBar(recognizer: UIPanGestureRecognizer) {
if recognizer.state == .Changed {
let translation = recognizer.translationInView(view)
if searchBarConstraint.constant < searchBar.bounds.height {
searchBarConstraint.constant += translation.y
}
}
}

As UITableView is a subclass of UIScrollView you can use its contentOffset to determine whether the table view is at the top or not:
let isAtTop = tableView.contentOffset.y == 0
Of course this only works it you have not set the default contentOffset to another point. Otherwise you would have to check for the y value of your default contentOffset.

Related

How to make a nav bar transparent before scrolling (iOS 14)

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.

Check if view is visible on the screen [Swift 5.1]

I have this View Controller that contains the bigTitle label (Please ignore the right-to-left):
In this situation, (bigTitle is visible), I want the top Navigation Bar to not contain any text (but still be visible!)
But when the user scrolls down in the scrollView and the bigTitle is not visible anymore, I want the Navigation Bar to contain the text that was in the bigTitle, in this case it's Welcome to our app!
This is my current code (right now it's not completed and it's in the viewDidLoad()) (feel free to change anything you want):
_ = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true, block: { (time) in
// If bigTitle is visible on the screen
if true {
self.bigTitle.alpha = 1
self.navBar.title = "" // navBar is my Navigation Bar reference
} else {
self.bigTitle.alpha = 0
self.navBar.title = self.bigTitle.text
}
})
Thanks!
Don't use a timer to track what happens when scrolling; use the scroll view's delegate. As the user scrolls, you are notified in the delegate method. Examine the label's frame; convert it to window coordinates to discover whether it is off the screen.

Swift - Programmatically refresh constraints

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.

How to scroll UICollectionView without touching it by dragging another element in Swift?

Hello! I'm trying to set up a CollectionView, that scrolls only when user scrolls another element (view with arrows on the image). Also, depending on which cell is in the middle of the screen, text in label should change to match this cell.
I've done the scrolling part this way: when user pans view with arrows, collection also moves, but not scrolls:
else if sender.state == .changed {
if scrollButtonImageView.frame.maxY < scrollRangeView.frame.maxY {
scrollButtonImageView.center = CGPoint(x: originalPosition.x, y: originalPosition.y + translation.y)
collectionView.center.y = scrollButtonImageView.center.y
My question is: Which is the proper way to scroll CollectionView without touching it and how to observe which cell is in the center of the screen and change data in labels instantly, when another cell got into center?
UPD: Now I'm using UISlider with same number of values as collection. But it is not very smooth. My code:
let currentValue = Int(sender.value)
collectionView.scrollToItem(at: IndexPath(row: currentValue, section: 0), at: .top, animated: true)
One plan might be to describe a rectangle for the area that you'd like to consider as in view (maybe as a fixed rectangle above the collection view) and then use the CGRect functions contains or intersection to identify when a child view of the collection is within the target area.

Using UIPageViewControllers scroll over entire view, including UIButtons

I have a UIView with a PageViewController and 4 UIButtons as subviews. The PageViewController and the button subviews are independent from each other. (PageViewController scrolls across 4 pages while the UIButtons remain constant)
The problem I'm facing is that the buttons take quite a bit of space and I'd like to use this scroll gesture anywhere on the UIView including the areas with buttons.
If the user taps on the button, its respective action would occur but I'd like if I can START a scroll/swipe gesture on a button and have the PageViewControllers scroll feature work.
I've tried/searched several ways including using UIScrollView instead of a PageViewController. Any help would be greatly appreciated. Thanks in advance. :)
Just add your own gesture recogniser and watch for gestures which velocity is greater in the x direction and going from left to right.
override func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
if (gestureRecognizer.isMemberOfClass(UIPanGestureRecognizer)){
let point:CGPoint = (gestureRecognizer as UIPanGestureRecognizer).velocityInView(self)
if (abs(point.x) > abs(point.y)){
return true
}
}
}
Handle the gesture over here:
func handlePanGestureRecognizer(gesture: UIPanGestureRecognizer){
let translation:CGPoint = gesture.translationInView(self)
if(translation.x > 0){
//its going from left to right...
//...transition to the previous page
}else{
//...transition to the next page
}