Setting content offset of dynamically expanding scroll view - objective-c

My app dynamically generates items onto a scroll view. Since there is not definite height for the scroll view, it keeps adding to its height after a condition. However, I can only fit 12 items on the screen, so I want it to scroll down automatically to the lowest part of the screen when items%12==0;. After doing some research, I learned about contentOffset. However, I'm not getting the behavior I want, it scrolls all the way to the top and freezes, until I add 12 more items to the screen. Here are my conditionals.
if (itemsLength %2 ==0) {
xPos = 20;
yPos += 60;
[scrollView setContentSize:CGSizeMake(self.view.frame.size.width,scrollViewHeight+=50)];
NSLog(#"%i",scrollViewHeight);
}
if (itemsLength %12 == 0) {
CGPoint bottomOffset = CGPointMake(0,self.scrollView.contentSize.height - self.scrollView.bounds.size.height);
[scrollView setContentOffset:bottomOffset animated:YES];
}
Thanks!
~Carpetfizz
EDIT: Is there a way to scroll to a certain point in my Scrollview, without changing the content offset? Doing so ruins the rest of my code.

Try this
if (itemsLength %12 == 0) {
[scrollView scrollRectToVisible:CGRectMake(0,scrollViewHeight-50,320,50) animated:YES];
}

Related

Moving UIVIewController modal up when keyboard appears independent of rotation with iOS7

I just struggled quite a while with this, so will document it for others.
Here's the problem I was having. With an iPad app, supporting iOS7, I have a modal view controller that has a text field near the bottom the modal. Thus, when the keyboard appears, I wanted to move that modal up so the text field would still be visible with the keyboard present. With iOS8, this problem has a pretty clean solution (e.g., see Moving a modally presented UIViewController up when keyboard appears on iPad with iOS8). With iOS7 I was using self.myNavController.view.superview.center for repositioning, but ran into problems when trying to move the modal given the appearance of the keyboard. The coordinate CGPoint adjustments I was using would not move the modal in the right direction with all four rotations/orientations of the iPad.
The problem in part lies in how iOS7 does the rotation-- with transforms. However, I was unable to resolve the issue using CGPointApplyAffineTransform, or conversion of points using views (e.g., convertPoint:fromView:).
The solution I found to this problem involved a few steps:
1) I found it necessary to change the center of the modal (an assignment to self.myNavController.view.superview.center) relative to the center of the screen. I computed the center of the screen based on [UIScreen mainScreen].bounds.size. For some example code, I used the method screenCenter below.
// Adapted from: http://stackoverflow.com/questions/24150359/is-uiscreen-mainscreen-bounds-size-becoming-orientation-dependent-in-ios8
+ (CGSize) screenSize;
{
CGSize screenSize = [UIScreen mainScreen].bounds.size;
CGSize rotatedSize;
if ([UIDevice ios7OrEarlier] && [[SMRotation session] isLandscape]) {
rotatedSize = CGSizeMake(screenSize.height, screenSize.width);
}
else {
rotatedSize = screenSize;
}
return rotatedSize;
}
+ (CGPoint) screenCenter;
{
CGSize size = [self screenSize];
CGPoint center = CGPointMake(size.width/2.0, size.height/2);
return center;
}
2) Now, given that you have computed the amount that you have to shift the modal upwards (e.g., given the keyboard height and your modal height and the position of the text field on the modal), call this amount dy. I next found it necessary if the app was in an inverted rotation (upside down portrait or landscape), to change the sign of dy before applying it to the CGPoint center position I was calculating. Something like this:
CGPoint newCenter = [SMRotation screenCenter];
if ([SMRotation session].isInverted) {
dy = -dy;
}
newCenter.y += dy;
With some of the code for isInverted here:
- (BOOL) isInverted;
{
switch (self.interfaceOrientation) {
case UIInterfaceOrientationPortraitUpsideDown:
case UIInterfaceOrientationLandscapeRight:
return YES;
case UIInterfaceOrientationPortrait:
case UIInterfaceOrientationLandscapeLeft:
case UIInterfaceOrientationUnknown:
return NO;
}
}
3) Then, if the app was in landscape I found it necessary to swap the x and y coordinates. Something like this:
if ([SMRotation session].isLandscape) {
newCenter = CGPointMake(newCenter.y, newCenter.x);
}
4 Finally, I did the assignment to update the center of the modal:
self.myNavController.view.superview.center = newCenter;

Intercepting UIScrollView Scrolls

