I'm trying to create a Parallax effect on a UIView inside a UIScrollView.
The effect seems to work, but not so well.
First i add two UIView sub-views to a UIScrollView and set the UIScrollViews contentSize.
The Views sum up and create a contentSize of {320, 1000}.
Then I implemented the following in scrollViewDidScroll:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGFloat offsetY = scrollView.contentOffset.y;
CGFloat percentage = offsetY / scrollView.contentSize.height;
NSLog(#"percent = %f", percentage);
if (offsetY < 0) {
firstView.center = CGPointMake(firstView.center.x, firstView.center.y - percentage * 10);
} else if (offsetY > 0){
firstView.center = CGPointMake(firstView.center.x, firstView.center.y + percentage * 10);
}
}
These lines of code do create a parallax effect, but as the scrolling continues, the view does not return to it's original position if i scroll to the original starting position.
I have tried manipulating the views layers and frame, all with the same results.
Any Help will be much appreciated.
The problem you have is that you are basing your secondary scrolling on a ratio of offset to size, not just on the current offset. So when you increase from an offset of 99 to 100 (out of say 100) your secondary scroll increases by 10, but when you go back down to 99 your secondary scroll only decreases by 9.9, and is thereby no longer in the same spot as it was last time you were at 99. Non-linear scrolling is possible, but not the way you are doing it.
A possible easier way to deal with this is to create a second scrollview and place it below your actual scrollview. Make it non intractable (setUserInteractionEnabled:false) and modify it's contentOffset during the main scrolling delegate instead of trying to move a UIImageView manually.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
[scrollView2 setContentOffset:CGPointMake(scrollView.contentOffset.x,scrollView.contentOffset.y * someScalingFactor) animated:NO];
}
But make sure not to set a delegate for the scrollView2, otherwise you may get a circular delegate method call that will not end well for you.
Scaling Factor being the key element...
...let me offer a 1:1 calculation:
Assuming 2 UIScrollView, one in the foreground and on in the rear, assuming the foreground controls the rear, and further assuming that a full width in the foreground corresponds to a full width in the background, you then need to apply the fore ratio, not the fore offset.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let foreSpan = foreScrolView.bounds.width - foreScrolView.contentSize.width
let foreRatio = scrollView.contentOffset.x / foreSpan
let rearSpan = rearScrollView.bounds.width - rearScrollView.contentSize.width
rearScrollView.setContentOffset(
CGPoint(x: foreRatio * rearSpan, y: 0),
animated: false)
}
Final effect
The two scrollers, fore and rear, each contain a UIImageView displayed at its full width:
let foreImg = UIImageView.init(image: UIImage(named: "fore"))
foreImg.frame = CGRect(x: 0, y: 0,
width: foreImg.frame.width,
height: foreScrolView.bounds.height)
foreScrolView.contentSize = foreImg.frame.size
foreScrolView.addSubview(foreImg)
let rearImg = UIImageView.init(image: UIImage(named: "rear"))
rearImg.frame = CGRect(x: 0, y: 0,
width: rearImg.frame.width,
height: rearScrollView.bounds.height)
rearScrollView.contentSize = rearImg.frame.size
rearScrollView.addSubview(rearImg)
This will scroll both images at a different speed, covering each image in full from edge to edge.
► Find this solution on GitHub and additional details on Swift Recipes.
Related
Im having an odd problem with a scrollview - I have tried doing this in a xib file and programmatically. Basically I 3 textfields that I want to be able to scroll to with my scrollview filling the whole screen.
To do this, I first created a scrollview on my xib and constrained it so it always filled the screen. I then expanded the contentsize to 3 times the size of the view so it should be able to scroll
scrollView.delegate = self
scrollView.frame = CGRectMake(0, 0, screenSize.width, screenSize.height)
scrollView.backgroundColor = UIColor.redColor()
self.scrollView.contentSize = CGSizeMake(self.view.frame.width*3, screenSize.height * (50/568))
scrollView.center = CGPointMake(self.view.frame.width * -1.5, self.view.frame.height * 0.5)
scrollView.addSubview(containerView)
view.addSubview(scrollView)
self.scrollView.delaysContentTouches = false
I have 3 textfields named search 1, 2, 3 that I then position within this scrollview like this so they are at different lengths off the screen to the LEFT -
search3.center = CGPointMake(screenSize.width * 0.5, screenSize.height * 0.45)
search2.center = CGPointMake(screenSize.width * -0.5, screenSize.height * 0.45)
search1.center = CGPointMake(screenSize.width * -1.5, screenSize.height * 0.45)
This has worked, meaning I have the first search field 2 views to the left, the other right next to it, etc
Problem is I can only scroll to the RIGHT because I can't seem to move the center of my scrollview off the left so its length expands 3 views to the right. I have tried with this -
scrollView.center = CGPointMake(self.view.frame.width * -1.5, self.view.frame.height * 0.5)
And various other methods creating the view programmatically but it sint working. This is what I get with all the things I need to scroll to far to the right -
Ive never had this. How can I make it expand the views width to the left?
Have you tried setting the scrollView's contentOffset? According to the docs, contentOffset represent:
The point at which the origin of the content view is offset from the origin of the scroll view.
If you want your scrollview to start in the middle (can scroll to the left and right) this might work for you with assumption contentView is 3 times the size of the scrollView
self.scrollView.contentOffset = CGPointMake(self.scrollView.contentSize.width/3 , 0.0f);
I've been working at this for a good 48 hours now and can't seem to solve it on my own. What I'm trying to achieve is when a UITableViewCell is scrolled from the bottom to the top of the screen it starts small, magnifies as it reaches the center portion of the screen, and then begins to reduce in size again as it scrolls up and off of the screen (and vice versa, top to bottom). I can kind of get this to work, but it seems as though the contentView is resizing, the actual cell height (of the UITableView) is not.
What I want:
Scrolling up or down smoothly adjusts the size of all the uitableviewcells based on its y position on the screen.
What appears to be happening (though I'm having trouble confirming):
Again, it's as if the cell height is not being adjusted, but the height of the contentView is.
I can achieve this by overriding the layoutVisibleCells method of the UITableView:
func layoutVisibleCells() {
let indexpaths = indexPathsForVisibleRows!
let totalVisibleCells = indexpaths.count - 1
if totalVisibleCells <= 0 { return }
for index in 0...totalVisibleCells {
let indexPath = indexpaths[index]
if let cell = cellForRowAtIndexPath(indexPath) {
var frame = cell.frame
if let superView = superview {
let point = convertPoint(frame.origin, toView:superView)
let pointScale = point.y / CGFloat(superView.bounds.size.height)
var height = frame.size.height + point.x;
if height < 150.0 {
height = 150.0;
}
if height > 200.0 {
height = 200.0;
}
frame.size.height = height;
NSLog("index %li: x: %f", index,frame.origin.x);
NSLog("point at index %li: %f, %f - percentage: %f", index,point.x,point.y, pointScale);
}
cell.frame = frame
}
}
}
I took the idea for making the adjustment in this method from COBezierTableView, which is written in Swift. The rest of my project is in Obj-C, so deciphering things was a bit challenging, as I'm still learning my way there.
Any insight on my current attempt, or suggestions for how to achieve this in another, completely different way, are totally welcome. Thanks in advance.
UPDATE
So I've actually found that the opposite is happening of what I suspected. The UITableViewCell frame IS being resized correctly. It's the cell's contentView that is not resizing fully. It does resize partially, but for some reason it does not resize to match the cell's frame. Not sure why ...
I made a button that adds some text to a textView and I want it to automatically scroll to the bottom as it is pressed so that user would be able to see the new text added.
I can't use this solution in Swift because I don't know Objective-C.
Does anyone know how can I scroll to the bottom of a textView in Swift? Thanks.
Tried both content offset and scrolltoview solutions, get mixed results and choppy scrolling; having looked around the below seemed to work and produce consistent scrolling to the bottom when needed.
In viewdidload:
self.storyTextView.layoutManager.allowsNonContiguousLayout = false
Then when needed:
let stringLength:Int = self.storyTextView.text.characters.count
self.storyTextView.scrollRangeToVisible(NSMakeRange(stringLength-1, 0))
Swift 4
let bottom = NSMakeRange(textLog.text.count - 1, 1)
textLog.scrollRangeToVisible(bottom)
Swift 3
let bottom = NSMakeRange(textLog.text.characters.count - 1, 1)
textLog.scrollRangeToVisible(bottom)
Update: thanks #AntoineRucquoy for Swift 4 reminder!
Simply, where myTextView is the UITextView in question:
let bottom = myTextView.contentSize.height
myTextView.setContentOffset(CGPoint(x: 0, y: bottom), animated: true) // Scrolls to end
So if you click the link you posted the accepted answer shows this objective-C code:
-(void)scrollTextViewToBottom:(UITextView *)textView
{
if(textView.text.length > 0 )
{
NSRange bottom = NSMakeRange(textView.text.length -1, 1);
[textView scrollRangeToVisible:bottom];
}
}
So your challenge is to convert that code to Swift.
Break it into pieces and tackle them one at a time. First, the method definition itself.
The method is called scrollTextViewToBottom. It takes a UITextView as a parameter, and does not return a result. How would you write that method definition in Swift?
Next look that the body of the method. The if statement should be exactly the same in Swift.
The creation of an NSRange is all but identical. You just need to change it a little bit:
let bottom = NSMakeRange(textView.text.length -1, 1)
The part that's probably the hardest for somebody who doesn't know Objective-C is the method call. It's sending the message scrollRangeToVisible to the object textView. The parameter passed is bottom. See if you can rewrite that line in Swift. Then put the whole thing together.
I use the following in an app that scrolls to the bottom automatically when text is added:
First when initializing your textView, do the following:
textView.layoutManager.allowsNonContiguousLayout = false
textView.addObserver(self, forKeyPath: "contentSize", options: .new, context: nil)
Then add the following observer method:
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
var bottom = textView.contentSize.height - textView.frame.size.height
if bottom < 0 {
bottom = 0
}
if textView.contentOffset.y != bottom {
textView.setContentOffset(CGPoint(x: 0, y: bottom), animated: true)
}
}
setting allowsNonContiguousLayout to false fixed contentSize problems for me.
Adding the contentSize observer will observe for any new changes in the contentSize of the textView and call the -observeValue(forKeyPath...) function when changes are made.
In the -observeValue(...) function, we first get the bottom (y contentOffset when fully scrolled to the bottom). We then check if that value is negative, meaning that the contentSize height is smaller than the textView frame height and you can't really do any scrolling. If you try to programmatically scroll with that negative value, it will cause that infamous jitter that many people know and love. So to avoid this jitter we simply set the value to what it already should be, 0 or you can also just return.
Then we just test to see if the contentOffset doesn't already equal the bottom value, we give it that new value. This avoids setting the contentOffset when it doesn't need to be set.
Language:Swift
Follow steps as below:
//Declare
#IBOutlet weak var trTextDataRead: UITextView!
//Cunstom method
func insertTextView(text: String){
//Insert text
trTextDataRead.text.append(text)
//Scroll to the end
let btm = NSMakeRange(trTextDataRead.text.lengthOfBytes(using: String.Encoding.utf8), 0)
trTextDataRead.scrollRangeToVisible(btm)
}
If you're dealing with the UITextView's attributedText property:
in viewDidLoad()
self.storyTextView.layoutManager.allowsNonContiguousLayout = false
in your scrolling method
let stringLength:Int = self.storyTextView.attributedText.string.characters.count
self.storyTextView.scrollRangeToVisible(NSMakeRange(stringLength-1, 0))
Swift 4
private func textViewScrollToBottom() {
let bottomRange = NSMakeRange(self.myTextView.text.count - 1, 1)
self.myTextView.scrollRangeToVisible(bottomRange)
}
UITextView has a property contentOffsent. You can either set textView.contentOffset or textView.setContentOffset(offset, animated: true)
For example if the contentSize of your text view is (100, 500) but the height of the text view is only 100, then to scroll to the bottom, set the contentOffset property to (0, 400) (this is for a vertical text view). More generically the formula for scrolling to the bottom is textView.contentSize.height-textView.height. Every time your button is pressed, set the offset.
I would also really recommend reading the documentation and trying to figure it out. Swift and iOS is quite well documented and a question like this is easily searchable via Google.
Edit: This works because UITextView inherits from UIScrollView.
Sidenote: I wrote a UITextView subclass where you can set the vertical text alignment so if you set the text alignment to .Bottom, the text will align with the bottom of the view.
class TextView: UITextView {
enum VerticalAlignment: Int {
case Top = 0, Middle, Bottom
}
var verticalAlignment: VerticalAlignment = .Middle
//override contentSize property and observe using didSet
override var contentSize: CGSize {
didSet {
let textView = self
let height = textView.bounds.size.height
let contentHeight:CGFloat = contentSize.height
var topCorrect: CGFloat = 0.0
switch(self.verticalAlignment){
case .Top:
textView.contentOffset = CGPointZero //set content offset to top
case .Middle:
topCorrect = (height - contentHeight * textView.zoomScale)/2.0
topCorrect = topCorrect < 0 ? 0 : topCorrect
textView.contentOffset = CGPoint(x: 0, y: -topCorrect)
case .Bottom:
topCorrect = textView.bounds.size.height - contentHeight
topCorrect = topCorrect < 0 ? 0 : topCorrect
textView.contentOffset = CGPoint(x: 0, y: -topCorrect)
}
if contentHeight >= height { //if the contentSize is greater than the height
topCorrect = contentHeight - height //set the contentOffset to be the
topCorrect = topCorrect < 0 ? 0 : topCorrect //contentHeight - height of textView
textView.contentOffset = CGPoint(x: 0, y: topCorrect)
}
}
}
// MARK: - UIView
override func layoutSubviews() {
super.layoutSubviews()
let size = self.contentSize //forces didSet to be called
self.contentSize = size
}
}
In the above example (pulled directly from my subclass), you'll notice I make extensive use of the contentOffset property. I do some calculations to figure out where the offset should be based on the vertical alignment property and then set the content offset property according (which is how you programmatically scroll with a scroll view)
A lot of people are explaining how to scroll to the bottom, but one thing to note is that this won't work if you place it in viewDidLoad. For example: I needed to use this to scroll a log to the bottom when the page loaded. In order to do this, I had to implement the following code
- (void) viewDidLoad {
[super viewDidLoad];
[_logTextView setText:[Logger loadLogText]];
}
- (void) viewDidLayoutSubviews {
[_logTextView
setContentOffset:CGPointMake(
0.0,
_logTextView.contentSize.height
- _logTextView.frame.size.height
)
animated:NO
];
}
The actual scrolling of the UITextView cannot be done in viewDidLoad.
In my viewDidLoad implementation I set the text for the text box.
In my viewDidLayoutSubviews implementation I set the content offset for the UITextView by generating a CGPoint using the height of the text views content minus the height of the text view itself. This way, when it scrolls to the bottom, the bottom of the text is not at the top of the box and instead, the bottom of the text is at the bottom of the box.
Swift 5 - with extension to UITextView + avoid slow scroll in large texts (this issue killed my main Thread)
extension UITextView {
func scrollToBottom() {
// IMPORTANT - only use (text as NSString) to get the length, since text.length is O(N) and it will kill the main thread.
let length = (text as NSString).length
if length > 1 {
scrollRangeToVisible(NSMakeRange(length - 1, 1))
}
}
}
I have a question about UIScrollView.I want to set the content in the middle of UIScrollView so, user can scroll horizontally left & right easily.ContentSize of the UIScrollView should be 3 times of the content.Below is the drawing :-
|---------------------------------ContentSize of UIScrollView-------------------------------------|
|-----------content----------|
How to set it?
Thanks!
I understand you want something like that :
|----scrollVIew.frame.size.width---|
|----------------scrollVIew.contentSize.width------------------|
|-content.frame.size.width-|
in which scrollVIew.contentSize.width = 3 x content.frame.size.width.
To perform something like that, first, set your scrollview.frame:
scrollView.frame = CGRectMake(0, 0, width, height);
Then create and add content view into your scrollView and set its frame:
CGFloat contentViewWidth = 200.f; // the width of your contentView
contentView.frame = CGRectMake(contentViewWidth, 0, contentViewWidth, height);
Finally, set the content size of your scrollview:
scrollView.contentSize = CGSizeMake(contentViewWidth * 3, height);
And if you want to center the content, you can manually scroll to the middle of your scrollVIew:
[scrollView setContentOffset:CGPointMake(scrollView.contentSize.width * 0.5f - width * 0.5f, 0)];
You should use setContentOffset method. There's another useful information on the reference page.
Im attempting to replicate the resizing behaviour of a screen seen in the iOS app Rdio. The screen in question provides an overview of a selected album and contains a UIView on the top half and a UITableView on the bottom half. When the tableView is scrolled, it first resizes upwards to fill the screen, then begins to scroll through its content normally once the maximum height is reached.
After some searching I found this question: Dragging UITableView which is basically asking for the same thing, however its accepted method is the same as my initial thoughts & trial, which was to use a UIPanGestureRecognizer and resize the tableviews height according to the translation of the pan.
This does not provide the behaviour i'm looking for. Using this method only allows you to statically drag the tableviews height up or down and it has the added issue of the panGesture overriding that of the tableViews which then prevents scrolling through the content.
The resizing behaviour of the Rdio app functions and feels exactly like a UIScrollView, it has inertia. You can drag it all the way, flick it up or down, and it smoothly resizes. When the tableView has reached its full-size or original half-size, the remaining inertia is seemingly passed on the tableview causing the cells to scroll as they normally would for that amount. I know they must be manipulating UIScrollViews, I just can't figure out how.
As a final note, eventually I will be using AutoLayout on this screen so i'm wondering how that will potentially hinder or help this situation as well.
Update
This approach has gotten me closest to the behaviour i'm looking for so far.
Flicking the tableView upwards behaves exactly like I wanted it to (resize with inertia & continue scrolling when max height is reached), although with less sensitivity than i'd like. Flicking downwards however, provides no inertia and instantly stops.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGRect scrollViewFrame = scrollView.frame;
CGFloat scrollViewYOffset = scrollView.contentOffset.y;
if (scrollViewFrame.origin.y <= kTableViewMaxYOrigin || scrollViewFrame.origin.y <= _originalTableViewFrame.origin.y)
{
scrollViewFrame.origin.y -= scrollViewYOffset;
if(scrollViewFrame.origin.y >= kTableViewMaxYOrigin && scrollViewFrame.origin.y <= _originalTableViewFrame.origin.y)
{
scrollViewFrame.size.height += scrollViewYOffset;
scrollView.frame = scrollViewFrame;
scrollView.contentOffset = CGPointZero;
}
}
}
I made a version that uses autolayout instead.
It took me a while of trial and error to get this one right!
Please use the comments and ask me if the answer is unclear.
In viewDidLoad save the initial height of your layout constraint determining the lowest down you want the scrollview to be.
- (void)viewDidLoad
{
...
_initialTopLayoutConstraintHeight = self.topLayoutConstraint.constant;
...
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
BOOL leaveScrollAlone = self.topLayoutConstraint.constant == _initialTopLayoutConstraintHeight && scrollView.contentOffset.y <= 0;
if (leaveScrollAlone)
{
// This allows for bounce when swiping your finger downwards and reaching the stopping point
return;
}
// Do some capping of that layout constraint to keep it from going past the range you want it to be.
// In this case, I use self.topLayoutGuide.length so that my UICollectionView scales all the way until
// it hits the bottom of the navigation bar
CGFloat topLayoutConstraintLength = _initialTopLayoutConstraintHeight - scrollView.contentInset.top;
topLayoutConstraintLength = MAX(topLayoutConstraintLength, self.topLayoutGuide.length);
topLayoutConstraintLength = MIN(topLayoutConstraintLength, _initialTopLayoutConstraintHeight);
self.topLayoutConstraint.constant = topLayoutConstraintLength;
// Keep content seemingly still while the UICollectionView resizes
if (topLayoutConstraintLength > self.topLayoutGuide.length)
{
scrollView.contentInset = UIEdgeInsetsMake(scrollView.contentInset.top + scrollView.contentOffset.y,
scrollView.contentInset.left,
scrollView.contentInset.bottom,
scrollView.contentInset.right);
scrollView.contentOffset = CGPointZero;
}
// This helps get rid of the extraneous contentInset.top we accumulated for keeping
// the content static while the UICollectionView resizes
if (scrollView.contentOffset.y < 0)
{
self.topLayoutConstraint.constant -= scrollView.contentOffset.y;
scrollView.contentInset = UIEdgeInsetsMake(scrollView.contentInset.top + scrollView.contentOffset.y,
scrollView.contentInset.left,
scrollView.contentInset.bottom,
scrollView.contentInset.right);
}
// Prevents strange jittery artifacts
[self.view layoutIfNeeded];
}
It turns out the key component to getting the smooth inertial resizing in both directions was to update the scrollViews contentInset.top by its contentOffset.y.
I believe this makes sense in retrospect as if the content within is already at the top it cannot scroll anymore, hence the sudden stop rather than smooth scroll. At least thats my understanding.
Another key point was to make sure the cells only started scrolling once maximum or original height was achieved. This was done simply by setting the scrollViews contentOffset to CGPointZero each time the view resized until maximum or original height was reached.
Here is the - (void)scrollViewDidScroll:(UIScrollView *)scrollView method demonstrating how to achieve this effect.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGRect scrollViewFrame = scrollView.frame;
CGFloat scrollViewTopContentInset = scrollView.contentInset.top;
CGFloat scrollViewYOffset = scrollView.contentOffset.y;
if (scrollViewFrame.origin.y <= kTableViewMaxYOrigin || scrollViewFrame.origin.y <= _originalTableViewFrame.origin.y)
{
scrollViewFrame.origin.y -= scrollViewYOffset;
if(scrollViewFrame.origin.y >= kTableViewMaxYOrigin && scrollViewFrame.origin.y <= _originalTableViewFrame.origin.y)
{
scrollViewFrame.size.height += scrollViewYOffset;
scrollViewTopContentInset += scrollViewYOffset;
scrollView.frame = scrollViewFrame;
scrollView.contentInset = UIEdgeInsetsMake(scrollViewTopContentInset, 0, 0, 0);
scrollView.contentOffset = CGPointZero;
}
}
}
I haven't seen the app in question, but from your description... in your tableView's delegate method -scrollViewDidScroll:, set tableView.frame.origin.y to albumView.frame.height - tableView.contentOffset.y and change its height accordingly.
(If you're using autolayout, you'll have to change the constraints pertaining to the tableView's frame rather than the frame itself.)