I've written a custom layout for a collection view. Part of the collection view involves inserting new items at the top with an animation beginning from offscreen. In order to do this I'm using the usual initialLayouts method. I'm able to animate the object in correctly but there seems to be a strange problem that I can't work out.
The cell that I'm animating in appears immediately in it's final position and then a second cell animates into place from the expected offscreen position, disappearing once the animation is complete. All of my other animations for deletion, bounds change etc are fine, so I don't believe my layout caches are wrong and normal layout is perfect. Potentially this is a cell reuse issue?
I've created a quick video demoing the problem (inserts start from 7 seconds) http://cl.ly/WMei
Has anyone seen this behaviour before and could point me in the right direction?
Unfortunately I can't share the whole layout class however here are my layoutAttributes methods and the initial insert attributes, I can try and give more info where asked for. I appreciate this is a hard one to debug so thanks a lot for taking the time to check it out :).
Here are the attributes I apply for top inserts:
MCLTXGridLayoutAttributes *att = [(MCLTXGridLayoutAttributes*)[self.dataStructure itemAtIndexPath:itemIndexPath] copy];
att.alpha = 1.0;
CGRect newFrame = att.frame;
newFrame.origin.y = self.collectionView.bounds.origin.y - att.totalHeight;
att.frame = newFrame;
return att;
and the layout attributes method:
- (UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
MCLTXGridLayoutAttributes *att = [MCLTXGridLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
CGFloat randomTopPadding = (arc4random() % kMaxVerticalSpacing);
randomTopPadding += kMinVerticalSpacing;
NSInteger randomExtraPadding = (NSInteger)arc4random() % kOneInXChanceOfRecievingExtraPadding;
att.topPadding = randomTopPadding + (randomExtraPadding == 1 ? kItemRandomExtraPadding : 0);
NSUInteger column = [self.dataStructure indexForShortestColumn];
att.columnIndex = column;
CGFloat y = MAX(self.tmpCachedHeight, [self.dataStructure heightForColumnWithIndex:column] + kMinItemSpacing) + att.topPadding;
CGFloat height = [self.datasource collectionView:self.collectionView heightForItemAtIndex:indexPath];
CGRect frame = CGRectMake([self xOriginForColumn:column], y, self.cachedColumnWidth, height);
att.frame = frame;
att.layerPriority = [self.datasource collectionView:self.collectionView layerPriorityForItemAtIndexPath:indexPath];
att.zIndex = att.layerPriority * -10;
att.transform3D = CATransform3DIdentity;
att.transform = CGAffineTransformIdentity;
return att;
}
I did eventually figure this out. We were using Magical Record and the problem came with the saving.
With my testing I was creating fake objects and saving using a background thread as you should do with MR. The problem was in our networking code that completely by accident was calling a write + save on the main thread context. This was a very small write but it was enough to upset the rendering and cause the visual ghosting effect.
If you're seeing this issue and you're using something like Magical Record or multithreaded core data along with fetched results controllers, then double check your save calls to make sure you aren't accidentally hitting the main thread.
Related
I've got a huge legacy project on objective-c and trying to implement new features before we starting to move it to swift.
So I'm working on chat right now and creating "Search" like in telegram.
After we got 200status from server we got 41 message in chat. Im set user screen to the middle of this messages, and need to implement pagination in both sides.
I've successfully added both features, and need to resolve last problem:
When user scroll UP or down to the another messages, I've got check in scrollViewDidScroll:
CGPoint offset = scrollView.contentOffset;
CGSize size = scrollView.contentSize;
CGFloat height = _tableView.frame.size.height;
if (offset.y > size.height - height) {
if((isLoadingMessages == NO) && (loadedMessagesUp % MAX_LOADING_MESS == 0)){
_isFromSearch = false;
NSInteger create = [[[_messagesArray lastObject] objectForKey:#"create"] integerValue];
_dateForPagination = create;
scrollDirectionDown = false;
[self loadMessages];
NSLog(#"pagination up");
}
}
if(offset.y < 10) {
if((isLoadingMessages == NO) && (loadedMessagesDown % MAX_LOADING_MESS == 0)){
_isFromSearch = false;
NSInteger create = [[[_messagesArray firstObject] objectForKey:#"create"] integerValue];
_dateForPagination = create;
scrollDirectionDown = true;
[self loadMessages];
NSLog(#"pagination down");
}
}
So, the question is:
When I've scrolledUP, my [_tableVIew contentOffset] calculated automatically (or I can't find where it happened, but I've check all project with cmd+f).
But when user scrollDOWN, to the new messages, table view added messages with method:
if (scrollDirectionDown) {
_paginationMessagesArray = [[[_paginationMessagesArray reverseObjectEnumerator]allObjects]mutableCopy];
[_paginationMessagesArray addObjectsFromArray: _messagesArray];
_messagesArray = _paginationMessagesArray;
}
And after this dropped my tableview to the [tableView offset] = 0 (equal indexPath.row = 0). So this automatically calls method with pagination and again dropped page to first element.
I think that I need to set new contentOffset after [tableview reloadData]. But I can't calculate it.
Can somebody help: how to calculate offset from to tableView minY to the tableView currentY ? Or I do everything wrong?
I suppose that I can't find some method in this terrible project, but I hope you can help to me with this situation.
in this picture I need to calculate offset from bottom of black square to the button of blue square. Because after [tableView reloadData] black square (which is screen of the phone) immediately drops to the bottom of blue Screen and turning pagination again. And it repeating and repeating..
THanks a lot, mates!
I found answer by changing my search in google :D 3 days I've spent on this.
UIScrollView disable vertical bounce only at bottom
I'd like to implement a "zoom" effect on a paging UIScrollView that I've created, but I am having a lot of difficulty. My goal is that as a user begins to scroll to the next page, the current page zooms out to become a little bit smaller. As the next page comes into view, it zooms in until it becomes its full size. The closest thing I could find to an example was this...
https://www.pinterest.com/pin/147141112804210631/
Can anyone give me some pointers on how to accomplish this? I've been banging my head against a wall for the last 3 days on this.
I would recommend using the scrollView.contentOffset.y of your paginated UIScrollView to keep track of the scroll and to use that value to animate the transform of your views inside the UIScrollView.
So add your paginated scrollview and make self as delegate.
paginatedScrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, [[self view] bounds].size.width, [[self view] bounds].size.height-paginatedScrollViewYOffset)];
[self.view addSubview:paginatedScrollView];
paginatedScrollView.pagingEnabled = YES;
[paginatedScrollView setShowsVerticalScrollIndicator:NO];
[paginatedScrollView setShowsHorizontalScrollIndicator:NO];
[paginatedScrollView setAlwaysBounceHorizontal:NO];
[paginatedScrollView setAlwaysBounceVertical:YES];
paginatedScrollView.backgroundColor = [UIColor clearColor];
paginatedScrollView.contentSize = CGSizeMake([[self view] bounds].size.width, [[self view] bounds].size.height*2); //this must be the appropriate size depending of the number of pages you want to scroll
paginatedScrollView.delegate = self;
Then use the delegate method scrollViewDidScroll to keep track of the scrollView.contentOffset.y
- (void) scrollViewDidScroll:(UIScrollView *)scrollView {
NSLog(#"Scroll Content Offset Y: %f",scrollView.contentOffset.y);
//use here scrollView.contentOffset.y as multiplier with view.transform = CGAffineTransformMakeScale(0,0) or with view.frame to animate the zoom effect
}
Use this Code scrollview its zoom in when scroll next page, the code is given below,
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
GridCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"CollectCell" forIndexPath:indexPath];
cell.myscrollview.minimumZoomScale = 5.0;
cell.myscrollview.zoomScale = 5.0;
cell.myscrollview.contentSize = cell.contentView.bounds.size;
return cell;
}
if you change the zoom scale value its automatically zoom in or zoom out to be showed when scroll next or previous page.
hope its helpful.
I actually just posted an answer to a very similar question, where somebody tried to achieve this effect using a UICollectionView. The link to my answer is here: https://stackoverflow.com/a/36710965/3723434
Relevant piece of code I will post here:
So another approach would be to to set a CGAffineTransformMakeScale( , ) in the UIScrollViewDidScroll where you dynamically update the pages' size based on their distance from the center of the screen.
For every page, calculate the distance of its center to the center of yourScrollView
The center of yourScrollView can be found using this nifty method: CGPoint point = [self.view convertPoint:yourScrollView.center toView:*yourScrollView];
Now set up a rule, that if the page's center is further than x away, the size of the page is for example the 'normal size', call it 1. and the closer it gets to the center, the closer it gets to twice the normal size, 2.
then you can use the following if/else idea:
if (distance > x) {
page.transform = CGAffineTransformMakeScale(1.0f, 1.0f);
} else if (distance <= x) {
float scale = MIN(distance/x) * 2.0f;
page.transform = CGAffineTransformMakeScale(scale, scale);
}
What happens is that the page's size will exactly follow your touch. Let me know if you have any more questions as I'm writing most of this out of the top of my head).
I've done some work on stylized app guide page before.
For Me, I would use CADisplayLink to track the contentOffset.x of the scrollView, associate the value with your animation process. Don't put your views on the scrollView, put them on an overlay view of this scrollView.
This solution follows the philosophy: Fake it before you make it.
Based on CADisplayLink and physics simulation of UIScrollView, you will get smooth animation. Believe me.
What you really want isn't a UIScrollView, it's a UICollectionView with a custom layout. UICollectionViewLayoutAttributes has a transform property that you can set.
Say for example, in layoutAttributesForElementsInRect::
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let attributes = super.layoutAttributesForElementsInRect(rect) else {
return nil
}
return attributes.map { attribute -> UICollectionViewLayoutAttributes in
if attribute.frame.origin.y < 0 {
let scale = -attribute.frame.origin.y / attribute.frame.height
attribute.transform = CGAffineTransformMakeScale(scale, scale)
}
return attribute
}
}
Here, you're filtering by if the element is on the screen (so non-visible elements won't be counted) and checking to see if the y offset is less than 0. If it is, you take the difference between the negated y value and the item's height and turn that into a proportional scale.
You can do it however you want, if you want the scale to be between 1 and 0.5 for example. I like this way of doing things over mucking around with a scroll view.
I am fairly new to programming in general, and have been following the CS193p videos on iTunesU. I am currently doing assignment 3, and am having trouble getting a bit of information from the View sent to the View Controller.
I believe I have set up the whole delegation thing correctly, so the question really is to how to get my View Controller to see a bit of information (such as self.bounds.size.width), which is a property that only the View has. Would this involve using self.dataSource? And if so, through what means could I pass this bit of information? My end goal is to have the View Controller perform some transformation to the View's properties, and send it back to the View's drawRect so that it could be drawn.
Thanks!!
** Edit, as requested, I have posted parts of my drawRect code below
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGFloat scale = 32;
CGPoint midPoint;
midPoint.x = self.bounds.origin.x + self.bounds.size.width/2;
midPoint.y = self.bounds.origin.y + self.bounds.size.height/2;
// ---- finding the y starting point
float totalXsInWidth;
totalXsInWidth = self.bounds.size.width / scale;
float leftMostX = totalXsInWidth / -2;
float graphResultY = sin(leftMostX); // ** in theory, I want "leftMostX" to be modifed by the equation entered (in the CONTROLLER)
NSLog(#"The leftMostX is %f", leftMostX);
[self.dataSource passingVariable:leftMostX]; //** Here I pass the variable from drawRect to get modifyed in the CONTROLLER
float graphResultY1;
graphResultY1 = 5; //this is a test, I want to see if the controller actually effect a change
graphResultY1 = [self.dataSource calcResult:self]; //this should now be a different number than 5 or 0 (the init value)
NSLog(#"From graphingview, the result is %f", graphResultY1); //** unfortunately = 0... :(
Having had a read of CS193p Assignment 3 (for anyone interested, it is available here in PDF format), it looks like you are being asked to create a protocol for your UIView subclass, and have the delegate of that protocol (the view's managing view controller) provide the data used for drawing.
If you have set up your protocol correctly, the view's drawRect method should be asking the view controller for data through the protocol's method, something like:
DataObject *data = [self.delegate getData];
// It might be [self.dataSource getData]; in your case
That should call the getData delegate method that you should have written into the view controller (I have made up a method signature for this, adapt to the one used by your protocol. Also, this code doesn't consider relevant memory management, if required):
- (DataObject *)getData
{
// Get data from the model, and return
DataObject *dataObjectToReturn = [Model getRelevantData];
return dataObjectToReturn;
}
The view's drawRect should now have the relevant instance of DataObject and can go about using that data to draw what it needs to draw.
Below is my original answer, which is not relevant to the specific problem above, but does show another method of view/view controller interaction. (This method isn't applicable to the above problem because the data needed for drawing shouldn't be owned by the view itself.)
To access the view's properties from that view's controller you would use self.view. Thus, to get the view's width, like in your example, you would use:
CGFloat viewWidth = self.view.bounds.size.width;
You can also set the controller's view, though note that for your example you have to supply an entire CGRect for the frame, as manipulating the width directly is not allowed:
// This gets a reference to the view's frame, then uses values from it to create a new
// CGRect that is 10.0 points wider, and sets the frame of the view to the new frame
CGRect viewFrame = self.view.frame;
self.view.frame = CGRectMake(viewFrame.origin.x, viewFrame.origin.y, viewFrame.size.width + 10.0f, viewFrame.size.height);
Im not famaliar with the assignment but if you are using a view within a view controller you can access that views property like so:
SomeView *someView = [SomeView new];
CGFloat width = someView.bounds.size.width;
I'm working on a game right now, in which every second, I want to create x number of new UIImage objects that begin at the top of the screen. After they have been instantiated, I want them automatically to fall down until they reach the bottom of the screen, at which point I no longer have any use for them.
Its almost like raindrops - X number of them are created every second, and they each fall down at different speeds.
I'm getting really confused as to how I would even just design my program to do this.
Any help would be greatly appreciated. Thanks.
Are they all copies of the same image, or at least copies of one among a limited set of images?
For such a game-like, graphics intensive app I would seriously consider using OpenGL ES, although the learning curve is steep for people familiar with UIKit only. Fortunately, there are third party, open-source libraries such as Cocos2d that make efficient 2d graphics almost as easy to code as UIKit.
Regarding your question in particular, I haven't watched the video mentioned by #ctrahey, but I can think of these patterns:
Have a (finite) 'pool' of reusable objects, which size is equal to the maximum amount of instances that might appear on screen at any given time. You definitely want to set this limit, since graphics performance is not infinite. Each time an object falls off-screen, 'reset' its state and reuse it (from the top, again). UITableView does something like this with table cells.
Create the instances on demand, and destroy them (release->dealloc) once they go off-screen.
You have to balance the runtime cost of creating/destroying instances vs. the cost/inconvenience of resetting objects.
Hope it helps
Step 1: Run a piece of code every second
timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(dispatchSomeRaindrops) userInfo:nil repeats:YES];
Step 2: Create some particles, send them down the screen, and clean them up when they reach the bottom.
- (void)dispatchSomeRaindrops
{
for (int i = 0; i < 5; i++) {
UIImageView *view = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"safari.png"]];
CGFloat halfHeight = view.frame.size.height / 2;
CGFloat x = arc4random() % (int)self.view.frame.size.width;
view.center = CGPointMake(x, -halfHeight);
[self.view addSubview:view];
NSTimeInterval duration = 10 + arc4random() % 10;
[UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
float endY = self.view.frame.size.height + halfHeight;
view.center = CGPointMake(x, endY);
} completion:^(BOOL finished) {
[view removeFromSuperview];
}];
}
}
Check out the Core Animation video from WWDC 2011, and near the end there is a bit on "Replicators", and it sounds like exactly what you are after.
FYI: found a terrific resource to handle this exact scenario. It does require you to use cocos2d, but it explains it in a very clear and understandable manner.
http://www.raywenderlich.com/352/how-to-make-a-simple-iphone-game-with-cocos2d-tutorial
If I want to animate UITableViewCell so it would bounce from left to right a few times, How can I do that? I'm trying that:
var bounds = activeCell.Bounds;
var originalLocation = bounds.Location;
var loc = originalLocation;
UIView.Animate(0.2,()=>{
loc.X = originalLocation.X + 20;
activeCell.Bounds = new RectangleF (loc, bounds.Size);
loc.X = originalLocation.X - 20;
activeCell.Bounds = new RectangleF (loc, bounds.Size);
});
It animates only the last state (i.e. moves element to the left). I tried to put them in separated Animate blocks - it didn't help. Tried to use different UIAnimationOptions - the same.
Here is a nice article explaining how to make it bounce.
http://khanlou.com/2012/01/cakeyframeanimation-make-it-bounce/
Moreover, there is an explanation the formula used to compute the bounce path.
For my personal use, I've taken the absolute value of the computation to simulate a rebound on ground.
- (void) displayNoCommentWithAnimation{
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"position.y"];
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
animation.duration = 2;
int steps = 120;
NSMutableArray *values = [NSMutableArray arrayWithCapacity:steps];
double value = 0;
float e = 2.71;
for (int t = 0; t < steps; t++) {
value = 210 - abs(105 * pow(e, -0.025*t) * cos(0.12*t));
[values addObject:[NSNumber numberWithFloat:value]];
}
animation.values = values;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
animation.delegate = self;
[viewThatNeedToBounce.layer addAnimation:animation forKey:nil];
}
- (void) animationDidStop:(CAAnimation *)animation finished:(BOOL)flag {
CAKeyframeAnimation *keyframeAnimation = (CAKeyframeAnimation*)animation;
[viewThatNeedToBounce.layer setValue:[NSNumber numberWithInt:210] forKeyPath:keyframeAnimation.keyPath];
[viewThatNeedToBounce.layer removeAllAnimations];
}
The problem with your approach is that UIView.Animate will record the changes that you make to your view, but only the final state for them.
If you change the Bounds property one hundred times in your animate block, only the last one is the one that will matter from the perspective of the animation framework.
CoreAnimation has a couple of quirks that are explained in the WWDC 2010 and WWDC2011 videos. They have great material and they explain a few of the tricks that are not very obvious.
That being said, animating cells in a UITableView is a complicated matter because you are really poking at a UITableView internals, so expect various strange side effects. You could lift the code from TweetStation that does that animation and deals with various corner cases. But even TweetStation and the Twitter for iOS app do not manage to be perfect, because you are animating things behind the back of a UIView that is constantly updating and making changes to very same properties you are animating.
From the top of my head, the easiest approach would be to put the animation code into a method and call that recursive as often as you want. Code untested, but it should work or at least give you an idea.
// Repeat 10 times, move 20 right and the left and right etc.
FancyAnim(activeCell, activeCell.Bounds.Location, 10, 20);
private void FancyAnim(UITableViewCell activeCell, PointF originalLocation, int repeat, float offset)
{
var bounds = activeCell.Bounds;
var loc = originalLocation;
UIView.Animate(0.2,
delegate
{
// Called when animation starts.
loc.X = originalLocation.X + offset;
activeCell.Bounds = new RectangleF (loc, bounds.Size);
},
delegate
{
// Called when animation ends.
repeat--;
// Call the animation method again but invert the movement.
// If you don't do this too often, you should not run out of memory because of a stack overflow.
if(repeat >= 0)
{
FancyAnim(activeCell, originalLocation, repeat, -offset);
}
});
You can however also use a path animation. You would define a path "20 units right, back to center, 20 units left, back to center" and repeat that animation as often as you like.
This requires you to deal with CAKeyFrameAnimation and will be slightly more code.
This site can get you jump started: http://www.bdunagan.com/2009/04/26/core-animation-on-the-iphone/
Lack of documentation and good samples sometimes really makes even simple tasks so annoyingly challenging.
Here is the solution
Sure code isn't elegant, but it works. Hope it will someday help somebody else, so he or she wouldn't need to spend half a day on something stupidly simple like that
var activeCell = ((Element)sender).GetActiveCell();
var animation =
(CAKeyFrameAnimation)CAKeyFrameAnimation.FromKeyPath ("transform.translation.x");
animation.Duration = 0.3;
animation.TimingFunction = // small details matter :)
CAMediaTimingFunction.FromName(CAMediaTimingFunction.EaseOut.ToString());
animation.Values = new NSObject[]{
NSObject.FromObject (20),
NSObject.FromObject (-20),
NSObject.FromObject (10),
NSObject.FromObject (-10),
NSObject.FromObject (15),
NSObject.FromObject (-15),
};
activeCell.Layer.AddAnimation (animation,"bounce");