customizing UICollectionView.contentOffset prior to appearance - swift

I have a UICollectionView where I want to set the collectionView.contentOffset.x value to the screen's midpoint. I want the collectionView's content to start at the screen's midpoint but to be scrollable to the left of that. Using collectionView.setContentOffset, I can do it after the view appears. However, I want it to appear on the screen when the view loads, already at that position.
I've tried using
func collectionView(_ collectionView: UICollectionView, targetContentOffsetForProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
return CGPoint(x: collectionView.frame.width/2, y: 0)
}
but it has no effect. My delegate and dataSource are set.
I've tried using collectionView.setContentOffset in viewDidLoad, then in viewDidLayoutSubviews, animated set to false. This didn't work.
Do I need a custom layout, or can I do without?

Alright, so found the answer on my own. In short, updating the UI on the main thread was the answer. Calling
DispatchQueue.main.async { [weak self] in
self?.scrubberCollectionView.setContentOffset(CGPoint(x: (self?.collectionViewStartingPoint)!, y: 0), animated: false)
}
from ViewDidLoad() worked like a charm. I saw it right after I started this morning. I really didn't think this question required me to post my code, but someone else might have seen it right away.
So, three takeaways: post code on StackOverflow, update the UI on the main thread, and take a break occasionally.

Related

scrollViewDidScroll called with strange contentOffsetY

I have a UIViewController that holds a NSFetchedResultsController. After the insertion of rows to the top, I want to keep the rows visible as they where before the insertions. This means I need to make some calculations to keep the contentOffSetY right after the update. The calculation is correct, but I noticed that scrollViewDidScroll gets called after it scrolled to my specified contentOffsetY, this results in a corrupted state. This is the logging:
Will apply an corrected Y value of: 207.27359771728516
Scrolled to: 207.5
Corrected to: 207.27359771728516
Scrolled to: 79.5 <-- Why is this logline here?
You can directly clone an example project: https://github.com/Jasperav/FetchResultControllerGlitch (commit https://github.com/Jasperav/FetchResultControllerGlitch/commit/d46054040139afeeb648e1e0b5b113bd98685b4a, the newest version of the project only glitches, the weird call to the scrollViewDidScroll method is now gone. If you fix the glitch I award the bounty. Just clone the newest version, run it and scroll a little bit. You will see strange content offset's (glitches)). Run the project and you will see the strange output. This is the controllerDidChangeContent method:
public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
let currentSize = tableView.contentSize.height
UIView.performWithoutAnimation {
tableView.endUpdates()
let newSize = tableView.contentSize.height
let correctedY = tableView.contentOffset.y + newSize - currentSize
print("Will apply an corrected Y value of: \(correctedY)")
tableView.setContentOffset(CGPoint(x: 0,
y: correctedY),
animated: false)
print("Corrected to: \(correctedY)")
}
}
If I call tableView.layoutIfNeeded right after the tableView.endUpdates(), the delegate is already called. What does it cause to call the delegate method? Is there any way it does not scroll?
I downloaded your code and made some tweaks to fix the glitching issue. Here is what I have done.
set estimatedRowHeight property of table view to some number
init() {
super.init(frame: .zero, style: .plain)
estimatedRowHeight = 50.0
register(MyTableViewCell.self, forCellReuseIdentifier: "cell")
}
created a function to handle the UITableView reload action without modifying the current contentOffset
func reloadDataWithoutScroll() {
let lastScrollOffset = contentOffset
beginUpdates()
reloadData()
layoutIfNeeded()
endUpdates()
layer.removeAllAnimations()
setContentOffset(lastScrollOffset, animated: false)
}
Updated controllerDidChangeContent function to make use of the reloadDataWithoutScroll function
UIView.performWithoutAnimation {
tableView.performBatchUpdates({
tableView.insertRows(at: inserts, with: .automatic)
})
inserts.removeAll()
tableView.reloadDataWithoutScroll()
}
When I execute with these changes, it doesn't scroll when a new row is added. However, it does scroll to show the new added row when the current contentOffset is 0. And I don't think that would be a problem, logically speaking.

How do you animate a UICollectionView using auto layout anchors?

