iOS Parallax Scrolling effect (like in Yahoo News Digest app) - ios

I would like to know how to implement parallax scrolling similar to Yahoo News Digest app. In which when user scroll horizontally background image scrolls in a different speed with the paging is enabled.
May be they do it with a ScrollView with a background view. Not exactly sure. Hint to implement such scrolling would be great. I have checked similar questions but couldn't find the answer I was looking for.

I've done this before with 2 scrollviews.
You have the main detail scroll view and then the parallax scroll view behind it (or wherever you want it).
Then you become the delegate of the detail scrollview.
In the method scrollView:didScroll you can then adjust the scroll of the parallax view.
If you're just doing the x axis then you want something like this...
CGFloat detailMaxOffset = self.detailScrollView.contentSize.width - CGRectGetWidth(self.scrollView.frame);
CGFloat percentage = self.detailScrollView.contentOffset.x / maxOffset;
CGFloat parallaxMaxOffset = self.parallaxScrollView.contentSize.width - CGRectGetWidth(self.parallaxScrollView.frame);
[self.parallaxScrollView setContentOffset:CGPointMake(percentage * parallaxOffset, 0);
This will set the scrollviews content offset "percentage" to be the same on each.
To get the parallax effect you just need to make the contentSize of each scrollview different.
If the parallax scroll view has a bigger content size than the detail scroll view it will scroll faster. If it has a smaller content size it will scroll slower.

Here is the answer. I subclass it from uitableview so that data can be reusable and wrap it in a uiview. https://github.com/michaelhenry/MHYahooParallaxView
Thanks,
Kel

100% working and dam easy
Take a view on imageview exactly size of image view.
by default alpha for view set 0.
//MARK: Scroll View Delegate methods
-(void)scrollViewDidScroll:(UIScrollView *)scrollView{
NSLog(#"X: %f Y: %f",scrollView.contentOffset.x,scrollView.contentOffset.y);
CGFloat scrollY = _mainScrollView.contentOffset.y;
CGFloat height = _alphaView.frame.size.height;
CGFloat alphaMonitor = scrollY/height;
_alphaView.alpha = alphaMonitor;
}

Swift 3
Here's how I got a parallax effect to work in Swift 3 for a vertical scroll in a tvOS app.
In ViewDidLoad():
parallaxScrollView.delegate = self
detailScrollView.delegate = self
And following:
override func viewDidLayoutSubviews() {
self.detailScrollView!.contentSize = CGSize(width: 1920, height: 2700)
self.parallaxScrollView?.contentSize = CGSize(width: 1920, height: 3000)
}
//// THE SCROLL VIEW
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// Parallax effect
let detailMaxOffset = self.detailScrollView.contentSize.height - self.detailScrollView.frame.height;
let percentage = self.detailScrollView.contentOffset.y / detailMaxOffset
let parallaxMaxOffset = self.parallaxScrollView.contentSize.height - self.parallaxScrollView.frame.height;
let parallaxOffset = CGPoint(x:0,y:(percentage * parallaxMaxOffset))
self.parallaxScrollView.setContentOffset((parallaxOffset), animated: false)
}

Related

UIScrollView Scrolling Up

I'm trying to get a UIScrollView to scroll upwards instead of downwards.I have everything set up except I don't know what line I should use to accomplish this. What should I do?
Niall
What I think you mean by this is that you want a scrollView that starts at the bottom and your content can be accessed by scrolling up.
In this case you simply need to figure out how large your content is, and set the setContentOffset of your scrollView to the height of your content:
override func viewDidLoad() {
// other code here...
// set the contentSize of the scrollView somewhere in here...
// get the offset based on the size of the content
let bottomOffset = CGPoint(x: 0, y: scrollView.contentSize.height - scrollView.bounds.size.height)
// do not animate because we want to immediately scroll to the bottom
scrollView.setContentOffset(bottomOffset, animated: false)
This code is adapted from this answer.

Set UIScrollview Scroll Direction

I set the content size so vertical scrolling becomes activated but I only want the user to be able to scroll north and not south. Any ideas on how I can accomplish this?
//set high greater than view
myScroll.contentSize = CGSize(width: myView.view.frame.width,height: myView.view.frame.height + 190)
I only want the ability to scroll up and disable the ability to scroll down which is the default direction of the scrollview
You can set the content offset of the scroll view to the bottom. i.e.,
myScroll.contentOffset = CGPoint(x: 0,y: 190) // Here 190 is the same value that represent the height increase done in contentSize.
Make a subclass of UIScrollView. Override layoutSubviews to make sure contentOffset.y only increases, except when it's beyond the end of the content range, so the scroll view can bounce at the bottom.
class MyScrollView: UIScrollView {
var priorOffset = CGPoint.zero
override func layoutSubviews() {
var offset = contentOffset
if offset.y < priorOffset.y {
let yMax = contentSize.height - bounds.height
if offset.y < yMax {
offset.y = priorOffset.y
contentOffset = offset
}
}
priorOffset = offset
super.layoutSubviews()
}
}
Result:
select your Scrollview
select identity inspector
set user define attributes (see image)
in image first 435 vertical scrolling & second 116 is horizontal scroll
Note : set your own scrolling
You can set the direction of UIPanGestureRecognizer which is attached to your UIScrollView.
Check out some popular question which has an up-to-date answer, for example this one.
Or just go with pod 'UIPanGestureRecognizerDirection'

How to make UIScrollView zoom in only one direction when using auto layout

I'm attempting to make a UIScrollView only allow zooming in the horizontal direction. The scroll view is setup with a pure autolayout approach. The usual approach is as suggested in a number of Stack Overflow answers (e.g. this one) and other resources. I have successfully used this in apps before autolayout existed. The approach is as follows: in the scroll view's content view, I override setTransform(), and modify the passed in transform to only scale in the x direction:
override var transform: CGAffineTransform {
get { return super.transform }
set {
var t = newValue
t.d = 1.0
super.transform = t
}
}
I also reset the scroll view's content offset so that it doesn't scroll vertically during the zoom:
func scrollViewDidZoom(scrollView: UIScrollView) {
scrollView.contentSize = CGSize(width: scrollView.contentSize.width, height: scrollView.frame.height)
scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: 0.0)
}
This works very nicely when not using autolayout:
However, when using autolayout, the content offset ends up wrong during zooming. While the content view only scales in the horizontal direction, it moves vertically:
I've put an example project on GitHub (used to make the videos in this question). It contains two storyboards, Main.storyboard, and NoAutolayout.storyboard, which are identical except that Main.storyboard has autolayout turned on while NoAutolayout does not. Switch between them by changing the Main Interface project setting and you can see behavior in both cases.
I'd really rather not switch off autolayout as it solves a number of problems with implementing my UI in a much nicer way than is required with manual layout. Is there a way to keep the vertical content offset correct (that is, zero) during zooming with autolayout?
EDIT: I've added a third storyboard, AutolayoutVariableColumns.storyboard, to the sample project. This adds a text field to change the number of columns in the GridView, which changes its intrinsic content size. This more closely shows the behavior I need in the real app that prompted this question.
Think I figured it out. You need to apply a translation in the transform to offset the centering UIScrollView tries to do while zooming.
Add this to your GridView:
var unzoomedViewHeight: CGFloat?
override func layoutSubviews() {
super.layoutSubviews()
unzoomedViewHeight = frame.size.height
}
override var transform: CGAffineTransform {
get { return super.transform }
set {
if let unzoomedViewHeight = unzoomedViewHeight {
var t = newValue
t.d = 1.0
t.ty = (1.0 - t.a) * unzoomedViewHeight/2
super.transform = t
}
}
}
To compute the transform, we need to know the unzoomed height of the view. Here, I'm just grabbing the frame size during layoutSubviews() and assuming it contains the unzoomed height. There are probably some edge cases where that's not correct; might be a better place in the view update cycle to calculate the height, but this is the basic idea.
Try setting translatesAutoresizingMaskIntoConstraints
func scrollViewDidZoom(scrollView: UIScrollView) {
gridView.translatesAutoresizingMaskIntoConstraints = true
}
In the sample project it works. If this is not sufficient, try creating new constraints programmatically in scrollViewDidEndZooming: might help.
Also, if this does not help, please update the sample project so we can reproduce the problem with variable intrinsicContentSize()
This article by Ole Begemann helped me a lot How I Learned to Stop Worrying and Love Cocoa Auto Layout
WWDC 2015 video
Mysteries of Auto Layout, Part 1
Mysteries of Auto Layout, Part 2
And so luckily, there's a flag for that. It's called translatesAutoResizingMask IntoConstraints [without space].
It's a bit of a mouthful, but it pretty much does what it says. It makes views behave the way that they did under the Legacy Layout system but in an Auto Layout world.
Although #Anna's solution works, UIScrollView provides a way of working with Auto Layout. But because scroll views works a little differently from other views, constraints are interpreted differently too:
Constraints between the edges/margins of scroll view and its contents attaches to the scroll view's content area.
Constraints between the height, width, or centers attach to the scroll view’s frame.
Constraints between scroll view and views outside scroll view works like an ordinary view.
So, when you add a subview to the scroll view pinned to its edges/margins, that subview becomes the scroll view's content area or content view.
Apple suggests the following approach in Working with Scroll Views:
Add the scroll view to the scene.
Draw constraints to define the scroll view’s size and position, as normal.
Add a view to the scroll view. Set the view’s Xcode specific label to Content View.
Pin the content view’s top, bottom, leading, and trailing edges to the scroll view’s corresponding edges. The content view now defines
the scroll view’s content area.
(...)
(...)
Lay out the scroll view’s content inside the content view. Use constraints to position the content inside the content view as normal.
In your case, the GridView must be inside the content view:
You can keep the grid view constraints that you have been using but, now, attached to content view. And for the horizontal-only zooming, keep the code exactly as it is. Your transform overriding handle it very well.
I'm not sure if this satisfies your requirement of 100% autolayout, but here's one solution, where the UIScrollView is set with autolayout and gridView added as a subview to it.
Your ViewController.swift should look like this:
// Add an outlet for the scrollView in interface builder.
#IBOutlet weak var scrollView: UIScrollView!
// Remove the outlet and view for gridView.
//#IBOutlet var gridView: GridView!
// Create the gridView here:
var gridView: GridView!
// Setup some views
override func viewDidLoad() {
super.viewDidLoad()
// The width for the gridView is set to 1000 here, change if needed.
gridView = GridView(frame: CGRect(x: 0, y: 0, width: 1000, height: self.view.bounds.height))
scrollView.addSubview(gridView)
scrollView.contentSize = CGSize(width: gridView.frame.width, height: scrollView.frame.height)
gridView.contentMode = .ScaleAspectFill
}
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
return gridView
}
func scrollViewDidZoom(scrollView: UIScrollView) {
scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: 0.0)
scrollView.contentSize = CGSize(width: scrollView.contentSize.width, height: scrollView.frame.height)
}

