I currently have a UITableView whose HeaderView is a UIScrollView. I am using this as a sort of carousel that displays constantly rotating information triggered to move on a timer every two seconds. What I am trying to do is detect when the user has moved the scroll view on their own. Here is the code I have to handle the ScrollView movement right now.
override func viewDidLoad() {
var myTimer = Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(runTimedCode), userInfo: nil, repeats: true)
}
func runTimedCode() {
var itemCount = newsArray.count // how many items are in the news reel
if currentPage < itemCount {
let newX = CGFloat(currentPage) * self.view.frame.width // calculate next page position
featuredScrollView.setContentOffset(CGPoint(x: newX, y: 0), animated: true)
currentPage += 1
} else {
// end of items
currentPage = 0
let newX = CGFloat(currentPage) * self.view.frame.width // calculate next page position
print("new x = \(newX)")
featuredScrollView.setContentOffset(CGPoint(x: newX, y: 0), animated: true)
}
}
However the scrollViewDidScroll function is only called when the tableview is moved. Is there any way to detect when the ScrollView above the tableView is scrolled? I'm completely stuck on this any help is appreciated. Thanks!
First, make sure that you have the delegate set up so that your view controller gets events from both the scroll view and the table view.
Next, keep references to make sure that you can distinguish the two views.
Finally, check which reference is triggering the viewdidscroll function and do your code there.
let view1:UIScrollView!
let view2:UITableView!
view1.delegate = self
view2.delegate = self
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == view1 {
// Do something
}
if scrollView == view2 {
// Do something
}
}
Related
I have a ViewControllerOne with a tableView constrained to a superview and filled with a content. User can scroll down some content, then switch to ViewControllerTwo and change tableView data source content on another.
When that happens and user returns to the ViewControllerOne I want the VC to be reset on its initial state at the top with a Large Title and a new content, but with a workaround I found it scrolls only till the tableView top and stops on a Small Title.
Here is the code:
When user picks a new Data Source in ViewControllerTwo I save it as a bool in UserDefaults:
UserDefaults.standard.set(true, forKey: "newDataSourcePicked")
In ViewControllerOne I trigger the scrolling method in a viewWillAppear():
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
scrollVCUp()
}
Here is scrollVCUp(). Here I use the saved bool. Also use delay because its not scrolling without it:
func scrollVCUp() {
if newDataSourcePicked {
traitCollection.verticalSizeClass == .compact ? setVCOffset(with: view.safeAreaInsets.top, and: updateLabelTopInset, delayValue: 0.1) : setVCOffset(with: biggestTopSafeAreaInset, and: updateLabelTopInset, delayValue: 0.1)
UserDefaults.standard.set(false, forKey: "newDataSourcePicked")
}
}
Here is setVCOffset():
func setVCOffset(with viewInset: CGFloat, and labelInset: CGFloat, delayValue: Double = 0.0) {
let firstVC = navigationController?.viewControllers.first as? CurrencyViewController
guard let scrollView = firstVC?.view.subviews.first(where: { $0 is UIScrollView }) as? UIScrollView else { return }
if delayValue > 0.0 {
DispatchQueue.main.asyncAfter(deadline: .now() + delayValue) {
scrollView.setContentOffset(CGPoint(x: 0, y: -(viewInset - labelInset)), animated: true)
}
} else {
scrollView.setContentOffset(CGPoint(x: 0, y: -(viewInset - labelInset)), animated: true)
}
}
I also have a tabBar and when I use the same code to scroll ViewControllerOne by tapping on a tabBar it scrolls and shows a Large Title, but doesn't work if we switch to another VC and back.
Here is a gif:
What should I do to scroll and always show a Large Title?
I found two possible approaches:
Approach 1
Don't use the same UIViewController instance, that holds the UITableView. Create a new one.
(Your case: when ViewControllerOne push ViewControllerTwo).
With this approach you get the "fresh" layout with large title every time you push the VC.
Approach 2
Scroll by calculating the UITableView.contentOffset. Use for that adjustedContentInset.top and round the value.
With this approach you get the same result like approach 1, but with a visible back scrolling animation.
class ViewControllerTwo {
private var _adjustedContentInsetTopRounded: CGFloat?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let y = _adjustedContentInsetTopRounded {
DispatchQueue.main.async {
self.tableView.setContentOffset(
CGPoint(
x: 0,
y: -y
),
animated: true
)
}
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
_adjustedContentInsetTopRounded = tableView.adjustedContentInset.top.rounded(.up)
}
}
The bottommost cell of my table view is a cell with a textField. When the user taps it, I want to scroll it so that the cell is right above the keyboard.
When I call the scrollRectToVisible(...) with animated false everything works as expected, but when animated is set to true the table scrolls the cell only so far, that the bottom of the textField is right above the keyboard (See left picture). Yet the bottonInsets should be correct, since I can scroll the cell the last bit manually and the cell sits right how it should (See right picture).
I think the table view scrolling the textField's bottom edge above the keyboard is the default behavior of a table view, but I'm afraid I don't know why it seems to override my own scrolling when I want it animated.
Left picture:
The textFields bottom edge right above the keyboard (I kept the border style so you can see it better).
Right picture:
How I want it. Cell's bottom edge right above the keyboard.
func repositionTextfieldCell(in tableView: UITableView) {
guard let textFieldCell = tableView.bottommostCell() else { return }
guard let keyboardRect = activeKeyboardRect else { return }
// - Adjust insets
var bottomInset = keyboardRect.size.height
tableView.contentInset.bottom = bottomInset
tableView.scrollIndicatorInsets.bottom = bottomInset
// - Make cell visible
let x = textFieldCell.frame.minX
let y = textFieldCell.frame.maxY
tableView.scrollRectToVisible(CGRect(origin: CGPoint(x: x, y: y),
size: CGSize(width: 1, height: 1)), animated: true)
}
add this in viewDidLoad() and create a NSlayout constraint for tableview bottom.
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillShow),
name: NSNotification.Name.UIKeyboardWillShow,
object: nil
)
create the function
#objc func keyboardWillShow(_ notification: Notification) {
if let keyboardFrame: NSValue = notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue {
let keyboardRectangle = keyboardFrame.cgRectValue
let keyboardHeight = keyboardRectangle.height
tableBottomConstraint.constant = self.view.frame.height - keyboardHeight
}
}
repeat the process to reset the tableBottomConstraint.constant = 0 in keyboardWillHide() method.
I could fix the problem.
The behavior seems to be depended on were scrollRectToVisible(...) is called. The behavior I described in the question occurs when scrollRectToVisible(...) is called in keyboardDidShow(...).
However when you call scrollRectToVisible(...) in keyboardWillShow(...) and set animated to false the cell / rect is pushed up by the keyboard sliding in. Which I think looks great.
I have one collection view configured as follow
superCollectionView!.alwaysBounceHorizontal = false
superCollectionView!.alwaysBounceVertical = false
if #available(iOS 10.0, *) {
superCollectionView!.refreshControl = refreshControl
} else {
superCollectionView!.backgroundView = refreshControl
}
but bounce effect is still there.
I want to remove bounce from bottom...
If you want to remove bouncing only from the bottom (For letting the refreshControl to be available), I'd suggest to handle it in scrollViewDidScroll: method to check if the scroll view contentOffset.y has been reached to bottom of the scroll view (logically, it is the content size of the scroll view minus the height of the visible frame of the scroll view), as follows:
Solution:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y >= scrollView.contentSize.height - scrollView.frame.size.height {
scrollView.setContentOffset(CGPoint(x: scrollView.contentOffset.x, y: scrollView.contentSize.height - scrollView.frame.size.height), animated: false)
}
}
Output:
After implementing scrollViewDidScroll as mentioned above, it should be behaves like:
Also:
What about achieving the opposite?
Referring to the above description, preventing the top bouncing would be:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y < 0 {
scrollView.setContentOffset(CGPoint(x: scrollView.contentOffset.x, y: 0), animated: false)
}
}
I want to have my tableViewHeaders visible as the user scrolls by pinning to the top which is the current behaviour in my tableView. However, when the tableView stops scrolling, I want to remove these 'pinned' headers. I am achieving this in my collectionView project using the following in my scrollView delegate methods:
if let cvl = chatCollectionView.collectionViewLayout as? UICollectionViewFlowLayout {
cvl.sectionHeadersPinToVisibleBounds = false
cvl.invalidateLayout()
}
Is there a similar way to hide a tableView's 'pinned' (sticky) headers? I am using a tableViewController.
This is my solution to this issue. I wonder if there is a simpler way to do this though.
Please note, this will only work if your header is a UITableViewHeaderFooterView. Not if you are using a UITableViewCell for a header. If you are using a UITableViewCell, tableView.headerView(forSection: indexPathForVisibleRow.section) will return nil.
In order to hide the pinned headers when the tableView stops scrolling and have them re-appear when the tableView starts scrolling again, override these four scrollView delegate methods.
In the first two (scrollViewWillBeginDragging and scrollViewWillBeginDecelerating), get the section header for the first section of the visible rows and make sure it is not hidden.
In the second two delegate methods, do a check to see that for each of the visible rows, the header frame for that row is not overlapping the frame for the row cell. If it is, then this is a pinned header and we hide it after a delay. We need to ensure that the scrollView is not still dragging before removing the pinned header as will be the case when the user lifts their finger but the scroll view continues to scroll. Also because of the time delay, we check that the scrollView is not dragging before removing it in case the user starts scrolling again less than 0.5 seconds after the scroll stops.
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
showPinnedHeaders()
}
override func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
showPinnedHeaders()
}
override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
removePinnedHeaders()
}
override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
removePinnedHeaders()
}
private func showPinnedHeaders() {
for section in 0..<totalNumberOfSectionsInYourTableView {
tableView.headerView(forSection: section)?.isHidden = false
}
}
private func removePinnedHeaders() {
if let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows {
if indexPathsForVisibleRows.count > 0 {
for indexPathForVisibleRow in indexPathsForVisibleRows {
if let header = tableView.headerView(forSection: indexPathForVisibleRow.section) {
if let cell = tableView.cellForRow(at: indexPathForVisibleRow) {
if header.frame.intersects(cell.frame) {
let seconds = 0.5
let delay = seconds * Double(NSEC_PER_SEC)
let dispatchTime = DispatchTime.now() + Double(Int64(delay)) / Double(NSEC_PER_SEC)
DispatchQueue.main.asyncAfter(deadline: dispatchTime, execute: {
if !self.tableView.isDragging && header.frame.intersects(cell.frame) {
header.isHidden = true
}
})
}
}
}
}
}
}
}
Additionally add removePinnedHeaders() to viewDidAppear() and any other rotation or keyboard frame change methods that will scroll your tableView.
In my tvOS app I have a TabBarController with 3 viewControllers. What I want to do is to automatically hide/change focus of the tabBar when I switch to the next viewController.
I saw some posts here, on SO that suggested to change alfa on the tabBar, but I would like to have a slide up animation, same way as it does when you change focus to something in the viewController.
Any kind of help is highly appreciated.
As Charles said.. Something like this in the derived UITabBarController:
var focusOnChildVC : Bool = false {
didSet {
self.setNeedsFocusUpdate()
}
};
override weak var preferredFocusedView: UIView? {
get {
let v : UIView?;
let focused = UIScreen.mainScreen().focusedView
//A bit of a hack but seems to work for picking up whether the VC is active or not
if (focusOnChildVC && focused != nil) {
v = self.selectedViewController?.preferredFocusedView
} else {
//If we are focused on the main VC and then clear out of property as we're done with overriding the focus now
if (focusOnChildVC) {
focusOnChildVC = false
}
v = super.preferredFocusedView;
}
return v
}
}
The basic idea of the solution described below is to subclass UITabBarController and selectively use the super implementation of weak var preferredFocusedView: UIView? { get } or one that returns selectedViewController?.preferredFocusView along with an implementation of didUpdateFocusInContext(_:withAnimationCoordinator:) that sets up an NSTimer that triggers a focus update and sets a flag that controls the preferredFocusView implementation.
More verbosely, Subclass UITabBarController and override didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator). In your implementation (make sure to call the super implementation) you can inspect the context and determine if a descendent view of the tabBar property is the nextFocusedView or the previousFocusedView (and the nextFocusedView is not a descendent).
If the tab bar is gaining focus you can create an NSTimer for the duration that you want to show the tab bar before hiding it. If the tab bar loses focus before the timer fires, invalidate it. If the timer fires, call setNeedsFocusUpdate() followed by updateFocusIfNeeded().
The last piece you need to get this to work is a flag that is set to true while the timer is set. You then need to override weak var preferredFocusedView: UIView? { get } and call the super implementation if the flag is false and if it is true return selectedViewController?.preferredFocusedView.
You can do it in a UITabBarController subclass:
final class TabBarViewController: UITabBarController {
private(set) var isTabBarHidden = false
func setTabBarHidden(_ isHidden: Bool, animated: Bool) {
guard isTabBarHidden != isHidden else {
return
}
var frame: CGRect
let alpha: CGFloat
if isHidden {
frame = tabBar.frame
frame.origin.y -= frame.height
alpha = 0
} else {
frame = tabBar.frame
frame.origin.y += frame.height
alpha = 1
}
let animations = {
self.tabBar.frame = frame
self.tabBar.alpha = alpha
}
if animated {
UIView.animate(withDuration: 0.3, animations: animations)
} else {
animations()
}
isTabBarHidden = isHidden
}
}