How to access scrollbar position in swift - ios

I'm trying to access the position of a scrollbar in scrollview so that I can attach some UI elements to it. The only method I've found to access this is scrollViewDidScroll(_ scrollView:).
This gives me the scrollView.contentOffset.y property, but I can't seem to use it during runtime to attach my UI to it. I think this gives me the entire scrollview length though, and not the position of the scrollbar.
I'm basically trying to do exactly what's circled in red in this screenshot. (KakaoTalk message)

The simplest way I've found to get the relative position of the scrollBar position is to find it's current percentage in the scrollView, and use that value to find the percent the scrollBar is at within the Bounds.
ScrollView position: (scrollView.contentOffset.y / scrollView.contentSize.height)
Which is basically Current Scrolling Position / Total Height of ScrollView
Let's call it scrollPercent.
let scrollPercent = (scrollView.contentOffset.y / scrollView.contentSize.height)
That will give you a % value between 0 and 1 (0% and 100%).
You can take the the scrollPercent and multiply it by the Parent View's max height view.bounds.size.height, to get the approximate Y value of the scrollbar within the view.
let scrollBarPosition = scrollPercent * (view.bounds.size.height)
This can be used as the Y value for your UI element.
When you are doing this, be sure to check that scrollView.contentSize.height is > 0, because it starts as zero, and if you try to divide by that, the number will reach infinite and your app will crash.
The final solution looks like this:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let scrollBarPosition: CGFloat = (scrollView.contentOffset.y / scrollView.contentSize.height) * (view.bounds.size.height)
if scrollView.contentSize.height > 0 {
dateScrollbar.snp.remakeConstraints { (make) in
make.centerY.equalTo(scrollBarPosition)
make.trailing.equalToSuperview().inset(110)
}
}
}
Note: I am using an autolayout library called SnapKit for setting autolayout constraints.
EDIT: The above solution works, but it will also extend your new UI off-screen at the top and bottom of your scrollView.
To fix this: Add a subview to the parentView, and make the subView's height the denominator of the scrollBarPosition. This will act like a "track" for any UI like the picture to slide against, and stay within its bounds.
let scrollBarTrack = UIView()
view.addSubView(scrollBarTrack)
//Put it wherever you want, with the height being equal to whatever you want, and the width can be something like 1.
and then update your scrollBarPosition
let scrollBarPosition = scrollPercent * (scrollBarTrack.bounds.size.height)
It should stay within whatever those bounds are :)

Related

swift infinite scroll up collectionView

Almost everyone can find examples with infinite collectionView load more data when collectionView scrolls to bottom, but maybe anybody known how infinite scroll collectionView from bottom to top?
Like a chat history load more when user scrolls up.
For example.
I have a list with 100000 items and with one of conditions. The app should show to a user items from 9500 to 9600 and when user scrolls up, the app should add to the collectionView items from 9400 to 9500 at the begin of collectionView, and when user scrolls down - the app should add to the collectionView to the end of collectionView and etc.
I've googled and have tried reverse logic for bottom infinite collectionView, but it unsuccessful.
CollectionView just scrolled up to the first item on the list.
Any ideas or tips?
The result you are describing is expected. Since you scrolled to top the content offset is at zero. And when you added new items the old ones were pushed to the bottom. It will not happen the other way around; when you scroll to bottom and add items beneath them the scroll position stays the same.
To be honest it is all about perspective. The position actually stays the same in both cases (or none) but depends on how you look at it. When you append items at bottom the position should stay the same looking from top (which is a default scenario). But when you add items at top the position stays the same from bottom.
So the solution in your case is that when you add items at the bottom you should change scroll from top:
let distanceFromTop = scrollView.contentOffset.y - 0.0;
// Reload data here
scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: 0.0 + distanceFromTop)
which basically means "do nothing at all".
But when you are appending items at the top you need to compute it from bottom:
let distanceFromBottom = scrollView.contentSize.height - scrollView.contentOffset.y;
// Reload data here
scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: scrollView.contentSize.height - distanceFromBottom)
You should note that you can do this sideways as well. Appending items on the right side would again be transparent. But adding them on the left should be like:
let distanceFromRight = scrollView.contentSize.width - scrollView.contentOffset.x;
// Reload data here
scrollView.contentOffset = CGPoint(x: scrollView.contentSize.width - distanceFromRight, y: scrollView.contentOffset.y)
I am using scrollView as this happens the same to every subclass of it. The same logic can be applied to table view or collection view.
Another note here is that estimated row heights or similar functionalities may cause anomalies when not done correctly. In such cases you may need to compute the offset a bit more smartly.