Conserving Momentum from UIScrollView to inner UITableView

I'm looking to implement something very similar to the iOS Twitter Profile page, as seen here:
(source: twimg.com)
Based on what I can see, they have a UIView at the top, and a UIScrollView covering the entire view with a UITableView within the UIScrollView.
This is a tutorial on replicating it, and can be seen here: http://www.thinkandbuild.it/implementing-the-twitter-ios-app-ui/
The issue I've run into is how to maintain the momentum from scrolling on the UIScrollView vs. the UITableView. With the Twitter Profile page, you can scroll in one smooth swipe and it will move the UIScrollView up (showing the UITableView more) and any 'momentum' that is still there will start scrolling the UITableView.
I assume this must be done within the scrollViewDidScroll and check for any offset left over after reaching the bottom of the UIScrollView.
func scrollViewDidScroll(scrollView: UIScrollView) {
if scrollView == self.myScrollView {
var maxOffset = 25.0
let offset = scrollView.contentOffset.y
self.myScrollView.frame = CGRectMake(0, -min(offset, maxOffset), view.frame.width, view.frame.height)
if offset - maxOffset > 0 {
self.myTableView.setContentOffset(CGPointMake(0,offset-maxOffset), animated:true)
}
}
This kind of works, although it certainly isn't smooth and doesn't appear as though it's maintaining momentum.
I don't think twitter's profile uses two scrollviews. It uses one tableview and adjusts the scroll indicator dynamically with the scrollIndicatorOffsets property. If you look closely at the scroll indicator you can see it shimmy a bit as you start to scroll up, which is consistent with this approach.

How to disable horizontal scrolling of UIScrollView?

I have a UIView like iPhone's Springboard. I have created it using a UIScrollView and UIButtons. I want to disable horizontal scrolling on said scrollview. I want only vertical scrolling. How do I accomplish this?
You have to set the contentSize property of the UIScrollView. For example, if your UIScrollView is 320 pixels wide (the width of the screen), then you could do this:
CGSize scrollableSize = CGSizeMake(320, myScrollableHeight);
[myScrollView setContentSize:scrollableSize];
The UIScrollView will then only scroll vertically, because it can already display everything horizontally.
UPDATED: (After #EranMarom pointed out on his comment)
You can stop horizontal scrolling or vertical scrolling in the ScrollViewDelegate Method.
Here it is how,
Stops Horizontal Scrolling:
If you want to scroll horizontally, then you need to increase the contentOffset.x. Preventing that stops the scrollview scroll in horizontal direction.
- (void)scrollViewDidScroll:(UIScrollView *)sender {
sender.contentOffset.x = 0.0
}
Stops Vertical Scrolling:
If you want to scroll vertically, then you need to increase the contentOffset.y. Preventing that stops the scrollview scroll in vertical direction.
- (void)scrollViewDidScroll:(UIScrollView *)sender {
sender.contentOffset.y = 0.0
}
Above code prevents the changes in x and y of a scrollview contentOffset and it leads to stop the scrolling in scrollViewDidScroll: method.
since iOS7 use
self.automaticallyAdjustsScrollViewInsets = NO;
//and create you page scroller with 3 pages
self.pageView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
[self.pageView setContentSize:CGSizeMake(self.view.frame.size.width*3, self.view.frame.size.height)];
[self.pageView setShowsVerticalScrollIndicator:NO];
[self.pageView setPagingEnabled:YES];
[self.view addSubview:self.pageView];
Swift solution
Create two outlets, one for your view and one for your scroll view:
#IBOutlet weak var myView: UIView!
#IBOutlet weak var scrollView: UIScrollView!
Then in your viewDidLayoutSubviews you can add the following code:
let scrollSize = CGSize(width: myView.frame.size.width,
height: myView.frame.size.height)
scrollView.contentSize = scrollSize
What we've done is collected the height and width of the view and set the scrollViews content size to match it. This will stop your scrollview from scrolling horizontally.
More Thoughts:
CGSizeMake takes a width & height using CGFloats. You may need to use your UIScrollViews existing height for the second parameter. Which would look like this:
let scrollSize = CGSize(width: myView.frame.size.width,
height: scrollView.contentSize.height)
In my case, with Swift 4.2 you can use:
Disable vertical scroll:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollView.contentOffset.y = 0.0
}
Disable horizontal scroll:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollView.contentOffset.x = 0.0
}
In my case the width of the contentView was greater than the width of UIScrollView and that was the reason for unwanted horizontal scrolling. I solved it by setting the width of contentView equal to width of UIScrollView.
Hope it helps someone
You can select the view, then under Attributes Inspector uncheck User Interaction Enabled .
Introduced in iOS 11 is a new property on UIScrollView
var contentLayoutGuide: UILayoutGuide
The documentation states that you:
Use this layout guide when you want to create Auto Layout constraints related to the content area of a scroll view.
Along with any other Autolayout constraints that you might be adding you will want to constrain the widthAnchor of the UIScrollView's contentLayoutGuide to be the same size as the "frame". You can use the frameLayoutGuide (also introduced in iOS 11) or any external width (such as your superView's.)
example:
NSLayoutConstraint.activate([
scrollView.contentLayoutGuide.widthAnchor.constraint(equalTo: self.widthAnchor)
])
Documentation:
https://developer.apple.com/documentation/uikit/uiscrollview/2865870-contentlayoutguide
#Gulfam Khan's answer is the correct one, I am adding imagery to help the concept get more visibility.
When we set the contentView to have equal width's with the Scroll view, if the multiplier is even slightly greater than 1:1, then we will get horizontal scrolling.
Here is what it produces:
If you do not want horizontal scrolling, you most likely do not have horizontal content that exceeds the width of the superview.
Therefore if you ensure the contentView width does not exceed the width of the scroll view, that will automatically resolve the problem as UIKit recognizes there is no horizontal content to scroll to. Like so:
Now you should only see vertical:
I had the tableview contentInset set in viewDidLoad (as below) that what causing the horizontal scrolling
self.tableView.contentInset = UIEdgeInsetsMake(0, 30, 0, 0);
Check if there are any tableview contentInset set for different reasons and disable it
I struggled with this for some time trying unsuccessfully the various suggestions in this and other threads.
However, in another thread (not sure where) someone suggested that using a negative constraint on the UIScrollView worked for him.
So I tried various combinations of constraints with inconsistent results. What eventually worked for me was to add leading and trailing constraints of -32 to the scrollview and add an (invisible) textview with a width of 320 (and centered).
Try This:
CGSize scrollSize = CGSizeMake([UIScreen mainScreen].bounds.size.width, scrollHeight);
[scrollView setContentSize: scrollSize];
Disable horizontal scrolling by overriding contentOffset property in subclass.
override var contentOffset: CGPoint {
get {
return super.contentOffset
}
set {
super.contentOffset = CGPoint(x: 0, y: newValue.y)
}
}
Once I did it replacing the UIScrollView with a UITableView with only 1 cell, it worked fine.
Use this single line.
self.automaticallyAdjustsScrollViewInsets = NO;

Resources