I have a UIView with a CATiledLayer visible within a UIScrollView.
At certain times I need to relocate the window contents relative to the scroll view origin.
I need to change the contentOffset property of the scroll view but, when I redraw, the contents are the same.
The sequence is
scrollView.contentOffset = newoffset ;
[contentView setNeedsDisplay]
This creates a jerky effect as the scrollView shifts.
Is there any way to freeze the visible area of the scroll view until the drawing is complete?
Essentially, I'd like to
[FREEZE DISPLAY]
scrollView.contentOffset = newoffset ; // But have no visible change
[contentView setNeedsDisplay]
[UNFREEZE DISPLAY AFTER a SHORT DELAY]
You should be doing that in your delegate methods of UIScrollView. It has a very specific set of methods that you can use to time your update appropriately.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView;
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
Between these 3 methods, I'm sure that one of these is going to make that call with the timing that you're looking for.
Last thing, watch out for how many times your calling the setNeedsDisplay method when utilizing these delegates. setNeedsDisplay isn't cheap on memory if you're calling it every millisecond!
Cheers
Related
I would like my UIScrollView to naturally glide to endings at certain incremental values, corresponding to every 50 points of width of a horizontally-scrolling UIScrollView To do this, I customed scrollViewWillEndDragging, like so (as recommended, but not described in detail in an answer here Scrolling a horizontal UIScrollView in stepped increments?):
- (void) scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
if(fmodf(targetContentOffset->x, 50.0)){
int roundingNumber1 = 50;
CGFloat newOffset = roundingNumber1 * floor(((scrollView.contentOffset.x)/roundingNumber1)+0.5);
targetContentOffset->x = newOffset;
}
}
However, I am not sure this is actually stopping the view at specific increments, and I also notice that the effect is asymmetric. Though my scrolling motions/velocity/etc are the same, scrolling right is much less fluid than scrolling left. Scrolling right stops faster and more abruptly. There's a video here. Why is this behavior asymmetric and how can I change it?
The reason I thin the scrolling is not stopping at increments of 50 is that I also have a UILabel underneath the scroll view that gets updated by other delegate functions to show the offset. The value it shows is rarely close to 50 when the scrolling is done. Is this because the scrolling is not incrementing to values of 50 or because I am not updating at the right times?
Thanks for any advice.
- (void) scrollViewDidScroll:(UIScrollView *)scrollView{
if(abs(self.lastOffset - scrollView.contentOffset.x) > 49){
CGFloat newNumber = scrollView.contentOffset.x;
self.numberProperty.text = [NSString stringWithFormat:#"%.00f", scrollView.contentOffset.x];
self.lastOffset = scrollView.contentOffset.x;
[self.view setNeedsDisplay];
}
}
- (void) scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView{
self.numberProperty.text = [NSString stringWithFormat:#"%.00f", scrollView.contentOffset.x];
}
Rather than implementing these delegate methods, you can set pagingEnabled on your scroll view to true to get this behavior for free. When paging is enabled, the scroll view will automatically snap to the nearest "page" when you end dragging, where the page width is equal to the scroll view's width.
Based on your video, it looks like the items in your scroll view are smaller than the width of the scroll view itself. To use paging, you'll have to do the following:
Make your scroll view have the same width as one of your items (50.0 units in your case).
Set scrollView.clipsToBounds to false so that the scroll view draws subviews outside of its much smaller bounds.
Set scrollView.pagingEnabled to true so that the scroll view scrolls with paging.
At this point, paging will work but you won't be able to drag the scroll view outside of its bounds. To make this work, you'll need to embed the scroll view in a larger view that forwards touch events to it.
Create a "touch forwarding" class and add it to your view.
This class takes all touch events it receives and sends them to its targetView property instead. DJK is a random prefix I made up for the class name.
#interface DJKTouchForwardingView : UIView
/** The view to which touch events should be forwarded. */
#property (weak, nonatomic) UIView *targetView;
#end
#implementation DJKTouchForwardingView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *child = nil;
if ((child = [super hitTest:point withEvent:event]) == self) {
return self.targetView;
}
return child;
}
#end
Make the scroll view a subview of the touch forwarding view and assign it to the targetView property.
Your scroll view will now be scrollable within the bounds of the touch forwarding view and will snap to 50 unit pages.
I'm racking my head over this one, I know this has to be possible but after going over every bit of documentation I still can't come up with something that works well.
Basically, I have a UICollectionView in the LAST cell of a UITableView. What I want to happen is, only when the UITableView is totally scrolled to the bottom, can the UICollectionView in it's last cell start scrolling. And, if the TableView offset reaches the bottom of the CollectionView's tableviewcell during a drag / pan, any additional dragging of the current table view drag / pan should effect the collection view instead of the table view.
Also, when the collection view is scrolling, if a user starts scrolling on the collection view, if the collection view reaches the top of it's scroll (Content Y offset of 0 or less), and additional scrolling of the current pan/drag gesture in affect should cause the containing table view to scroll up.
The reason I want to achieve this effect, is because the table view cell above the last cell containing the collection view, contains a UISegmentControl that toggles the contents of the UICollectionView, and I want the user to be able to toggle this segment at any time while scrolling in the CollectionView. Meaning the collection view has to scroll but the parent table view needs to not scroll..
I've tried playing with the gesture recognizers, and doing something with
– gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:
This gets me halfway there, I can use a flag to return yes/no to this method if the collection view is to scroll into a negative offset (Past it's top offset) or if the table view has reached the end of it's total scroll.. I get sort of close to what I want to achieve, but if I scroll up slowly on the collection view, it doesn't fire the simultaneous recognizer, same happens if I scroll down on the table view too slowly.
Another issue is, I do NOT want the collection view to bounce. However setting bounces to NO totally prevents the simultaneous recognizer to fire at all. I even tried setting content offset to CGPointZero on the collection view in it's viewDidScroll if it's y offset were to dip below zero. This also doesn't work and prevents the simultaneous method from firing...
Does anyone have any idea what to do? Or something to point me in the right direction?
UPDATE -
Still trying at this, I've made little progress towards the behavior I'm trying to achieve. I've messed with toggling userInteractionEnabled in the viewDidScroll method, as have I tried in willBeginDragging. The same with scrollEnabled property.. No luck :( I get a behavior similar to what I want with this, however the parent view will not scroll up until the user lets off the screen and attempts to scroll again..
UPDATE -
Is there anyway to transition the panGestureRecognizer currently handling scroll events DURING scrolling? If I could transition the scroll handler from the child to the parent while still scrolling this would solve my issue. I've looked through apple's gesture related and uiscrollview related documentation and can't find anything close to doing that.
UPDATE -
Just got done trying something like this..
- (CGPoint)maxParentContentOffset
{
return CGPointMake(0, self.parentScrollView.contentSize.height - self.frame.size.height - 44);
}
- (void)parentScrollViewDidScroll:(UIScrollView *)parentScrollView
{
if (self.contentOffset.y > 0) {
self.parentScrollView.contentOffset = [self maxParentContentOffset];
}
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView.contentOffset.y < 0) {
scrollView.contentOffset = CGPointZero;
}
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
CGPoint translation = [scrollView.panGestureRecognizer velocityInView:scrollView.superview];
if (translation.y < 0) {
[UIView animateWithDuration:0.5f animations:^(void) {
self.parentScrollView.contentOffset = [self maxParentContentOffset];
}];
}
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
However there is a problem... for some reason I keep getting a bad access error on the following method
- (void)parentScrollViewDidScroll:(UIScrollView *)parentScrollView
{
if (self.contentOffset.y > 0) {
self.parentScrollView.contentOffset = [self maxParentContentOffset];
}
}
Specifically setting the content offset. Which is really strange, because with breakpoints I'm seeing parentScrollView and self as being set. I'm wondering if it's not a bad access but it's getting trapped in an infinite loop for some reason? Any ideas?
Even though you may manage to make it work now, embedding a collection view inside a table view (both UIScrollView subclasses) is not a good idea and it will bug as soon as apple modifies their implementation.
Try to migrate to a single UICollectionView layout. After all there's nothing you can't achieve with a collection view that a table view can.
Separate your "table view" and "collection view" in two (or more) collection view sections, then implement layoutAttributesForItemAtIndexPath: differently according to the indexPath.section.
To make it table view-like you'll want to return frames whose width are the same as the collection view.
If your layout is simpler then you could use a UICollectionViewFlowLayout (maybe your already are) and implement collectionView:layout:sizeForItemAtIndexPath: as described above.
Figured this out after a good 8 hours.. I had a confliction due to infinite setting of the parent scroll view offset, since I had multiple objects that were of the same class that received a call whenever their parent view scrolled, both trying to set the same parent view offset to zero, which caused the other class to see scroll changing, and calling their method to change offset, and the process happening infinitely causing a crash.
This code however, solved everything and functions exactly as I was desiring. Hopefully this helps anyone else in the future looking to get congruent scrolling between a parent and child scroll view.
- (CGPoint)maxParentContentOffset
{
return CGPointMake(0, self.parentScrollView.contentSize.height - self.frame.size.height - 44);
}
- (void)parentScrollViewDidScroll:(UIScrollView *)parentScrollView
{
if (self.contentOffset.y > 0 && self.isDragging) {
self.parentScrollView.contentOffset = [self maxParentContentOffset];
}
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView.contentOffset.y < 0) {
scrollView.contentOffset = CGPointZero;
}
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
CGPoint translation = [scrollView.panGestureRecognizer velocityInView:scrollView.superview];
if (translation.y < 0) {
[UIView animateWithDuration:0.5f animations:^(void) {
self.parentScrollView.contentOffset = [self maxParentContentOffset];
}];
}
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
In an application that I've worked, we needed something close to that, we wanted to have a uitableview inside the first cell of another uitableview, which should be scrollable (you can check the app here)
Our approach was quite different, and we thought about doing it your way, but that way was a bit overkill for the time we had, and needed too much custom control instead of using what apple already gives us.
I'll share our method just for you to have another way to solve your problem, since I can't figure out what is wrong in that method call.
On the outer tableview's datasource lifecycle, we were able to know how our inner table view would be, allowing us to feed our inner table view with all the data, which would then gave us the total content size of the inner table view.
With this info, and since we knew (and in your case, you know) the actual cell where the inner table view would be placed, we make that cell's height equal to the height of the inner tableview content size. With this, the inner table view is 'totally visible'(in your case, the collection view) inside the outer table view cell, but since the cell is so big, it looks like we are scrolling the inner table view when we are simply scrolling a really big, special, cell.
And it won't bounce, if you don't want it too.
In our case, this worked, and the implementation was pretty straight forward.
In your case, and since you have a UISegmentedController, you should do a beginUpdates endUpdates and a 'reloadData', upon press in the UISegmenedController, in your outer table view in order to be able to recalculate the collection view's content size and resize the last cell, but that is pretty straight forward.
I know I'm not answering your question, but this method allowed us to achieve something so similar in such a fast and straight forward way that I thought it was worth sharing.
Hope it helps.
There is a button at the bottom of my view controller. When the user scrolls down the button has to be attached to the scrollview at certain height.
I need to attach a button to the scrollview, immediately when the contentOffset.y reaches a particular value. -(void) scrollviewDidScroll doesn't help me as there might be a jump in contentOffset when the user is scrolling fast. Any leads on this are helpful.
Also, whenever I add a subview to the scrollview, -(void) viewDidLayoutSubviews is called. Which in turn sets the contentOffset to {0,0}. How can I achieve the functionality I need?
I needed to do the same thing with a UITableView and for me using scrollViewDidScroll worked.
I created a view called staticBar and added it as a subview of the tableView, but I had to rearrange the tableview subviews for it to appear in the right place. I don't have my code in front of me, but in -scrollViewDidScroll: it looked something like this:
- (void)scrollViewDidScroll:(UIScrollView*)scrollView
{
CGFloat staticBarAdjustedY = _staticBarY - scrollView.contentOffset.y;
CGFloat scrollViewYFloor = scrollView.frame.size.height - _staticBar.frame.size.height;
// This way maximum Y the view can have is at the base of the scrollView
CGFloat newY = MIN( staticBarAdjustedY, scrollViewYFloor);
_staticBar.frame = (CGRect){ { _staticBar.frame.origin.x, newY}, _staticBar.frame.size}
}
I will check my code later today and add more details here.
Also, you said the scrollviewDidScroll has jumps in contentOffset, but it's worth mentioning that these jumps are the same that the scrollView uses to scroll its own view. So it's not like you are "losing" frames on this delegate method.
Hope it helps.
PS: So, here is the rest of my code.
//I place my custom view as a subview of the tableView below it's last subview
//The last subview is for scroll indicators.
WTButtonsBar *buttonBar = [[WTButtonsBar alloc] init];
[self.tableView insertSubview:buttonBar belowSubview:self.tableView.subviews.lastObject];
In scrollViewDidScroll:
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
//In my app I needed my view to stick to the top of the screen
//thats why I use MAX here
//self.buttonsBarOriginalY is the view's position in the scrollView when it isn't attached to the top.
CGFloat newY = MAX(scrollView.contentOffset.y, self.buttonsBarOriginalY)
[_buttonsBar setFrame:(CGRect){{0, newY}, _buttonsBar.frame.size}];
}
I am trying to scroll on main uiwebview and when it end scrolling, I want to update that scrolling offset to other uiwebview. I can do that successfully by this.
-(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
//user will drag the file and stop on the spot..no deceleration
if(scrollView == self.showPdfWebview.scrollView && !decelerate)
{
NSLog(#"Offset value in scrollViewDidEndDragging is %#",NSStringFromCGPoint(self.showPdfWebview.scrollView.contentOffset));
[[pdfOn scrollView] setContentOffset:CGPointMake(0, self.showPdfWebview.scrollView.contentOffset.y) animated:YES];
self.testLabel.text= [NSString stringWithFormat:#"Offset value in scrollViewDidEndDragging is %#. Offset value of pdfon is %#. Frame of showpdfwebview is %#. Frame of pdfOn is %#",NSStringFromCGPoint(showPdfWebview.scrollView.contentOffset),NSStringFromCGPoint(pdfOn.scrollView.contentOffset),NSStringFromCGRect(showPdfWebview.frame),NSStringFromCGRect(pdfOn.frame)];
}}
However, there is one problem. My main uiwebview frame and other uiwebview frame size is different. As a result, I think scrolling on my main webview and on other webview is different.If I scroll a lot on my main uiwebview, it scroll only a little on other webview.
I got output like this. So, it is wrong. I would like to know how to solve.
They should be same >> Offset value in scrollViewDidEndDragging is {0,1374}. Offset value of pdfon is (0,565).
Frame of showpdfwebview is {{0,50}, {1024,698}}. Frame of pdfon is {{0,0},{1920,1080}}
scrollViewDidEndDragging: is just one of the available delegate methods and the scroll view content offset will often continue to change after it has been called (see the decelerate parameter). So, consider using one of the other methods instead.
Also, if you want a proportional change then you should calculate the percentage offset of the first view and apply that to the second (divide the content offset by the height of v1 and then multiply by the height of v2 to get the v2 content offset).
I have a UIScrollView which contains a UIView and a UITableView. My goal is to adjust the height of the UIScrollView to allow me to scroll the contents of the UIScrollView to a specific point.
Here is my view: It has a UIView up top and a UITableView down below.
When I scroll, I want the UIView to stop at a specific point like so:
The tableView would be able to continue scrolling, but the UIView would be locked in place until the user scrolled up and brought the UIView back to its original state.
A prime example of what I am trying to do is the AppStore.app on iOS 6. When you view the details of the app, the filter bar for Details, Reviews and Related moves to the top of the screen and stops. I hope this all made sense.
Thanks
I ended up going with a simpler approach. can't believe I didn't see this before. I created two views, one for the UITableView's tableHeaderView and one for the viewForHeaderInSection. The view I wanted to remain visible at all times is placed in the viewForHeaderInSection method and the other view is placed in the tableHeaderView property. This is a much simpler approach, I think than using a scrollview. The only issue I have run into with this approach is all my UIView animations in these two views no longer animate.
Here is my code.
[self.tableView setTableHeaderView:self.headerView];
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
return self.tableViewHeader;
}
add yourself as a UIScrollViewDelegate to the UITableView and implement the - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView so that if your views are in their starter positions they do this:
- your UITableView animates its size to the second state:
[UIView animateWithDuration:.1f animations:^{
CGRect theFrame = myView.frame;
theFrame.size.height += floatOfIncreasedHeight;
myView.frame = theFrame;
}];
- your UIView animates its vertical movement
[UIView animateWithDuration:3 delay:0 options:UIViewAnimationOptionCurveLinear animations:^(void){
view.center = CGPointMake(view.center.x , view.center.y + floatOfVerticalMovement);
}completion:^(BOOL Finished){
view.center = CGPointMake(view.center.x , view.center.y - floatOfVerticalMovement);]
Finally always in the delegate implement – scrollViewDidScrollToTop: so that you know can animate back to the initial state (using the same techniques reversed).
UPDATE:
since your views are inside a scroll view, there is a simpler way if you are ok with the table view being partly out of bounds in your starter position (i.e. instead of changing size it just scrolls into view):
make the scroll view frame size as big as your final tableview + your initial (entire) view and place it at 0,0 (so its final part will be hidden outside of the screen)
scrollview.frame = CGRectMake(0,0,tableview.frame.size.width,tableview.frame.size.height + view.frame.size.height);
you make the container scrollview contents as big as the entire table view + the entire view + the amount of the view that you want out of the way when scrolling the table view.
scrollview.contentSize = CGSizeMake(scrollview.frame.size.width, tableview.frame.size.height + view.frame.size.height + floatOfViewHeightIWantOutOfTheWay);
you place the view one after the other in the scrollview leaving all the additional empty space after the table view
view.frame = CGRectMake(0,0,view.frame.size.width, view.frame.size.height);
tableview.frame = CGRectMake(0,view.frame.size.height, tableview.frame.size.width, tableview.frame.size.height);
now it should just work because since iOS 3 nested scrolling is supported
You can easily achieve this by setting the content size of the scrollView correctly and keep the height of the UITableView smaller than your viewcontroller's height, so that it fits the bottom part of the top UIView and the UITableView...
Another scenario is to split the top View in 2 parts.
The part that will scroll away and the part that will be visible.
Then set the part that will scroll away as the entire UITableView header and the part that will remain visible as the header view for the first table section.
So then you can achieve this with a single UITableView, without having to use a UIScrollView
What you're looking for is something like what Game Center happens to do with it's header which can actually be modelled with a table header, a custom section header view, and some very clever calculations that never actually involve messing with the frame and bounds of the table.
First, the easy part: faking a sticky view. That "view that's always present when scrolling the table" implemented as a section header. By making the number of sections in the table 1, and implementing -headerViewForSection:, it's possible to seamlessly make the view scroll with the tableview all for free (API-wise that is):
- (UITableViewHeaderFooterView *)headerViewForSection:(NSInteger)section {
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0,0,320,50)];
label.text = #"Info that was always present when scrolling the UITableView";
label.textAlignment = UITextAlignmentCenter;
label.backgroundColor = [UIColor colorWithRed:0.243 green:0.250 blue:0.253 alpha:1.000];
label.textColor = UIColor.whiteColor;
return label;
}
Finally, the hard part: KVO. When the table scrolls, we have to keep the header up there sticky with regards to the top of the view's frame, which means that you can KVO contentOffset, and use the resultant change in value to approximate the frame that the view should stick to with a little MIN() magic. Assuming your header is 44 pixels tall, the code below calculates the appropriate frame value:
CGPoint offset = [contentOffsetChange CGPointValue];
[self.tableView layoutSubviews];
self.tableView.tableHeaderView.frame = CGRectMake(0,MIN(0,offset.y),CGRectGetWidth(self.scrollView.frame),44);
If the above is infeasible, SMHeadedList actually has a fairly great, and little known, example of how complicated it can be to implement a "double tableview". That implementation has the added benefit of allowing the "header" tableview to scroll with the "main" tableview.
For future visitors, I've implemented a much simpler version, albeit one that accomplishes the goal with Reactive Cocoa, and a little bit of a different outcome. Even so, I believe it may be relevant.
What if you break the UIView into the top and bottom. The bottom will be the info.
Set UITableView.tableHeaderView = topView in viewDidLoad
and the return bottomView as Section Header in delegate method to make it float:
(UITableViewHeaderFooterView *)headerViewForSection:(NSInteger)section
{
return bottomView;
}
Just using the UITableView can solve with your problem. it is not need to use another scroll view.
set your view as the header view of UITableView. Then add your present view to the header view.
complete - (void)scrollViewDidScroll:(UIScrollView *)scrollView; . Tn the function to check the contentoffset of scroll view, and set the present view's frame.