Prevent UITextView from offsetting its text container

I am tying to modify the height of a UITextView dynamically (up to a max height) while the user enters text. I am experiencing a very strange behavior when there are an even number of lines in the text view.
I am using autolayout and the text view has a height constraint. I respond to calls to the text view's delegate (textViewDidChange(_:)), where I calculate and adjust the height constraint based on the contentSize.
Here is the code:
func textViewDidChange(_ textView: UITextView) {
let newHeight = textView.contentSize.height
let newConstraintConst = max(MinTextViewHeight, min(MaxTextViewHeight, newHeight))
self.textViewHeightConstraint.constant = newConstraintConst
}
This works well, it resizes the frame up to MaxTextViewHeight and then the text view can scroll. However, when there are an even number of lines in the text view, the text view adds a kind of offset to the bottom of its NSTextContainer, causing the top line to be cut off:
However, when there are odd lines the NSTextContainer is no longer offset:
At first I thought it was somehow being controlled by the text view's textContainerInset but that is only used to pad the space inside the NSTextContainer, as setting it to .zero removes the space inside but does not affect the offset (and incidentally makes it even worse, as the top line almost completely disappears):
I have looked through the UITextView class reference and I don't see any property that would let me manipulate or even get the value of this offset.
As a workaround I am increasing the text container's top inset and removing the bottom inset:
textView.textContainerInset = UIEdgeInsetsMake(10, 0, 0, 0)
This works so far, but I arrived at a value of 10 by trial-and-error, and so far I've only tested it on a single device.
I am not interested in more hacky workarounds that require fragile, fixed values; I am trying to understand how this offset is being set and a proper way to fix it. I'm hoping that someone can provide some insight, thanks!
Just a speculation, but I think the problem is that the text view assumes that the height of itself does not change while calling textViewDidChange, so it scrolls when it thinks it has to, regardless of you changing its frame.
Not sure if you think my solution is too hacky, but this will stop it from scrolling when you don't want it. I simply pin the content offset to the top as long as the wanted content size is smaller than your max size.
Just add this:
func scrollViewDidScroll(scrollView: UIScrollView) {
if textView.contentSize.height <= MaxTextViewHeight && textView.contentOffset.y > 0.0 {
textView.contentOffset.y = 0.0;
}
}

How to animate centered square to the top

