I have a problem that I cannot resolve at the moment namely I'm trying to set a different number of columns in the collection view based on the iPad position (collection should update on device rotation)
Currently, (depending on how the device is positioned on start) on the first rotation items width is not calculated properly:
iPad screen
It'd be great if someone could help and point out where is an issue.
Code for this project is on my GitHub: https://github.com/ceboolion/compositionalLayout
Thanks for your help.
I gave your UICollectionView a background of orange and ran your code and got similar results:
I noticed few things
From this I could conclude the UICollectionView seems to be set up and working well on rotation, probably due to auto layout so I feel the issue is more to do with the layout code.
I also noticed when you start scrolling, the View fixes itself to what you want
Finally, I added one line of code to your func relayoutCollectionView(with size: CGSize) function to see what the dimensions are when you want update the layout calculations and columns
func relayoutCollectionView(with size: CGSize) {
collectionView.collectionViewLayout.shouldInvalidateLayout(forBoundsChange: CGRect(x: 0,
y: 0,
width: size.width,
height: size.height))
collectionView.setCollectionViewLayout(createFlowLayout(),
animated: true,
completion: nil)
collectionView.collectionViewLayout.invalidateLayout()
// I added this line
print("Collection View Width: \(collectionView.frame.size.width), Collection View Height: \(collectionView.frame.size.height)")
}
When I turned from portrait to landscape, the output printed was Collection View Width: 810.0, Collection View Height: 1080.0 which does not seem to be right.
So from this I assume that it is too early for invalidateLayout to be called to get the right calculations.
When I do something like this from a UIViewController, I will make invalidateLayout() call from viewWillLayoutSubviews()
In your case, what I tried and worked was adding the similar override func layoutSubviews() in your class CollectionView: UIView
override func layoutSubviews() {
super.layoutSubviews()
collectionView.collectionViewLayout.invalidateLayout()
}
I removed relayoutCollectionView from your func relayoutCollectionView
Now it seems to work for the first time as well although I can still not explain why your current works every other time except for the first time.
Just as top answer say, it will rotaion to wrong layout after scroll collection.
I try to set animated to false, it work!
But I can't explain why.
collectionView.setCollectionViewLayout(createFlowLayout(), animated: false, completion: nil)
I'm working on a feature that will animate a UIView up from the bottom of the screen to tell the user their data was saved. However, it doesn't appear in the same place on an iPhone 11 Pro Max as it does on an iPhone X.
iPhone 11 Pro Max (this is what I want it to look like):
but on an iPhone X:
I should probably note that I created the view on the storyboard and have an #IBOutlet connected to it. On the storyboard, it's positioned where you see it in the top screenshot. I did not place any constraints on it. The rest is done in code.
The code to animate is very simple:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
savedView.center.y += view.bounds.height // start with it off the screen
savedView.center.x = (view.bounds.width / 2)
}
override func viewDidAppear(_ animated: Bool) {
super.viewWillAppear(true)
// animate it onto the screen
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.0, options: [], animations: {
self.savedView.center.y -= self.view.bounds.height <-- the problem must be here
}, completion: nil)
}
I thought it might be due to different device size classes but all iPhones are 'regular' height so I don't think that's it.
How can I get this view to end up in the same position (relative to the bottom of the screen) on all devices?
In order to place your views in the same place across all devices is a must to have constraints set up. So that, the system is able to show your views correctly.
So to achieve a bottom-centered attached behavior for your view, you need to set up a constraint to the safe-area bottom, a constraint called centered-horizontally, width and height, assuming that this view is the only component on screen. If it is not, then there must be a top constraint for the view with the component in the top of it.
Hope this helps, at least to give you an idea.
Constraints reference
I was able to figure it out.
The problem was that the view's initial center.y position was starting off at 814 based on its position in the storyboard. So it looked good on the storyboard which I'm viewing as an iPhone 11 Pro but, when run on other device simulators with different dimensions, the math didn't work anymore.
The solution was to set the center.y position based on the view.bounds.height giving it a consistent starting position on any device:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
savedView.center.y = (view.bounds.height - 60) // position the view 60 pixels bottom
savedView.center.x = (view.bounds.width / 2) // center it horizontally
savedView.center.y += view.bounds.height // move it outside the visible bounds
}
I am creating a custom progress bar and have ran into more issues. I have a progress bar that show an image going from full to nothing with a shadow that is red in the background. Basically this is a normal progress bar but with a custom image, I want to add a label or something like that on to that will go from 0 to 100% in the time it takes to animate. I tried adding counters to the animate but they do not increment like I want. This is my animation block so far.
override func viewDidAppear(_ animated: Bool) {
UIView.animate(withDuration: 3.0, animations: {
self.catScanE.frame = CGRect(x: 27 , y: 200, width: 320, height: 0)
}, completion: {finished in self.performSegue(withIdentifier: "lastSegue", sender: nil)})
}
Two things about your issue:
You need to set initial frame for catScanE before the animation.
The destination height of catScanE is 0. Did you mean to make it go from
large to small?
I am trying to recreate the effect that YouTube mobile app on iOS has, when a video is playing in full-screen, landscape mode. Just the top part of a window (next/recommended videos) is visible, and swiping up shows them overlapped with video (that keeps running in background). Swiping down hides them again.
So I added the following code within my video controller that shows the video in landscape mode:
private func showPopover() {
let popoverController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "popoverController")
popoverController.modalPresentationStyle = .popover
popoverController.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection(rawValue: 0)
popoverController.popoverPresentationController?.delegate = self
let desiredWidth:CGFloat = 200
let desiredHeight:CGFloat = 300
// Initially it's at low right corner
let origin = CGPoint(x: (self.view.superview?.frame.width)! - desiredWidth - 10, y: (self.view.superview?.frame.height)! - 10)
let size = CGSize(width: desiredWidth, height: desiredHeight)
popoverController.popoverPresentationController?.sourceRect = CGRect(origin: origin, size: size)
popoverController.popoverPresentationController?.sourceView = popoverController.view
self.present(popoverController, animated: true, completion: nil)
}
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
The popover shows up fine, but is always completely on screen. I need it to be only partially visible (just the top 10 pixels, if possible). But something is preventing the view controller from being positioned that way.
How do I move the popover so that only top 10 pixels are visible? It should look something like in the image below (note: this was generating by editing image, not programmatically):
Replace this:
popoverController.popoverPresentationController?.sourceView = popoverController.view
with this:
popoverController.popoverPresentationController?.sourceView = self.view
UPDATE
Ok I understand now. My answer would be it is not possible to only show some part of the popover.
My suggestion here is using UITableViewController as your popover and making its height just enough to show the first row, which is the text in this case.
Then, in optional func scrollViewDidScroll(_ scrollView: UIScrollView) delegate method, implement logics to detect swiping up to show and swiping down to hide.
Found a way, and it's ridiculously easy (with hindsight, of course!). Changed the last line of showPopover function to:
self.present(popoverController, animated: true) {
popoverController.popoverPresentationController?.presentedView?.center = CGPoint(x: (self.view.superview?.frame.width)! - desiredWidth - 10, y: (self.view.superview?.frame.height)! + 100)
}
I will get a specific number instead of using 100 but the view moves to this position without any issues.
How do I make a UIScrollView scroll to the top?
UPDATE FOR iOS 7
[self.scrollView setContentOffset:
CGPointMake(0, -self.scrollView.contentInset.top) animated:YES];
ORIGINAL
[self.scrollView setContentOffset:CGPointZero animated:YES];
or if you want to preserve the horizontal scroll position and just reset the vertical position:
[self.scrollView setContentOffset:CGPointMake(self.scrollView.contentOffset.x, 0)
animated:YES];
Here is a Swift extension that makes it easy:
extension UIScrollView {
func scrollToTop() {
let desiredOffset = CGPoint(x: 0, y: -contentInset.top)
setContentOffset(desiredOffset, animated: true)
}
}
Usage:
myScrollView.scrollToTop()
For Swift 4
scrollView.setContentOffset(.zero, animated: true)
iOS 11 and above
Try to play around with the new adjustedContentInset (It should even work with prefersLargeTitles, safe area etc.)
For example (scroll to the top):
var offset = CGPoint(
x: -scrollView.contentInset.left,
y: -scrollView.contentInset.top)
if #available(iOS 11.0, *) {
offset = CGPoint(
x: -scrollView.adjustedContentInset.left,
y: -scrollView.adjustedContentInset.top)
}
scrollView.setContentOffset(offset, animated: true)
Use setContentOffset:animated:
[scrollView setContentOffset:CGPointZero animated:YES];
Answer for Swift 2.0/3.0/4.0 and iOS 7+:
let desiredOffset = CGPoint(x: 0, y: -self.scrollView.contentInset.top)
self.scrollView.setContentOffset(desiredOffset, animated: true)
In iOS7 I had trouble getting a particular scrollview to go to the top, which worked in iOS6, and used this to set the scrollview to go to the top.
[self.myScroller scrollRectToVisible:CGRectMake(0, 0, 1, 1) animated:NO];
In SWIFT 5
Just set content Offset to zero
scrollView.setContentOffset(CGPoint.zero, animated: true)
Swift 3.0.1 version of rob mayoff's answer :
self.scrollView.setContentOffset(
CGPoint(x: 0,y: -self.scrollView.contentInset.top),
animated: true)
I think I have an answer that should be fully compatible with iOS 11 as well as prior versions (for vertical scrolling)
This takes into account the new adjustedContentInset and also accounts for the additional offset required when prefersLargeTitles is enabled on the navigationBar which appears to require an extra 52px offset on top of whatever the default is
This was a little tricky because the adjustedContentInset changes depending on the titleBar state (large title vs small title) so I needed to check and see what the titleBar height was and not apply the 52px offset if its already in the large state. Couldn't find any other method to check the state of the navigationBar so if anyone has a better option than seeing if the height is > 44.0 I'd like to hear it
func scrollToTop(_ scrollView: UIScrollView, animated: Bool = true) {
if #available(iOS 11.0, *) {
let expandedBar = (navigationController?.navigationBar.frame.height ?? 64.0 > 44.0)
let largeTitles = (navigationController?.navigationBar.prefersLargeTitles) ?? false
let offset: CGFloat = (largeTitles && !expandedBar) ? 52: 0
scrollView.setContentOffset(CGPoint(x: 0, y: -(scrollView.adjustedContentInset.top + offset)), animated: animated)
} else {
scrollView.setContentOffset(CGPoint(x: 0, y: -scrollView.contentInset.top), animated: animated)
}
}
Inspired by Jakub's solution
It's very common when your navigation bar overlaps the small portion of the scrollView content and it looks like content starts not from the top. For fixing it I did 2 things:
Size Inspector - Scroll View - Content Insets --> Change from Automatic to Never.
Size Inspector - Constraints- "Align Top to" (Top Alignment Constraints)- Second item --> Change from Superview.Top to Safe Area.Top and the value(constant field) set to 0
To fully replicate the status bar scrollToTop behavior we not only have to set the contentOffset but also want to make sure the scrollIndicators are displayed. Otherwise the user can quickly get lost.
The only public method to accomplish this is flashScrollIndicators. Unfortunately, calling it once after setting the contentOffset has no effect because it's reset immediately. I found it works when doing the flash each time in scrollViewDidScroll:.
// define arbitrary tag number in a global constants or in the .pch file
#define SCROLLVIEW_IS_SCROLLING_TO_TOP_TAG 19291
- (void)scrollContentToTop {
[self.scrollView setContentOffset:CGPointMake(self.scrollView.contentOffset.x, -self.scrollView.contentInset.top) animated:YES];
self.scrollView.tag = SCROLLVIEW_IS_SCROLLING_TO_TOP_TAG;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
self.scrollView.tag = 0;
});
}
In your UIScrollViewDelegate (or UITable/UICollectionViewDelegate) implement this:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView.tag == SCROLLVIEW_IS_SCROLLING_TO_TOP_TAG) {
[scrollView flashScrollIndicators];
}
}
The hide delay is a bit shorter compared to the status bar scrollToTop behavior but it still looks nice.
Note that I'm abusing the view tag to communicate the "isScrollingToTop" state because I need this across view controllers. If you're using tags for something else you might want to replace this with an iVar or a property.
In modern iOS, set the the scroll view's content offset back to its top left adjustedContentInset:
let point = CGPoint(x: -scrollView.adjustedContentInset.left,
y: -scrollView.adjustedContentInset.top)
scrollView.setContentOffset(point, animated: true)
Scroll to top for UITableViewController, UICollectionViewController or any UIViewController having UIScrollView
extension UIViewController {
func scrollToTop(animated: Bool) {
if let tv = self as? UITableViewController {
tv.tableView.setContentOffset(CGPoint.zero, animated: animated)
} else if let cv = self as? UICollectionViewController{
cv.collectionView?.setContentOffset(CGPoint.zero, animated: animated)
} else {
for v in view.subviews {
if let sv = v as? UIScrollView {
sv.setContentOffset(CGPoint.zero, animated: animated)
}
}
}
}
}
iOS 16
For table and collection views, the following always works for me:
let top = CGRect(x: 0, y: 0, width: 1, height: 1)
tableView.scrollRectToVisible(top, animated: true)
collectionView.scrollRectToVisible(top, animated: true)
For scroll views:
let top = CGPoint(x: 0, y: -adjustedContentInset.top)
scrollView.setContentOffset(top, animated: animated)
adjustedContentInset returns the insets applied by the safe area (if any) and any custom insets applied after instantiation. If either safe or custom insets are applied, the content inset of the scroll view when it's at its top will be negative, not zero, which is why this property should be used.
iOS 2.0+
Mac Catalyst 13.0+
You can try: scrollView.scrollsToTop = true
You can refer it from documentation of developer.apple.com
I tried all the ways. But nothing worked for me. Finally I did like this.
I added self.view .addSubview(self.scroll) line of code in the viewDidLoad. After started setting up frame for scroll view and added components to scroll view.
It worked for me.
Make sure you added self.view .addSubview(self.scroll) line in the beginning. then you can add UI elements.