I have a UIScrollView and a UITableView inside the UIScrollView.
I would like to intercept scrolling of the UITableView and only allow scrolling if the super view (UIScrollView) have reached a specific contentOffset.
I have created subclass of both UIScrollView and UITableView, how do i catch scrolling event and intercepting the scrolling while the user is still scrolling?
Example of what i'm trying to accomplish:
The UITableView is going to have a header, if i scroll down the header will collapse to 30% of original size and and stay visible at the top. After i have scrolled back to the top of the UITableView i want the header to expand. In other word i want to extend the scrolling of a UITableView with a header that can collapse/expand.
There might be better way to accomplish this, i'm open for suggestions.
There may be a better way, but this solution is working for me.
UITableView (let's call it innerScrollView) inside of UIScrollView (let's call it scrollView) inside whatever the main view is (let's call it view).
Set an outlet in your view controller for the innerScrollView (self.innerScrollView).
Set bounce in XIB or code to: scrollView.bounces = YES and innerScrollView.bounces = NO.
Set scrolling to disabled initially for the innerScrollView.
In code (viewDidLoad:) set scrollView.contentSize.height to view.frame.size.height + the 70% of the header you want to disappear. Resize innerScrollView to fill the resized contentView to the bottom.
Set your view controller as the delegate for scrollView and implement scrollViewDidScroll: as follows:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat maxYOffset = self.scrollView.contentSize.height - self.scrollView.frame.size.height;
CGFloat minInnerYOffset = self.innerScrollView.contentInset.top;
CGFloat currentYOffset = scrollView.contentOffset.y;
CGFloat newYOffsetDelta = currentYOffset - maxYOffset;
CGFloat currentInnerYOffset = self.innerScrollView.contentOffset.y;
if (scrollView.contentOffset.y >= maxYOffset) {
self.innerScrollView.scrollEnabled = YES;
self.scrollView.contentOffset = CGPointMake(0, maxYOffset);
if (currentYOffset != maxYOffset) {
self.innerScrollView.contentOffset = CGPointMake(0, currentInnerYOffset + newYOffsetDelta);
}
}
else if (currentInnerYOffset > minInnerYOffset) {
self.scrollView.contentOffset = CGPointMake(0, maxYOffset);
self.innerScrollView.contentOffset = CGPointMake(0, currentInnerYOffset + newYOffsetDelta);
}
else if (currentYOffset < scrollView.contentInset.top) {
scrollView.contentOffset = CGPointMake(0, scrollView.contentInset.top);
}
else {
self.lowerScrollView.scrollEnabled = NO;
}
}
I just whipped this up and quickly tested it. It all seems to work, but there may be some improvements and tweaks needed. Regardless, this should get you started at least.
Well, you should use the following UIScrollViewDelegate method
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
// here perform the action you need
}

Synchronised scrolling between two instances of NSScrollView