I have a UIView in a portrait-only app.
The view is centered vertically and horizontally with AutoLayout ("manually" using storyboards).
The width equals the (main)view.width * 0.9
The height is the same size of the width (it is a square).
I want to tap a button inside this UIView and animate it only vertically until it reaches the top border of the screen (eg. height*0.9, 10 pts, whatever is possible).
When I click again, I want to reposition back the view to its original position (centered as it was when I first tapped).
During the transition the square should not be tappable.
After reading many posts I could not understand what's the best way to do this (I red mainly developers saying old techniques using centerX should be avoided and lamentations about some versions of the SO behaving in strange ways).
I suppose I should find a way to get the current "position" of the constraints and to assign a constraint the "final" position, but I was not able to do it.
Any help is appreciated
You are all going the wrong way about this.
Add one constraint that pins the view to the top, and add one constraint that pins the view to centerY. It will complain, so pick one and disable it (I think the property in Interface Builder is called Installed).
If the initial state is the view in the center, disable the constraint that pins it to the top, and viceversa.
Now write IBOutlets for both constraints in your controller and connect them to those constraints. Make sure the declaration of that variable is not weak, otherwise the variable will become nil when the constraint is disabled.
Whenever you want to toggle your animation you can enable one constraint and disable the other.
#IBOutlet var topConstraint: NSLayoutConstraint!
#IBOutlet var centerConstraint: NSLayoutConstraint!
func toggleState(moveToTop: Bool) {
UIView.animateWithDuration(0.25) {
self.topConstraint.isActive = moveToTop
self.centerConstraint.isActive = !moveToTop
self.view.layoutIfNeeded()
}
}
While you can use Autolayout to animate - to take the constraint constraining the centerY and set its constant to a value that would move to the top (e.g., constant = -(UIScreen.main.bounds.height / 2)), I would recommend using view's transform property.
So to move the view to the top you can use:
let topMargin = CGFloat(20)
let viewHalfHeight = self.view.bounds.height / 2
let boxHalfHeight = self.box.bounds.height / 2
UIView.animate(withDuration: 0.2) {
box.transform = CGAffineTransform.identity
.translatedBy(x: 0, y: -(viewHalfHeight - (boxHalfHeight + topMargin)))
}
You are moving box.center related to the view.center - so if you want to move the box to the top, you have to move its center by half a view's height (because the view's centerY is exactly height / 2 far from the view's top). That is not enough though, because then only a bottom half of the box is visible (now the box.centerY == view.top). Therefore you have to move it back by the box.bounds.height / 2 (in my code boxHalfHeight) - to make the top half visible. And to that boxHalfHeight you add topMargin so that there is some margin to the top.
Then, to move the box back to original position:
UIView.animate(withDuration: 0.2) {
box.transform = CGAffineTransform.identity
}
EDIT
If you really want to go with autolayout, you have to have a reference to the centerY constraint, so for example if it is created this way:
let boxCenterYConstraint = self.box.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)
boxCenterYConstraint.isActive = true
Then you can try this:
// calculating the translation is the same
let topMargin = CGFloat(20)
let viewHalfHeight = self.view.bounds.height / 2
let boxHalfHeight = self.box.bounds.height / 2
let diff = -(viewHalfHeight - (boxHalfHeight + topMargin))
boxCenterYConstraint.constant = diff
self.view.setNeedsLayout()
UIView.animate(withDuration: 0.2) {
self.view.layoutIfNeeded()
}
And animation back:
boxCenterYConstraint.constant = 0
self.view.setNeedsLayout()
UIView.animate(withDuration: 0.2) {
self.view.layoutIfNeeded()
}

Changing view's alpha based on UIScrollView's contentOffset.x when scrolling multiple screens

I have a horizontal UIScrollView that has a width of 960.
The UIScrollView contains 3 UIView's, each one set to a background color.
The first one is pink, the second blue, and the third is green.
What I need to do is change the alpha of the colors(views) based on the user scrolling.
So if the user is on the first screen (pink) and starts to scroll to the second page (blue), then the pink should start to fade and the blue will become more visible, and once the user fully swipes to that 2nd page it would be all blue.
Here is how I do this when using only one UIView:
func scrollViewDidScroll(scrollView: UIScrollView) {
// Offset Percentage
var percentageHorizontalOffset = scrollView.contentOffset.x / 320;
// Reduce alpha
pinkView.alpha = percentageHorizontalOffset
}
This is easy because the percentage goes from 0.0 to 1.0
However, I need to modify the code to support 3 screens. I tried this earlier by replacing 320 with 960 in the method above but this causes a couple problems.
The first is that you are no longer getting a percentage in the range of 0.0 to 1.0, once you scroll on the second page it will be a range of 1.0 to 2.0 which won't help me properly modify the alpha.
The second is that the alpha change doesn't feel very smooth, especially when the UIScrollView is swiped quickly. I setup a mess of if statements earlier trying to get this to work but nothing worked.
How can I properly fade the alpha of all 3 UIView's based on the contentOffset.x when my UIScrollView's content size is bigger than just one screen?
Simplest way would be to treat the 3 colored views as an array.
// Put all your views in an array
#property NSArray *coloredViews;
// Later where you set things up, allocate the array.
coloredViews=#[pinkView,blueView,greenView];
Then use the following algorithm to set the alpha.
This assumes only 2 of N views on screen at a time. One on left and one on the right.
The alpha of the left is based on the amount of it off screen.
The alpha of the right is based on the amount of it on screen.
So as left goes off it goes to 0 causing right to go to 1 and vice versa.
This makes your scroll method:
func scrollViewDidScroll(scrollView: UIScrollView) {
// Work out which view is showing on the left. 0 to N-1
// Based on each being 320 wide.
CGFloat offset=scrollView.contentOffset.x;
NSUInteger leftHandSideViewIndex=(NSUInteger)offset.x/320;
// Left hand side alpha is 1 minus percentage (as a fraction) off screen
self.coloredViews[leftHandSide].alpha =
1.0 - (offset - (320.0 * leftHandSideViewIndex))/320.0;
// Right hand side alpha is 1 - percentage (as a fraction) on screen
// Only do this if there is a right hand side.
if (leftHandSideViewIndex < self.coloredViews.count-1){
self.coloredViews[leftHandSide+1].alpha =
1.0 - ((320.0 * leftHandSizeViewIndex+1)-offset)/320.0;
}
}
Give it a try. Might not be 100% right, but hopefully gives you the idea.You probably want to set a width variable rather than using 320.0.

