The UIScrollView has a lot of information available to the programmer, but I dont see an obvious way to control the location that the control stop at after decelerating from a scroll gesture.
Basically I would like the scrollview to snap to specific regions of the screen. The user can still scroll like normal, but when they stop scrolling the view should snap to the most relevant location, and in the case of a flick gesture the deceleration should stop at these locations too.
Is there an easy way to do something like this, or should I consider the only way to accomplish this effect to write a custom scrolling control?
Since the UITableView is a UIScrollView subclass, you could implement the UIScrollViewDelegate method:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint *)targetContentOffset
And then compute what the closest desired target content offset is that you want, and set that on the inout CGPoint parameter.
I've just tried this and it works well.
First, retrieve the unguided offset like this:
CGFloat unguidedOffsetY = targetContentOffset->y;
Then Figure out through some math, where you'd want it to be, noting the height of the table header. Here's a sample in my code dealing with custom cells representing US States:
CGFloat guidedOffsetY;
if (unguidedOffsetY > kFirstStateTableViewOffsetHeight) {
int remainder = lroundf(unguidedOffsetY) % lroundf(kStateTableCell_Height_Unrotated);
log4Debug(#"Remainder: %d", remainder);
if (remainder < (kStateTableCell_Height_Unrotated/2)) {
guidedOffsetY = unguidedOffsetY - remainder;
}
else {
guidedOffsetY = unguidedOffsetY - remainder + kStateTableCell_Height_Unrotated;
}
}
else {
guidedOffsetY = 0;
}
targetContentOffset->y = guidedOffsetY;
The last line above, actually writes the value back into the inout parameter, which tells the scroll view that this is the y-offset you'd like it to snap to.
Finally, if you're dealing with a fetched results controller, and you want to know what just got snapped to, you can do something like this (in my example, the property "states" is the FRC for US States). I use that information to set a button title:
NSUInteger selectedStateIndexPosition = floorf((guidedOffsetY + kFirstStateTableViewOffsetHeight) / kStateTableCell_Height_Unrotated);
log4Debug(#"selectedStateIndexPosition: %d", selectedStateIndexPosition);
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:selectedStateIndexPosition inSection:0];
CCState *selectedState = [self.states objectAtIndexPath:indexPath];
log4Debug(#"Selected State: %#", selectedState.name);
self.stateSelectionButton.titleLabel.text = selectedState.name;
OFF-TOPIC NOTE: As you probably guessed, the "log4Debug" statements are just logging. Incidentally, I'm using Lumberjack for that, but I prefer the command syntax from the old Log4Cocoa.
After the scrollViewDidEndDecelerating: and scrollViewDidEndDragging:willDecelerate: (the last one just when the will decelerate parameter is NO) you should set the contentOffset parameter of your UIScrollView to the desired position.
You also will know the current position by checking the contentOffset property of your scrollview, and then calculate the closest desired region that you have
Although you don't have to create your own scrolling control, you will have to manually scroll to the desired positions
To add to what felipe said, i've recently created a table view that snaps to cells in a similar way the UIPicker does.
A clever scrollview delegate is definitely the way to do this (and you can also do that on a uitableview, since it's just a subclass of uiscrollview).
I had this done by, once the the scroll view started decelerating (ie after scrollViewDidEndDragging:willDecelerate: is called), responding to scrollViewDidScroll: and computing the diff with the previous scroll event.
When the diff is less than say a 2 to 5 of pixels, i check for the nearest cell, then wait until that cell has been passed by a few pixels, then scroll back in the other direction with setContentOffset:animated:.
That creates a little bounce effect that is very nice for user experience, as it gives a good feedback on the snapping.
You'll have to be clever and not do anything when the table is bouncing at the top or bottom (comparing the offset to 0 or the content size will tell you that).
It works pretty well in my case because the cells are small (about 80-100px high), you might run into problems if you have a regular scroll view with bigger content areas.
Of course, you will not always decelerate past a cell, so in this case i just scroll to the nearest cell, and the animation looks jerky. Turns out with the right tuning, it barely ever happens, so i'm cool with this.
Spend a few hours tuning the actual values depending on your specific screen and you can get something decent.
I've also tried the naive approach, calling setContentOffset:animated: on scrollViewDidEndDecelerating: but it creates a really weird animation (or just plain confusing jump if you don't animate), that gets worse the lower the deceleration rate is (you'll be jumping from a slow movement to a much faster one).
So to answer the question:
- no, there is no easy way to do this, it'll take some time polishing the actual values of the previous algorithm, which might not work at all on your screen,
- don't try to create your own scroll view, you'll just waste time and badly reinvent a beautiful piece of engineering apple created with truck loads of bug. The scrollview delegate is the key to your problem.
Try something like this:
- (void) snapScroll;
{
int temp = (theScrollView.contentOffset.x+halfOfASubviewsWidth) / widthOfSubview;
theScrollView.contentOffset = CGPointMake(temp*widthOfSubview , 0);
}
- (void) scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
{
if (!decelerate) {
[self snapScroll];
}
}
- (void) scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
[self snapScroll];
}
This takes advantage of the int's drop of the post-decimal digits. Also assumes all your views are lined up from 0,0 and only the contentOffset is what makes it show up in different areas.
Note: hook up the delegate and this works perfectly fine. You're getting a modified version - mine just has the actual constants lol. I renamed the variables so you can read it easy
Related
I know that in a view-based table view, the row class NSTableCellView subclasses from NSView. This class contains two properties, an NSTextField and an NSImageView. I am only using the NSTextField without an image view. However, some cells in my table view must contain multiple lines of text, while others may only contain one or two lines. I need to be able to resize individual NSTableCellView views depending on the size of their NSTextField textField property.
Therefore, I needed to do the following:
Get the frameSize of the NSTextField in the table cell view.
Set the frameSize of the NSTableCellView to the frameSize of the NSTextField (the one we got in set one)
However, this approach hasn't been working. I have begun to think that my approach to resize the NSTableCellView is incorrect. Here is the code that I have been using:
[tableCellView setFrameSize:[[tableCellView textField] frame].size];
[tableCellView setNeedsDisplay:YES];
Is there a problem with this approach? I would expect the cell to resize, but it doesn't? What is going wrong?
Thanks.
[edit] I should have started by commenting that the size of the textField has little to do with how large it would need to be to display all of its content.
I use this code to determine the height of a string based on the width of a table cell:
- (CGFloat) displayStringHeightWithWidth:(CGFloat)width
{
CGSize size = NSMakeSize(width,0);
NSRect bounds = [self.displayString boundingRectWithSize:size
options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading];
return bounds.size.height;
}
Ideally you can adapt that to finding the height of the textField.stringValue or textField.attributedStringValue. Not that the above is also from OSX, not iOS, so YMMV on some of the fluff.
So that changes your algorithm to:
Get the width of the table column
Get the height of the required bounding rect for the textfield's text
Tell the tableView that the row height is whatever you found in 2
Now. Regarding #3. I believe that you have to use the tableView:heightOfRow: in NSTableViewDelegate protocol as well as call the table's noteHeightOfRowsWithIndexesChanged: to have row heights change. The tableView's not otherwise aware that the height of your cell has changed. Note the discussion in the documentation. It could be your method would work without the delegate and just telling the table that the row heights for the rows that you are changing are dirty... but I wouldn't really expect it.
In my application i am using CABasicAnimation for animation. I want to change the speed of the animation dynamically so i have added one slider to change the speed. Following is my animation code. But i am not able to change the speed, when i change the value of speed nothing happens.
CABasicAnimation * a = [CABasicAnimation animationWithKeyPath:#"position"];
[a setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]];
CGPoint startPt = CGPointMake(self.view.bounds.size.width + displayLabel.bounds.size.width / 2,
displayLabel.frame.origin.y);
CGPoint endPt = CGPointMake(displayLabel.bounds.size.width / -2, displayLabel.frame.origin.y);
[a setFromValue:[NSValue valueWithCGPoint:startPt]];
[a setToValue:[NSValue valueWithCGPoint:endPt]];
[a setAutoreverses:NO];
[a setDuration:speeds];
[a setRepeatCount:HUGE_VAL];
[displayLabel.layer addAnimation:a forKey:#"rotationAnimation"];
- (IBAction)speedSlider:(id)sender {
speeds = slider.value;
}
I think the best way to change speed is change your layer's time system
displayLabel.layer.timeOffset =
[displayLabel.layer convertTime:CACurrentMediaTime() fromLayer:nil];
displayLabel.layer.beginTime = CACurrentMediaTime();
displayLabel.layer.speed= slider.value;
You can see this for advance. https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CoreAnimation_guide/AdvancedAnimationTricks/AdvancedAnimationTricks.html#//apple_ref/doc/uid/TP40004514-CH8-SW2
EDIT: It looks like you will have a further problem, though: it doesn't look like you can change values like that on a running animation. You will have to remove the current animation and add a new one with the altered value. This may need a bit of care to prevent a jarring effect when you add the new animation.
From the thread above, you might be able to do this by not repeating the animation, but by using a delegate (see here) to keep re-adding the animation, and setting the new speed for the next animation cycle instead.
Original post:
You are changing the value that you had originally passed in to the animation. This isn't going to affect the running animation. You'll need to get a reference to that, and change the duration property of the animation object. Something like this in your action method:
CABasicAnimation *a = [displayLabel.layer animationForKey:#"rotationAnimation"];
a.duration = slider.value;
I think jrturton is correct that you can't change properties on an animation that is already running. But what you could do is break the animation into short segments and change the speed for the next segment when the slider value changes.
Instead of animating from point A to point D, you'd animate from A-B, then B-C, then C-D. Use the parent class's animationDidStop to check the current point, check the slider value, and kick off the next animation.
This might produce jerky motion, but if you use very small segments, you might be able to smooth it out.
u should stop the animation and restart a new with a new duration time
but remember to log down the fromValue and the toValue , and use the old toValue as the new fromValue to perform a seamless change
set speed as what you need.
a.duration=0.5;
Try this...
If you just want Autoscrolling text then you can also use one class
http://blog.stormyprods.com/2009/10/simple-scrolling-uilabel-for-iphone.html
It might also work in your case, try it.
I have a small question, I have a code to generate 200 UIImages in random positions and rotations.
I need to detect the touch event on these images, but I need to check if the touched UIImage is not covered by any other image (even if they intersect in a small area).
Can anybody help me on this?
BTW : I'm trying to do something similar to this game : http://www.dressup247.com/game/1014/Bank-Note-Stack.html
I would suggest to make all images as UIButton's with background image and set for all buttons one action. One more advise - set tag order for every button from most low-lying to most high-lying (and store biggest tag in some iVar). You can do it when layout is generated. It allows you to detect how many views lay over tapped view.
Use CGRectIntersectsRect intersecting views.
-(IBAction)banknotTapped:(UIButton*)sender{
int tag = sender.tag
NSMutableArray *highestViews = [NSMutableArray array];
for (int i=sender.tag; i < biggestTag; ++i){
UIView *v = [self.view viewWithTag:i];
if (CGRectIntersectsRect(sender.frame, v.frame) ) // it allows to find intersecting views
[highestViews addObject:v];
}
}
Now highestViews will contain all highest views that intersect with sender.
Note: tags allow you to detect views, but if you will delete images from superview then it can lead to problems since tag order will broken. Hide views instead of deleting or follow #Costique method in order to determine views order.
You can use the following snippet to determine the order of a subview in its parent view:
NSUInteger order = [containerView.subviews indexOfObjectIdenticalTo: subview];
The less the order, the "higher" the subview is. The topmost subview will have zero order.
While I'd suggest that it would be faster to do this mathematically, defining the banknotes as rectangles and testing for overlap using something like the separating axes theorem, that's obviously not what you're asking and not suitable for general case images.
So I'd suggest that you create a CGBitmapContext the same size as your play area and that when seeding your play area, for each note you place you do something like:
assign new, as yet unused colour to the bank note
draw it to the bitmap context at its destination position, but as a solid object of the assigned colour — so you preserve the outline of the original shape but at each pixel you draw either the solid, assigned colour or no colour at all
count how many pixels in the entire context are now the assigned colour, store that with your object representing the note
Subsequently, when the game starts run through the buffer and count how many of each assigned colour. All notes that have the same number stored as are currently visible are on top. Whenever a note is removed, redraw all the others in order and do the colour count once.
Note that you're not doing the colour count on the total buffer once per note, just once in total. So it's a fixed cost and probably occurs less often than once per tap.
Probably the easiest way to do the drawing as a single colour is to create a mask version of each graphic when it's loaded and then to draw that with a suitable tint. There's an introduction to alpha masks here; you'll probably want to create a custom bitmap image context rather than using the implicit one returned by UIGraphicsGetCurrentContext and to post filter to test the output alpha — pushing down to 0 if its less than some threshold, up to 255 otherwise.
I'm trying to create a UITableView with dates, shouldn't be very exciting, I know. It starts around the current date, but the user should be able to scroll down (the future) and up (the past) as far as he/she wants. This results in a potentially infinite number of rows. So what's the way to go about creating this?
Returning NSIntegerMax as the number of rows already crashes the app, but even if it wouldn't, that still doesn't account for being able to scroll up. I could start half way of course, but eventually there's a maximum.
Any ideas how to do or fake this? Can I update/reload the table somehow without the user noticing, so I never run into a border?
SOLUTION:
I went with #ender's suggestion and made a table with a fixed amount of cells. But instead of reloading it when the user scrolls to near the edges of the fixed cells, I went with reloading the table when the scrolling grinds to a halt. To accomodate with a user scrolling great distances without stopping, I just increased the row count to 1000, putting the ROW_CENTER constant to 500. This is the method that takes care of updating the rows.
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
NSArray *visible = [self.tableView indexPathsForVisibleRows];
NSIndexPath *upper = [visible objectAtIndex:0];
NSIndexPath *lower = [visible lastObject];
// adjust the table to compensate for the up- or downward scrolling
NSInteger upperDiff = ROW_CENTER - upper.row;
NSInteger lowerDiff = lower.row - ROW_CENTER;
// the greater difference marks the direction we need to compensate
NSInteger magnitude = (lowerDiff > upperDiff) ? lowerDiff : -upperDiff;
self.offset += magnitude;
CGFloat height = [self tableView:self.tableView heightForRowAtIndexPath:lower];
CGPoint current = self.tableView.contentOffset;
current.y -= magnitude * height;
[self.tableView setContentOffset:current animated:NO];
NSIndexPath *selection = [self.tableView indexPathForSelectedRow];
[self.tableView reloadData];
if (selection)
{
// reselect a prior selected cell after the reload.
selection = [NSIndexPath indexPathForRow:selection.row - magnitude inSection:selection.section];
[self.tableView selectRowAtIndexPath:selection animated:NO scrollPosition:UITableViewScrollPositionNone];
}
}
The magic breaks when a user scrolls to the edge of the table without stopping, but with the table view bounces property disabled, this merely feels like a minor glitch, yet totally acceptable. As always, thanks StackOverflow!
You should establish a fixed number of cells and adjust your datasource when the user scrolls near the end of the tableview. For example, you have an array with 51 dates (today, 25 future and 25 past). When the app tries to render a cell near one of the borders, reconfigure your array and call reloadData
You might also have a look at the "Advanced Scroll View Techniques" talk of the WWDC 2011. They showed how you would create a UIScrollView which scrolls indefinitely. It starts at about 5 mins. in.
Two thoughts:
Do you really need a UITableView? You could use a UIScrollView, three screens high. If end of scrolling is reached, layout your content and adjust scrolling position. This gives the illusion of infinite scrolling. Creating some date labels and arranging them in layoutSubviews: should not be too much of an effort.
If you really want to stick to UITableView you could think about having two UITableViews. If scrolling in your first one reaches a critical point, spin off a thread and populate the second one. Then at some point, exchange the views and trigger the scrolling manually so that the user does not note the change. This is just some idea from the top of my head. I have not implemented something like this yet, but I implemented the infinite UIScrollView.
I answered this question in another post: https://stackoverflow.com/a/15305937/2030823
Include:
https://github.com/samvermette/SVPullToRefresh
SVPullToRefresh handles the logic when UITableView reaches the bottom. A spinner is shown automatically and a callback block is fired. You add in your business logic to the callback block.
#import "UIScrollView+SVInfiniteScrolling.h"
// ...
[tableView addInfiniteScrollingWithActionHandler:^{
// append data to data source, insert new cells at the end of table view
// call [tableView.infiniteScrollingView stopAnimating] when done
}];
This question has already been asked: implementing a cyclic UITableView
I'm copying that answer here to make it easier because the asker hasn't ticked my answer.
UITableView is same as UIScrollView in scrollViewDidScroll method.
So, its easy to emulate infinite scrolling.
double the array so that head and tail are joined together to emulate circular table
use my following code to make user switch between 1st part of doubled table and 2nd part of doubled table when they tend to reach the start or the end of the table.
:
/* To emulate infinite scrolling...
The table data was doubled to join the head and tail: (suppose table had 1,2,3,4)
1 2 3 4|1 2 3 4 (actual data doubled)
---------------
1 2 3 4 5 6 7 8 (visualizing joined table in eight parts)
When the user scrolls backwards to 1/8th of the joined table, user is actually at the 1/4th of actual data, so we scroll instantly (we take user) to the 5/8th of the joined table where the cells are exactly the same.
Similarly, when user scrolls to 6/8th of the table, we will scroll back to 2/8th where the cells are same. (I'm using 6/8th when 7/8th sound more logical because 6/8th is good for small tables.)
In simple words, when user reaches 1/4th of the first half of table, we scroll to 1/4th of the second half, when he reaches 2/4th of the second half of table, we scroll to the 2/4 of first half. This is done simply by subtracting OR adding half the length of the new/joined table.
Written and posted by Anup Kattel. Feel free to use this code. Please keep these comments if you don't mind.
*/
-(void)scrollViewDidScroll:(UIScrollView *)scrollView_
{
CGFloat currentOffsetX = scrollView_.contentOffset.x;
CGFloat currentOffSetY = scrollView_.contentOffset.y;
CGFloat contentHeight = scrollView_.contentSize.height;
if (currentOffSetY < (contentHeight / 8.0)) {
scrollView_.contentOffset = CGPointMake(currentOffsetX,(currentOffSetY + (contentHeight/2)));
}
if (currentOffSetY > ((contentHeight * 6)/ 8.0)) {
scrollView_.contentOffset = CGPointMake(currentOffsetX,(currentOffSetY - (contentHeight/2)));
}
}
P.S. - I've used this code on one of my apps called NT Time Table (Lite). If you want the preview, you can check out the app: https://itunes.apple.com/au/app/nt-time-table-lite/id528213278?mt=8
If your table can sometimes be too short, at the beginning of the above method you can add a if logic to exit when data count is say for example less than 9.
I have a tableview. One of the columns in the tableview uses an NSLevelIndicatorCell.
I want to be able to allow the user to edit the warn and critical values for the level indicator such that when they enter a value into a a "warning level" textbox, it changes the warn value of the level indicators being displayed in ALL of the tableview's rows.
I am very much a newbie with Objective-C so all I can figure out so far is that I must need a delegate method to watch the textbox BUT if I succeed in doing that, how on earth do I send the new value to the particular tableview column so that the update happens to ALL of the rows (i.e. how do I send what message to the tableview and target a cell within a column within a tableview)?
Here is the code to the solution I came up with should anyone need it.
- (IBAction)setWarningLevel:(id)sender {
double v;
NSScanner *ns = [NSScanner scannerWithString:[warnLevel stringValue]];
[ns scanDouble:&v];
[levelIndicator setWarningValue:v];
}
This is a textbook case for using Cocoa bindings. Just bind the value of the text field to the NSLevelIndicatorCell in your table view (do that in Interface Builder). The updates should happen automagically.
I think it should apply for all the cells in the table view if you apply the binding to the cell in IB. However if it doesn't, you will need to write a couple lines of code that set up the binding every time a new row in the table is created. That link above will explain everything in detail, but basically you will be setting up a Key-Value Observer relationship in code between the text field and the instance of the level indicator in the row being created.
I think you may have overdone it.
NSTextField subclasses NSControl, so you need to look in the docs for NSControl for a useful function.
Try re-writing it like this; assuming you're taking the value from a warnLevel textfield.
- (IBAction)setWarningLevel:(id)sender {
double v = [warnLevel doubleValue];
[levelIndicator setWarningValue:v];
}
Although this is usually shortened to this;
- (IBAction)setWarningLevel:(id)sender {
[levelIndicator setWarningValue:[warnLevel doubleValue]];
}
You should probably have some validation that the textfield has a valid number. If you're only choosing a couple of numbers have a look at using a stepper control.
Usually, with Cocoa, if you feel like you're jumping through too many hoops, there is sometimes an easier way.
Usually ;-)