I'm trying to animate a collectionview by using layout constraints however I cannot get it to work. I have a collectionview that takes up the entire screen and on a button tap I want to essentially move the collectionview up to make room for another view to come in from the bottom - see image below
The incoming UIView animates just fine (the view coming up from the bottom) - The reason I want to move the collectionview is that the incoming UIView obscures the collection view so am just trying to move the collectionview up at the same time as the new view so that all of the content in the collectionview can be displayed without being hidden by the new view - I use a reference view to get the right layout constraints for the final position for the collectionview Image to show what I am trying to achieve Am I going about it the right way?
Nothing happens with the code example below and I am not sure where to go from here - the same approach is used for animating the incoming view and works just fine but doesn't seem to work for the collectionview...
Any help would be kindly appreciated
var colViewBottomToReferenceTop: NSLayoutConstraint?
var colViewBottomToViewBottom: NSLayoutConstraint?
override func viewDidLoad() {
super.viewDidLoad()
colViewBottomToReferenceTop = musicCollectionView.bottomAnchor.constraint(equalTo: referenceView.topAnchor)
colViewBottomToViewBottom = musicCollectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
NSLayoutConstraint.active([colViewBottomToViewBottom!])
NSLayoutConstraint.deactivate([colViewBottomToReferenceTop!])
}
func playerShow() {
NSLayoutConstraint.activate([colViewBottomToReferenceTop!])
UIView.animate(withDuration: 0.5, animations: {
self.view.layoutIfNeeded()
})
}
#IBAction func btnTapped(_ sender: UIButton) {
playerShow()
}
You want to deactivate the other before animation
NSLayoutConstraint.deactivate([colViewBottomToViewBottom!])
NSLayoutConstraint.activate([colViewBottomToReferenceTop!])
Look to this Demo

Prefer Large Titles and RefreshControl not working well

I am using this tutorial to implement a pull-to-refresh behavior with the RefreshControl. I am using a Navigation Bar. When using normal titles everything works good. But, when using "Prefer big titles" it doesn't work correctly as you can see in the following videos. Anyone knows why? The only change between videos is the storyboard check on "Prefer Large Titles".
I'm having the same problem, and none of the other answers worked for me.
I realised that changing the table view top constraint from the safe area to the superview fixed that strange spinning bug.
Also, make sure the constant value for this constraint is 0 🤯.
At the end what worked for me was:
In order to fix the RefreshControl progress bar disappearing bug with large titles:
self.extendedLayoutIncludesOpaqueBars = true
In order to fix the list offset after refreshcontrol.endRefreshing():
let top = self.tableView.adjustedContentInset.top
let y = self.refreshControl!.frame.maxY + top
self.tableView.setContentOffset(CGPoint(x: 0, y: -y), animated:true)
If you were using tableView.tableHeaderView = refreshControl or tableView.addSubView(refreshControl) you should try using tableView.refreshControl = refreshControl
It seems there are a lot of different causes that could make this happen, for me I had a TableView embedded within a ViewController. I set the top layout guide of the tableview to the superview with 0. After all of that still nothing until I wrapped my RefreshControl end editing in a delayed block:
DispatchQueue.main.async {
if self.refreshControl.isRefreshing {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: {
self.refreshControl.endRefreshing()
})
}
}
The only working solution for me is combining Bruno's suggestion with this line of code:
tableView.contentInsetAdjustmentBehavior = .always
I've faced the same problem. Call refreshControl endRefreshing before calling further API.
refreshControl.addTarget(controller, action: #selector(refreshData(_:)), for: .valueChanged)
#objc func refreshData(_ refreshControl: UIRefreshControl) {
refreshControl.endRefreshing()
self.model.loadAPICall {
self.tableView.reloadData()
}
}
The only solution that worked for me using XIBs was Bruno's one:
https://stackoverflow.com/a/54629641/2178888
However I did not want to use a XIB. I struggled a lot trying to make this work by code using AutoLayout.
I finally found a solution that works:
override func loadView() {
super.loadView()
let tableView = UITableView()
//configure tableView
self.view = tableView
}
I had this issue too, and i fixed it by embedded my scrollView (or tableView \ collectionView) inside stackView, and it's important that this stackView's top constraint will not be attached to the safeArea view (all the other constraints can). the top constraint should be connect to it's superview or to other view.
I was facing the same issue for very long, the only working solution for me was adding refresh control to the background view of tableview.
tableView.backgroundView = refreshControl
Short Answer
I fixed this by delaying calling to API until my collection view ends decelerating
Long Answer
I notice that the issue happens when refresh control ends refreshing while the collection view is still moving up to its original position. Therefore, I delay making API call until my collection view stops moving a.k.a ends decelerating. Here's a step by step:
Follow Bruno's suggestion
If you set your navigation bar's translucent value to false (navigationBar.isTranslucent = false), then you will have to set extendedLayoutIncludesOpaqueBars = true on your view controller. Otherwise, skip this.
Delay api call. Since I'm using RxSwift, here's how I do it.
collectionView.rx.didEndDecelerating
.map { [unowned self] _ in self.refreshControl.isRefreshing }
.filter { $0 == true }
.subscribe(onNext: { _ in
// make api call
})
.disposed(by: disposeBag)
After API completes, call to
refreshControl.endRefreshing()
Caveat
Do note that since we delay API call, it means that this whole pull-to-refresh process is not as quick as it could have been done without the delay.
Unfortunately, no advice helped. But I found a solution that helped me. Setting the transparency of the navigation bar helped.enter image description here
Problem can be solved if add tableview or scroll view as root view in UIViewController hierarchy (like in UITableViewController)
override func loadView() {
view = customView
}
where customView is UITableView or UICollectionView