iCarousel - How to shift the focus to the left side

I need to implement iCarousel in a way so that it looks like this:
I have done all the modifications at my end to make it look like this but now the problem is that iCarousel works with the center image in the focus. Can I make it so that if there are only two images, they don't appear in the center but rather on the left? Think about it like the "left indented" content.
How do I do this?
iCarousal provide a property to sift focus left or right by set the width offset.
For right side use this code for swift 2.3 -
let widthOffset: Float = -120.0
self.customCarousel.viewpointOffset = CGSize(width: CGFloat(widthOffset), height: CGFloat(0))
This should be the property in iCarousel you are looking for viewpointOffset
Example...
// Modifiy widthOffset so it works well in your case
float widthOffset = 100.0;
[_yourCarousel setViewpointOffset:CGSizeMake(widthOffset, 0)];
The viewpointOffset property doesn't really work that way. It will let you align to one edge, but then when you scroll to the end you'll have the same problem.
iCarousel wasn't designed to do this. I suggest you switch to SwipeView instead (http://github.com/nicklockwood/SwipeView) which has the same delegate/datasource interface, but has an alignment property that you can use to set edge alignment, like this:
swipeView.alignment = SwipeViewAlignmentEdge;
Visit https://github.com/nicklockwood/iCarousel/issues/142
it may help.
nicklockwood (author) answer:
You could use the delegate and create a custom transform that offsets all of your views by a negative amount. Something like this:
- (CATransform3D)carousel:(iCarousel *)carousel itemTransformForOffset:(CGFloat)offset baseTransform:(CATransform3D)transform
{
float OFFSET_WIDTH = ...; // set this to about half the width of your view
return CATransform3DMakeTranslation(offset * itemWidth - OFFSET_WIDTH, 0, 0);
}
The only problem would be that when you reach the rightmost end of the
carousel there would be a gap, but you could use placeholder views to
fill the gap up.
I tripped over the same problem. After simple math I've found a more accurate answer. I've put this code in my viewDidAppear() function, because when I put it in viewDidLoad() the view.frame.width and view.frame.height took the original width of my storyboard, which didn't fit to bigger devices.
My use case was the following. My iCarousel items are simple squares. That means, I have for example a height of 3 and a width of 3. But the width of my iCarousel view is 5. In sum I have 5-3=2 space, 1 left and 1 right (because we know it's default is centered). So last step is to divide with 2 to get the width on one site = 1. With this knowledge we just have to set the viewPointOffset to 1 (in this case).
Final formular: let widthOffset = (view.frame.width - view.frame.height) / 2
Example in my code:
let widthOffset: Float = Float((mDetailCarousel.frame.width - mDetailCarousel.frame.height) / 2 )
mDetailCarousel.viewpointOffset = CGSize(width: CGFloat(widthOffset), height: CGFloat(0))

Resources