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.
Related
I have a weird problem. I'm trying to calculate the exact height between the bottom of the navigation bar and the top of the keyboard no matter which iOS device I'm running the app on. Here is the method where I'm doing this calculation:
#objc func keyboardWillShow(_ notification: Notification) {
if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
let keyboardRectangle = keyboardFrame.cgRectValue
let keyboardHeight = keyboardRectangle.height
let navigationBarHeight: CGFloat = self.navigationController!.navigationBar.frame.height
let viewableArea = screenSize.height - keyboardRectangle.height - reportPostInstructionContainerViewHeight - characterCountContainerViewHeight - reportPostInstructionContainerViewHeight + 4
//iPhone 12 and above is "- 20"; iPhone 8 needs to be "+ 4"; iPhone 12 mini is "- 24"
print("**** navigationBarHeight: \(navigationBarHeight)")
print("**** keyboardHeight: \(keyboardHeight)")
print("**** screenSize.height: \(screenSize.height)")
print("**** total screen height - keyboard height: \(screenSize.height - keyboardRectangle.height)")
print("**** viewableArea: \(viewableArea)")
textViewHeight = viewableArea
print("**** textViewHeight: \(textViewHeight)")
UIView.animate(withDuration: 0.01, animations: { () -> Void in
self.textView.anchor(
top: self.horizontalDividerView.bottomAnchor,
leading: self.view.leadingAnchor,
bottom: nil,
trailing: self.view.trailingAnchor,
identifier: "ReportPostPFAVC.textView.directionalAnchors",
size: .init(width: 0, height: self.textViewHeight)
)
self.textView.layoutIfNeeded()
})
}
}
The line where "viewableArea" is being calculated seems to be the issue. For example, if I'm running the app on an iPhone 8, I need to add 4 to this calculation in order to size the text view properly.
Here is an image for reference:
I'm trying to get the bar with the "Report" button to sit perfectly on top of the keyboard. But, if I test on different devices sometimes I need to subtract 20 or 24 instead of adding 4.
I don't really understand where this gap is coming from.
Can anyone advise?
I will try to approach this from a different angle as I am not sure where exactly your issue is and what exactly was your logic from the code alone that you provided.
What you need to achieve is to find the coordinates of two frames in the same coordinate system. The two frames being; the keyboard frame and the navigation bar frame. And the "same coordinate system" is best defined by one of your views such as the view of your view controller.
There are convert methods on UIView which are designed to convert frames to/from different coordinate systems such as views.
So in your case all you need to do is
let targetView = self.view!
let convertedNavigationBarFrame = targetView.convert(self.navigationController!.navigationBar.bounds, from: self.navigationController!.navigationBar)
let convertedKeyboardFrame = targetView.convert(keyboardFrame.cgRectValue, from: nil)
In this example I used self.view as my coordinate system in which I want the two frames. This means the coordinates will be within a view controller. To get a height between two frames (which is your question) I could use absolutely any view that is in same window hierarchy and I should be getting the same result.
Then in this example I convert bounds of navigation bar from navigation bar, to target view. I found this to be best approach when dealing with UIView frames.
Last I convert keyboard frame to target view. The keyboard frame has a screen coordinate system which leads to using from: nil.
Getting the vertical distance between them is then a simple subtraction of two vertical coordinates
convertedKeyboardFrame.minY - convertedNavigationBarFrame.maxY
To have a full example I cerated a new project. In storyboard:
I added a navigation controller
I set the navigation controller to be "initial".
I set the old auto-generated view controller to be the root view controller of the navigation controller.
I added a text field which will trigger the
appearance of keyboard.
Then applied the following code:
The example code:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
self.navigationController?.navigationBar.backgroundColor = .green
}
private lazy var checkView: UIView = {
let checkView = UIView(frame: .zero)
checkView.backgroundColor = UIColor.black
checkView.layer.borderColor = UIColor.red.cgColor
checkView.layer.borderWidth = 5
self.view.addSubview(checkView)
return checkView
}()
#objc func keyboardWillShow(_ notification: Notification) {
if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
let targetView = self.view!
let convertedNavigationBarFrame = targetView.convert(self.navigationController!.navigationBar.bounds, from: self.navigationController!.navigationBar)
let convertedKeyboardFrame = targetView.convert(keyboardFrame.cgRectValue, from: nil)
checkView.frame = CGRect(x: 30.0, y: convertedNavigationBarFrame.maxY, width: 100.0, height: convertedKeyboardFrame.minY - convertedNavigationBarFrame.maxY)
}
}
}
The checkView appears between navigation bar and keyboard to show that the computation is correct. The view should fill the space between the two items and border is used to show that this view does not stretch below keyboard or above navigation bar.
I cannot find a way to present a popover view that is just as big as its contents. I'm using the following code:
func show_popup(_ popVC: UIViewController, sender: UIButton)
{
let v = sender
popVC.modalPresentationStyle = .popover
let popOverVC = popVC.popoverPresentationController
popOverVC?.delegate = self
popOverVC?.sourceView = v
popOverVC?.sourceRect = CGRect(x: v.bounds.midX, y: v.bounds.minY, width: 0, height: 0)
//popVC.preferredContentSize = popVC.view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
//popVC.preferredContentSize = CGSize(width: 220, height: 140)
present(popVC, animated: true)
popup = popVC
}
If I explicitly set the preferredContentSize to 220x140 then the popover is displayed with that size, like this:
Image showing explicitly set popover size
If I set the preferredContentSize to systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) then the popover does not set its size to the content (what is desired) but the popover is sized to the system default, like this:
Image showing system default popover size
Nevermind, I found a workaround. Since the popup only consists of a linear list of buttons I can calculate the width of the text for the widest button and then set the popup size based upon that width.
I use the following code to scroll to top of the UICollectionView:
scrollView.scrollRectToVisible(CGRect(origin: .zero, size: CGSize(width: 1, height: 1)), animated: true)
However, on iOS 11 and 12 the scrollView only scrolls to the top, without revealing the large title of the UINavigationBar (when prefersLargeTitle has ben set to true.)
Here is how it looks like:
The result I want to achieve:
It works as it is designed, you are scrolling to position y = 0, assign your controller to be UIScrollView delegate and print out scroll offset:
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
print(scrollView.contentOffset)
}
You will see when Large title is displayed and you move your scroll view a but and it jumps back to the Large title it will not print (0.0, 0.0) but (0.0, -64.0) or (0.0, -116.0) - this is the same value as scrollView.adjustedContentInset, so if you want to scroll up and display large title you should do:
scrollView.scrollRectToVisible(CGRect(x: 0, y: -64, width: 1, height: 1), animated: true)
You don't want to use any 'magic values' (as -64 in the currently accepted answer). These may change (also, -64 isn't correct anyway).
A better solution is to observe the SafeAreaInsets changes and save the biggest top inset. Then use this value in the setContentOffset method. Like this:
class CollectioViewController: UIViewController {
var biggestTopSafeAreaInset: CGFloat = 0
override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()
self.biggestTopSafeAreaInset = max(ui.safeAreaInsets.top, biggestTopSafeAreaInset)
}
func scrollToTop(animated: Bool) {
ui.scrollView.setContentOffset(CGPoint(x: 0, y: -biggestTopSafeAreaInset), animated: animated)
}
}
It seems that using a negative content offset is the way to go.
I really like the idea of Demosthese to keep track of the biggest top inset.
However, there is a problem with this approach.
Sometime large titles cannot be displayed, for example, when an iPhone is in landscape mode.
If this method is used after a device has been rotated to landscape then the offset of the table will be too much because the large title is not displayed in the navigation bar.
An improvements to this technique is to consider biggestTopSafeAreaInset only when the navigation bar can display a large title.
Now the problem is to understand when a navigation bar can display a large title.
I did some test on different devices and it seems that large titles are not displayed when the vertical size class is compact.
So, Demosthese solution can be improved in this way:
class TableViewController: UITableViewController {
var biggestTopSafeAreaInset: CGFloat = 0
override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()
self.biggestTopSafeAreaInset = max(view.safeAreaInsets.top, biggestTopSafeAreaInset)
}
func scrollToTop(animated: Bool) {
if traitCollection.verticalSizeClass == .compact {
tableView.setContentOffset(CGPoint(x: 0, y: -view.safeAreaInsets.top), animated: animated)
} else {
tableView.setContentOffset(CGPoint(x: 0, y: -biggestTopSafeAreaInset), animated: animated)
}
}
}
There is still a case that could cause the large title to not be displayed after the scroll.
If the user:
Open the app with the device rotated in landscape mode.
Scroll the view.
Rotate the device in portrait.
At this point biggestTopSafeAreaInset has not yet had a chance to find the greatest value and if the scrollToTop method is called the large title will be not displayed.
Fortunately, this is a case that shouldn't happen often.
Quite late here but I have my version of the story.
Since iOS 11 there is the adjustedContentInset on the scroll view.
That however reflects only the current state of the UI thus if the large navigation title is not revealed, it won't be taken into account.
So my solution is to make couple of extra calls to make the system consider the large title size and calculate it to the adjustedContentInset:
extension UIScrollView {
func scrollToTop(animated: Bool = true) {
if animated {
// 1
let currentOffset = contentOffset
// 2
setContentOffset(CGPoint(x: 0, y: -adjustedContentInset.top - 1), animated: false)
// 3
let newAdjustedContentInset = adjustedContentInset
// 4
setContentOffset(currentOffset, animated: false)
// 5
setContentOffset(CGPoint(x: 0, y: -newAdjustedContentInset.top), animated: true)
} else {
// 1
setContentOffset(CGPoint(x: 0, y: -adjustedContentInset.top - 1), animated: false)
// 2
setContentOffset(CGPoint(x: 0, y: -adjustedContentInset.top), animated: false)
}
}
}
Here is what's happening:
When animated:
Get the current offset to be able to apply it again (important for achieving the animation)
Scroll without animating to the currently calculated adjustedContentInset plus some more because the large title was not considered when calculating the adjustedContentInset
Now the system takes into account the large title so get the current adjustedContentInset that will include its size so store it to a constant that will be used in the last step
Scroll back to the original offset without animating so no visual changes will be noticed
Scroll to the previously calculated adjustedContentInset this time animating to achieve the desired animated scrolling
When !animated:
Scroll without animation to the adjustedContentInset plus some more. At this stage the system will consider the large title so...
Scroll to the current adjustedContentInset as it was calculated with the large title in it
Kind of a hack but does work.
I am trying to get something like this to work. This is the Uber App. Where a user can swipe another view up, in front of a background view.
The background view is fairly simple, it has been done already. The view which will be swiped on top will be a UITableView. I want the user to be able to see just a little top part first when the app launches, then upon swiping a little it should stop in the middle and then after fully swiping up should take it all the way to the top, replacing the Background view.
Frameworks I have looked at are pullable view for iOS. But it is way too old and doesn't get any nice animations across. I have also looked at SWRevealViewController but I can't figure out how to swipe up from below.
I have also tried to use a button so when a user clicks on it, the table view controller appears modally, covering vertical, but that is not what I want. It needs to recognize a gesture.
Any help is greatly appreciated. Thanks.
I'm aware that the question is almost 2 and a half years old, but just in case someone finds this through a search engine:
I'd say that your best bet is to use UIViewPropertyAnimator. There's a great article about it here: http://www.swiftkickmobile.com/building-better-app-animations-swift-uiviewpropertyanimator/
EDIT:
I managed to get a simple prototype working with UIViewPropertyAnimator, here's a GIF of it:
Here's the project on Github: https://github.com/Luigi123/UIViewPropertyAnimatorExample
Basically I have two UIViewControllers, the main one called ViewController and the secondary one called BottomSheetViewController. The secondary view has an UIPanGestureRecognizer to make it draggable, and inside the recognizer's callback function I do 3 things (after actually moving it):
① calculate how much percent of the screen have been dragged,
② trigger the animations in the secondary view itself,
③ notify the main view about the drag action so it can trigger it's animations. In this case I use a Notification, passing the percentage inside notification.userInfo.
I'm not sure how to convey ①, so as an example if the screen is 500 pixels tall and the user dragged the secondary view up to the 100th pixel, I calculate that the user dragged it 20% of the way up. This percentage is exactly what I need to pass into the fractionComplete property inside the UIViewPropertyAnimator instances.
⚠️ One thing to note is that I couldn't make it work with an actual navigation bar, so I used a "normal" view with a label in it's place.
I tried making the code smaller by removing some utility functions like checking if the user interaction is finished, but that means that the user can stop dragging in the middle of the screen and the app wouldn't react at all, so I really suggest you see the entire code in the github repo. But the good news is that the entire code that executes the animations fits in about 100 lines of code.
With that in mind, here's the code for the main screen, ViewController:
import UIKit
import MapKit
import NotificationCenter
class ViewController: UIViewController {
#IBOutlet weak var someView: UIView!
#IBOutlet weak var blackView: UIView!
var animator: UIViewPropertyAnimator?
func createBottomView() {
guard let sub = storyboard!.instantiateViewController(withIdentifier: "BottomSheetViewController") as? BottomSheetViewController else { return }
self.addChild(sub)
self.view.addSubview(sub.view)
sub.didMove(toParent: self)
sub.view.frame = CGRect(x: 0, y: view.frame.maxY - 100, width: view.frame.width, height: view.frame.height)
}
func subViewGotPanned(_ percentage: Int) {
guard let propAnimator = animator else {
animator = UIViewPropertyAnimator(duration: 3, curve: .linear, animations: {
self.blackView.alpha = 1
self.someView.transform = CGAffineTransform(scaleX: 0.8, y: 0.8).concatenating(CGAffineTransform(translationX: 0, y: -20))
})
animator?.startAnimation()
animator?.pauseAnimation()
return
}
propAnimator.fractionComplete = CGFloat(percentage) / 100
}
func receiveNotification(_ notification: Notification) {
guard let percentage = notification.userInfo?["percentage"] as? Int else { return }
subViewGotPanned(percentage)
}
override func viewDidLoad() {
super.viewDidLoad()
createBottomView()
let name = NSNotification.Name(rawValue: "BottomViewMoved")
NotificationCenter.default.addObserver(forName: name, object: nil, queue: nil, using: receiveNotification(_:))
}
}
And the code for the secondary view (BottomSheetViewController):
import UIKit
import NotificationCenter
class BottomSheetViewController: UIViewController, UIGestureRecognizerDelegate {
#IBOutlet weak var navBarView: UIView!
var panGestureRecognizer: UIPanGestureRecognizer?
var animator: UIViewPropertyAnimator?
override func viewDidLoad() {
gotPanned(0)
super.viewDidLoad()
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(respondToPanGesture))
view.addGestureRecognizer(gestureRecognizer)
gestureRecognizer.delegate = self
panGestureRecognizer = gestureRecognizer
}
func gotPanned(_ percentage: Int) {
if animator == nil {
animator = UIViewPropertyAnimator(duration: 1, curve: .linear, animations: {
let scaleTransform = CGAffineTransform(scaleX: 1, y: 5).concatenating(CGAffineTransform(translationX: 0, y: 240))
self.navBarView.transform = scaleTransform
self.navBarView.alpha = 0
})
animator?.isReversed = true
animator?.startAnimation()
animator?.pauseAnimation()
}
animator?.fractionComplete = CGFloat(percentage) / 100
}
// MARK: methods to make the view draggable
#objc func respondToPanGesture(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: self.view)
moveToY(self.view.frame.minY + translation.y)
recognizer.setTranslation(.zero, in: self.view)
}
private func moveToY(_ position: CGFloat) {
view.frame = CGRect(x: 0, y: position, width: view.frame.width, height: view.frame.height)
let maxHeight = view.frame.height - 100
let percentage = Int(100 - ((position * 100) / maxHeight))
gotPanned(percentage)
let name = NSNotification.Name(rawValue: "BottomViewMoved")
NotificationCenter.default.post(name: name, object: nil, userInfo: ["percentage": percentage])
}
}
EDIT: So, some time has passed and now there is a really awesome library called Pulley. It does exactly what I wanted it to do, and its a breeze to setup!
Original answer:
Thanks to both Rikh and Tj3n for giving me hints. I managed to do something very basic, it doesn't have nice animations like Uber but it gets the job done.
With the following code, you can swipe any UIViewController. I use a UIPanGestureRecognizer on my image, which will stay on top of the dragged view at all times. Basically, you use that image and it recognizes where it gets dragged, and it sets the view's frame according to the user's input.
First go to your storyboard and add an identifier for the UIViewController that will be dragged.
Then in the MainViewController, use the following code:
class MainViewController: UIViewController {
// This image will be dragged up or down.
#IBOutlet var imageView: UIImageView!
// Gesture recognizer, will be added to image below.
var swipedOnImage = UIPanGestureRecognizer()
// This is the view controller that will be dragged with the image. In my case it's a UITableViewController.
var vc = UIViewController()
override func viewDidLoad() {
super.viewDidLoad()
// I'm using a storyboard.
let sb = UIStoryboard(name: "Main", bundle: nil)
// I have identified the view inside my storyboard.
vc = sb.instantiateViewController(withIdentifier: "TableVC")
// These values can be played around with, depending on how much you want the view to show up when it starts.
vc.view.frame = CGRect(x: 0, y: self.view.frame.height, width: self.view.frame.width, height: -300)
self.addChildViewController(vc)
self.view.addSubview(vc.view)
vc.didMove(toParentViewController: self)
swipedOnImage = UIPanGestureRecognizer(target: self, action: #selector(self.swipedOnViewAction))
imageView.addGestureRecognizer(swipedOnImage)
imageView.isUserInteractionEnabled = true
}
// This function handles resizing of the tableview.
func swipedOnViewAction() {
let yLocationTouched = swipedOnImage.location(in: self.view).y
imageView.frame.origin.y = yLocationTouched
// These values can be played around with if required.
vc.view.frame = CGRect(x: 0, y: yLocationTouched, width: UIScreen.main.bounds.width, height: (UIScreen.main.bounds.height) - (yLocationTouched))
vc.view.frame.origin.y = yLocationTouched + 50
}
Final Product
Now, It is possible that my answer might not be the most efficient way of going at this, but I am new to iOS so this is the best I could come up with for the time being.
You can embed that table view inside a custom scroll view that will only handle touch when touch that table view part (override hittest), then drag it up (disable tableview scroll), till the upper part then disable scroll view and enable tableview scroll again
Or, you can just add the swipe gesture into your tableview and change it's frame along and disable swipe when it reach the top
Experiment with those and eventually you will achieve the effect you wanted
As Tj3n pointed out, you could use a UISwipeGesture to display the UITableView. So using constraints (instead of frames) heres how you could go about doing that:
Go to your UIViewController inside your Story board on which you wish to display the UITableView. Drag and drop the UITableView and add a leading, trailing and height to the UITableView. Now add a vertical constraint between the UIViewController and UITableView so that the UITableView appears below the UIViewController(Play around with this vertical value until you can display the top part of the UITableView to suit your need). Create outlets for the vertical spacing constraint and height constraint (in case you need to set a specific height that you can figure out at run time). On the swipe up just animatedly set the vertical constraint to be equal to the negative value of the height sort of like:
topSpaceToViewControllerConstraint.constant = -mainTableViewHeightConstraint.constant
UIView.animate(withDuration: 0.3) {
view.layoutIfNeeded()
};
Alternatively
If you want to be able to bring the UITableView up depending on the pan amount (i.e depending on how much the user has moved across the screen or how fast) you should use a UIPanGestureRecognizer instead and try and set frames instead of autoLayout for the UITableView (as I'm not a big fan of calling view.layoutIfNeeded repeatedly. I read somewhere that it is an expensive operation, would appreciate it if someone would confirm or correct this).
func handlePan(sender: UIPanGestureRecognizer) {
if sender.state == .Changed {
//update y origin value here based on the pan amount
}
}
Alternatively using UITableViewController
Doing what you wish to perform is also possible using a UITableViewController if you wish to but it involves a lot of faking and effort by creating a custom UINavigationControllerDelegate mainly to create a custom animation that will use UIPercentDrivenInteractiveTransition to pull the new UITableViewController up using a UIPanGestureRecognizer if you want it depending on the pan amount. Otherwise you can simply add a UISwipeGestureRecognizer to present the UITableViewController but you will still have to again create a custom animation to "fake" the effect you want.
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.