How to dismiss keyboard in UISearchController when changing focus in tvos?

I'm completely new to tvos and I'm trying to implement a UISearchController view where, in my SearchResultsViewController, I have two UICollectionViews displayed one above the other:
The problem is that when the user swipes down to select one of the items in the UICollectionView, the keyboard doesn't dismiss. Even swiping back up to select the keyboard doesn't fully scroll up and it's impossible to see what you're typing. The resulting view is this:
Ideally, I'd like to dismiss the keyboard when the user swipes down to focus on anything else in the interface. I looked at Apple's tvos UIKit Catalog and their example shows a UISearchController which dismisses the keyboard when changing focus, but I don't see that they're doing anything differently.
Here is the code I'm using to setup my UISearchController when the user clicks on a button:
#IBAction func onSearchButton(sender: AnyObject) {
guard let resultsController = storyboard?.instantiateViewControllerWithIdentifier(SearchResultsViewController.storyboardID) as? SearchResultsViewController else { fatalError("Unable to instantiate a SearchResultsViewController.") }
// Create and configure a `UISearchController`.
let searchController = UISearchController(searchResultsController: resultsController)
searchController.searchResultsUpdater = resultsController
searchController.hidesNavigationBarDuringPresentation = false
let searchPlaceholderText = NSLocalizedString("Search for a Show or Movie", comment: "")
searchController.searchBar.placeholder = searchPlaceholderText
// Present the search controller from the root view controller.
guard let rootViewController = view.window?.rootViewController else { fatalError("Unable to get root view controller.") }
rootViewController.presentViewController(searchController, animated: true, completion: nil)
}
After quite a bit of trial and error, I was able to figure out the solution.
The keyboard will automatically dismiss itself as long as:
1) The item the user focuses on is inside of a scrollview
2) The scrollview content size is larger than the screen height by at least 1px (1081px).
After quite a lot of trial and error, finally I figured out.
The reason is that you have nested ScrollViews in searchResultsController.
"ScrollViews" of-course includes UICollectionView, UITableView, and UIScrollView.
According to my investigation, UISearchController behaves as follows.
If the first view which gets focused in searchResultsController is subview of the inner scrollView (which is the horizontal UICollectionView, in your case), then you won't get keyboard hidden as expected.
Interestingly, if the first view which gets focused in searchResultsController is subview of outer scrollView, then you will get keyboard hidden completely, animated, just as expected (!).
I think this is sort of UIKit's bug.
I had exactly same layout and wasn't able to achieve this so far. I believe you return false in tableView(tableView: UITableView, canFocusRowAtIndexPath indexPath: NSIndexPath) -> Bool so that each cells in collection view can scroll horizontally with proper focus behavior. I think it's actually causing the issues. If you make the first cell in the tableview focusable the problem goes away but of course focus behavior is not desired. I just found that out today and will try more tomorrow to find out what I can do about this. I sense that I will need a new design that allows me to use a single collectionview or tableview that has its cells focusable in resultsController. Hope this is easily achievable in tvOS 10.
Work Around Solution: Add one dummy cell at indexPath.row == 0 with height as 1 pixel and enable the focus on it.