I have two instances of NSScrollView both presenting a view on the same content. The second scroll view however has a scaled down version of the document view presented in the first scroll view. Both width and height can be individually scaled and the original width - height constraints can be lost, but this is of no importance.
I have the synchronised scrolling working, even taking into account that the second scroll view needs to align its scrolling behaviour based on the scaling. There's one little snag I've been pulling my hairs out over:
As both views happily scroll along the smaller view needs to slowly catch up with the larger view, so that they both "arrive" at the end of their document at the same time. Right now this is not happening and the result is that the smaller view is at "end-of-document" before the larger view.
The code for synchronised scrolling is based on the example found in Apple's documentation titled "Synchronizing Scroll Views". I have adapted the synchronizedViewContentBoundsDidChange: to the following code:
- (void) synchronizedViewContentBoundsDidChange: (NSNotification *) notification {
// get the changed content view from the notification
NSClipView *changedContentView = [notification object];
// get the origin of the NSClipView of the scroll view that
// we're watching
NSPoint changedBoundsOrigin = [changedContentView documentVisibleRect].origin;;
// get our current origin
NSPoint curOffset = [[self contentView] bounds].origin;
NSPoint newOffset = curOffset;
// scrolling is synchronized in the horizontal plane
// so only modify the x component of the offset
// "scale" variable will correct for difference in size between views
NSSize ownSize = [[self documentView] frame].size;
NSSize otherSize = [[[self synchronizedScrollView] documentView] frame].size;
float scale = otherSize.width / ownSize.width;
newOffset.x = floor(changedBoundsOrigin.x / scale);
// if our synced position is different from our current
// position, reposition our content view
if (!NSEqualPoints(curOffset, changedBoundsOrigin)) {
// note that a scroll view watching this one will
// get notified here
[[self contentView] scrollToPoint:newOffset];
// we have to tell the NSScrollView to update its
// scrollers
[self reflectScrolledClipView:[self contentView]];
}
}
How would I need to change that code so that the required effect (both scroll bars arriving at an end of document) is achieved?
EDIT: Some clarification as it was confusing when I read it back myself: The smaller view needs to slow down when scrolling the first view reaches the end. This would probably mean re-evaluating that scaling factor... but how?
EDIT 2: I changed the method based on Alex's suggestion:
NSScroller *myScroll = [self horizontalScroller];
NSScroller *otherScroll = [[self synchronizedScrollView] horizontalScroller];
//[otherScroll setFloatValue: [myScroll floatValue]];
NSLog(#"My scroller value: %f", [myScroll floatValue]);
NSLog(#"Other scroller value: %f", [otherScroll floatValue]);
// Get the changed content view from the notification.
NSClipView *changedContentView = [notification object];
// Get the origin of the NSClipView of the scroll view that we're watching.
NSPoint changedBoundsOrigin = [changedContentView documentVisibleRect].origin;;
// Get our current origin.
NSPoint curOffset = [[self contentView] bounds].origin;
NSPoint newOffset = curOffset;
// Scrolling is synchronized in the horizontal plane so only modify the x component of the offset.
NSSize ownSize = [[self documentView] frame].size;
newOffset.x = floor(ownSize.width * [otherScroll floatValue]);
// If our synced position is different from our current position, reposition our content view.
if (!NSEqualPoints(curOffset, changedBoundsOrigin)) {
// Note that a scroll view watching this one will get notified here.
[[self contentView] scrollToPoint: newOffset];
// We have to tell the NSScrollView to update its scrollers.
[self reflectScrolledClipView:[self contentView]];
}
Using this method the smaller view is "overtaken" by the larger view when both scrollers reach a value of 0.7, which is not good. The larger view then scrolls past its end of document.
I think you might be approaching this in the wrong way. I think you should be getting a percentage of how far down each scroll be is scrolled in relation to itself and apply that to the other view. One example of how this could be done is this way using NSScroller's -floatValue:
NSScroller *myScroll = [self verticalScroller];
NSScroller *otherScroll = [otherScrollView verticalScroller];
[myScroll setFloatValue:otherScroll.floatValue];
I finally figured it out. The answer from Alex was a good hint but not the full solution as just setting the float value of a scroller doesn't do anything. That value needs translation to specific coordinates to which the scroll view needs to scroll its contents.
However, due to differences in size of the scrolled document view, you cannot just simply use this value, as the scaled down view will be overtaken by the "normal" view at some point. This will cause the normal view to scroll past its end of document.
The second part of the solution was to make the normal sized view wait with scrolling until the scaled down view has scrolled its own width.
The code:
// Scrolling is synchronized in the horizontal plane so only modify the x component of the offset.
NSSize ownSize = [[self documentView] frame].size;
newOffset.x = MAX(floor(ownSize.width * [otherScroll floatValue] - [self frame].size.width),0);
The waiting is achieved by subtracting the width of the scroll view from the width times the value of the scroller. When the scaled down version is still traversing its first scroll view width of pixels, this calculation will result in a negative offset. Using MAX will prevent strange effects and the original view will quietly wait until the value turns positive and then start its own scrolling. This solution also works when the user resizes the app window.

Objective-C : Endless UIScrollView without pagingEnabled

In my iPhone app there is a scrollview pagingEnabled=NO which can contain up to 200 subviews (150 x 150) and the challenge is to do lazy loading and simulate endless scrolling (without bouncing) in horizontal directions.
Is there a solution or an alternative for this request?
Lazy loading of a scroll view is demonstrated in one of Apple's sample code projects: PageControl.
To fake endless scrolling what I'd suggest is to make your scroll view very wide to begin with, wider than an average person would scroll in one set of scrolling behaviors. Then in your delegate methods -scrollViewDidEndScrollingAnimation:, -scrollViewDidEndDragging:willDecelerate: and -scrollViewDidEndDecelerating:, one or more of which will be called after the user is done scrolling, reposition your content to exist in the center of the scroll view and update your contentOffset point without animating.
For that to work visually you would need to also disable the horizontal scroller. You'll also need to consider how to determine what view to draw at a particular contentOffset with this method since you won't be able to just divide the contentOffset.x by the scroll view's bounds anymore to find out where you are.
Hello I found the way to do it.
I have a master array with all the subviews (in my case they are images, so I store the names).
The scrollview only has 3 subviews: left, current, right.
Paging is enabled, so the user cant really spin more that one view left/right at any time.
What I do is:
1) track his current position on the master array. If he moves left, subtract one; right add one. Like this:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
[some code to determine current page, based on contentOffset]
if (page == 0){
NSLog(#"Going left");
if (currentPage > 0){
currentPage--;
} else {
//cycle to last
currentPage = [images count] -1;
}
} else if (page == 2){
NSLog(#"Going right");
if (currentPage < ([images count] -1)){
currentPage++;
} else {
//cycle to first
currentPage = 0;
}
} else{
NSLog(#"Not moving");
}
2) after the user moves, I reload 3 new images, like this:
//updates the 3 views of the scrollview with a new center page.
-(void) updateScrollViewForIndex:(NSInteger)newCenterPage{
//fist clean scroll view
for (UIView *sView in [scroll subviews]){
[sView removeFromSuperview];
}
NSInteger imgCount = [images count];
//set center view
[self loadImageIndex:newCenterPage atPosition:1];
//set left view
if (newCenterPage > 0){
[self loadImageIndex:newCenterPage-1 atPosition:0];
} else {
//its the first image, so the left one is the last one
[self loadImageIndex:imgCount-1 atPosition:0];
}
//set right view
if (newCenterPage < imgCount-1){
[self loadImageIndex:newCenterPage+1 atPosition:2];
} else {
//its the last image, so ther right one is the first one
[self loadImageIndex:0 atPosition:2];
}
}
3) Finally re-center the scroll view to the center view again:
[scroll setContentOffset:CGPointMake(1 * viewWidth, 0)];
Hope this helps, although "the man with the plan" is Mr. Clark, who pointed the way.
Gonso
Matt Gallagher has a blog post which describes a solution to this exact problem. I've used it and it works great.
The UIScrollView and UIPageControl in Cocoa Touch allow for user interfaces with multiple panning pages. The sample project that Apple provides (PageControl) keeps all child views for every page in a lazily loaded array. I'll show you how you can implement this using just two child views, no matter how many virtual pages you wish to represent.
It works by shuffling around the child views and reseting their content when necessary. I used this for displaying flash cards, where there could be anywhere from 3 to 3,000 items. Although it's set up right now for paging, I'm sure you could get it to work with regular scrolling.

Scrollable UINavigationBar similar to Mobile Safari

My application uses a UINavigationController and the final view (detail view) lets you view an external website within the application using a UIWebView.
I'd like to free up some additional screen real estate when the user is viewing a webpage and wanted to emulate how Safari on iPhone works where their URL bar at the top scrolls up and off the screen when you're viewing content in the UIWebView that's below the fold.
Anyone have ideas on how to achieve this? If I set the navigationBarHidden property and roll my own custom bar at the top and set it and a UIWebView within a UIScrollView then there are scrolling issues in the UIWebView as it doesn't play nicely with other scrollable views.
Based on #Brian suggestion I made this code:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat height = navigationBar.frame.size.height;
CGFloat y = scrollView.bounds.origin.y;
if (y <= 0) {
CGRect frame = navigationBar.frame;
frame.origin.y = 0;
navigationBar.frame = frame;
} else if (tableView.contentSize.height > tableView.frame.size.height) {
CGFloat diff = height - y;
CGRect frame = navigationBar.frame;
frame.origin.y = -y;
navigationBar.frame = frame;
CGFloat origin = 0;
CGFloat h = height; // height of the tableHeaderView
if (diff > 0) {
origin = diff;
h = y;
}
frame = tableView.frame;
frame.origin.y = origin;
frame.size.height = tableView.superview.frame.size.height - origin;
tableView.frame = frame;
CGRect f = CGRectMake(0, 0, tableView.frame.size.width, h);
UILabel* label = [[UILabel alloc] initWithFrame:f];
tableView.tableHeaderView = label;
[label release];
}
}
My code has a UITableView but should work with any scrollable component. If you have other components than the navigationBar and the UIScrollView subclass, you should change the way the height of the scrollable component is calculated. Something like this:
frame.size.height = tableView.superview.frame.size.height - origin - otherComponentsHeight;
I needed to add a dumb tableHeaderView to have the desired behaviour. The problem was that when scrollViewDidScroll: is called the content has an offset, but the apparience in Mobile Safari is that the content is not scrolled until the navigationBar fully disappears. I tried first changing the contentOffset.y to 0, but obviously it didn't work since all the code relies on the scrolling mechanism. So I just added a tableHeaderView whose height is exactly the scrolled offset, so the header is never really seen, and the content appears to not scroll until the navigationBar fully disappears.
If you don't add the dumb tableHeaderView, then the scrollable component appears to scroll behind the navigationBar.
With the tableHeaderView, the scrollable component is actually scrolling (as seen in the scrollbar), but since there is a tableHeaderView whose height is exactly the same than the scrolled offset, the scrollable content appears to not be scrolling until the navigationBar fully disappears:
Have a delegate for the scrolling events in the UIWebView and when you initially start scrolling the UIWebView, have the UIWebView increase in height and have it's Y position decrease at the same time while simultaneously shifting the nav bar up in the Y direction. Once the nav bar has been completely shifted out of view, stop increasing the size of the UIWebView and just allow normal scrolling to occur.
This will give the illusion of the nav bar being part of the UIWebView as it scrolls off the screen.
Also, you'll need to do the reverse when you are scrolling in the opposite direction and are reaching the top of the content of the UIWebView.
Can't give you a straight answer, but have a look at iWebKit. Maybe that provides a solution. The demo, at least, contains a "full screen" item.