Reset scroll on UICollectionView

I have a horizontal UICollectionView which works fine and scrolls. When I tap an item I update my data and call reloadData. This works and the new data is displayed in the UICollectionView.
The problem is the scroll position doesn't change and it is still viewing the last place. I want to reset the scrolling to the top (Or left in my case). How can I do this?
You want setContentOffset:. It takes a CGPoint as and argument that you can set to what ever you want using CGPointMake, but if you wish to return to the very beginning of the collection, you can simply use CGPointZero.
[collectionView setContentOffset:CGPointZero animated:YES];
You can use this method to scroll to any item you want:
- (void)scrollToItemAtIndexPath:(NSIndexPath *)indexPath
atScrollPosition:(UICollectionViewScrollPosition)scrollPosition
animated:(BOOL)animated
I use this quite often in different parts of my app so I just extended UIScrollView so it can be used on any scroll view and scroll view subclass:
extension UIScrollView {
/// Sets content offset to the top.
func resetScrollPositionToTop() {
self.contentOffset = CGPoint(x: -contentInset.left, y: -contentInset.top)
}
}
So whenever I need to reset the position:
self.collectionView.resetScrollPositionToTop()
In Swift:
collectionView.setContentOffset(CGPointZero, animated: true)
If you leave the view that houses the collection view, make a change in the 2nd view controller, and need the collection view to update upon returning:
#IBAction func unwindTo_CollectionViewVC(segue: UIStoryboardSegue) {
//Automatic table reload upon unwind
viewDidLoad()
collectionView.reloadData()
collectionView.setContentOffset(CGPointZero, animated: true)
}
In my situation the unwind is great because the viewDidLoad call will update the CoreData context, the reloadData call will make sure the collectionView updates to reflect the new CoreData context, and then the contentOffset will make sure the table sets back to the top.
If you view controller contains safe area, code:
collectionView.setContentOffset(CGPointZero, animated: true)
doesn't work, so you can use universal extension:
extension UIScrollView {
func scrollToTop(_ animated: Bool) {
var topContentOffset: CGPoint
if #available(iOS 11.0, *) {
topContentOffset = CGPoint(x: -safeAreaInsets.left, y: -safeAreaInsets.top)
} else {
topContentOffset = CGPoint(x: -contentInset.left, y: -contentInset.top)
}
setContentOffset(topContentOffset, animated: animated)
}
}
CGPointZero in my case shifted the content of the collection view because you are not taking in count the content inset. This is what it worked for me:
CGPoint topOffest = CGPointMake(0,-self.collectionView.contentInset.top);
[self.collectionView setContentOffset:topOffest animated:YES];
Sorry I couldn't comment at the time of this writing, but I was using a UINavigationController and CGPointZero itself didn't get me to the very top so I had to use the following instead.
CGFloat compensateHeight = -(self.navigationController.navigationBar.bounds.size.height+[UIApplication sharedApplication].statusBarFrame.size.height);
[self.collectionView setContentOffset:CGPointMake(0, compensateHeight) animated:YES];
Hope this helps somebody in future. Cheers!
To deal with an UINavigationController and a transparent navigation bar, I had to calculate the extra top offset to perfectly match the position of the first element.
Swift 1.1 code:
let topOffest = CGPointMake(0, -(self.collectionView?.contentInset.top ?? O))
self.collectionView?.setContentOffset(topOffest, animated: true)
Cheers!
SWIFT 4.2 SOLUTION-
Say I have a tableView with each tableViewCell containing a HorizontalCollectionView
Due to reuse of cells, even though first tableViewCell is scrolled to say page 4, downwards, other cell is also set to page 4.
This works in such cases-
In the cellForRowAt func in tableView,
let cell = //declaring a cell using deque
cell.myCollectionView.contentOffset = CGPoint(x:0,y:0)