UISnapBehavior Causing Distortion - ios

For whatever reason, whenever I use the snapping behavior, it causes the UIButtons to turn into rectangles of various sizes when "snapped". I'm probably just looking right over the issue. My deadline is tomorrow to get the last bugs worked out, so I would GREATLY appreciate ANY help! Thank you!
vvv - ViewController.m - vvv
//set points
CGPoint bullyboxPoint = {80, 188};
CGPoint weemPoint = {240, 188};
CGPoint yeaPoint = {80, 339};
CGPoint charityPoint = {240, 339};
CGPoint phhsPoint = {80, 488};
CGPoint experiencePoint = {240, 488};
//make the buttons closer to speed up the snap
bullybox.frame = CGRectMake(40, 788, 120, 120);
weem.frame = CGRectMake(200, 788, 120, 120);
yea.frame = CGRectMake(30, 939, 120, 120);
charity.frame = CGRectMake(190, 939, 120, 120);
phhs.frame = CGRectMake(20, 1328, 120, 120);
experience.frame = CGRectMake(180, 1328, 120, 120);
//create behaviors
UISnapBehavior *snapBehavior = [[UISnapBehavior alloc] initWithItem:self->bullybox snapToPoint:bullyboxPoint];
UISnapBehavior *snapBehavior1 = [[UISnapBehavior alloc] initWithItem:self->weem snapToPoint:weemPoint];
UISnapBehavior *snapBehavior2 = [[UISnapBehavior alloc] initWithItem:self->yea snapToPoint:yeaPoint];
UISnapBehavior *snapBehavior3 = [[UISnapBehavior alloc] initWithItem:self->charity snapToPoint:charityPoint];
UISnapBehavior *snapBehavior4 = [[UISnapBehavior alloc] initWithItem:self->phhs snapToPoint:phhsPoint];
UISnapBehavior *snapBehavior5 = [[UISnapBehavior alloc] initWithItem:self->experience snapToPoint:experiencePoint];
//dampen the snap
snapBehavior.damping = 1.2f;
snapBehavior1.damping = 1.2f;
snapBehavior2.damping = 1.2f;
snapBehavior3.damping = 1.2f;
snapBehavior4.damping = 1.2f;
snapBehavior5.damping = 1.2f;
//add behaviors
[self.animator addBehavior:snapBehavior];
[self.animator addBehavior:snapBehavior1];
[self.animator addBehavior:snapBehavior2];
[self.animator addBehavior:snapBehavior3];
[self.animator addBehavior:snapBehavior4];
[self.animator addBehavior:snapBehavior5];

The problem is probably that you're using Autolayout. Autolayout and UIKit Dynamics are enemies (if you look at Apple's example code, you'll see that they have circumvented this issue by turning Autolayout off).
The way I like to solve this problem is: Make snapshot images of the objects to be animated; hide the actual objects and put the snapshot images in their place; animate the snapshot images; when it's all over, take the snapshot images out of the interface and show the actual objects in their new location.
This works because the snapshot images are not subject to Autolayout. However, you will still need to grapple with the issue, because you cannot move buttons that are subject to Autolayout by setting their frame; you must set their constraints.

Related

IOS/Objective-C: Subview of Subview not displaying

I am trying to create a chart where a bar in the form of a UIView displays on top of a background UIView. I'd like both to display on top of the UIView for the whole screen. I have done this before successfully, but while I can get the first view to display, I somehow can't get my code to display the bar. Could it have something to do with setting the color? Or can anyone suggest why the second subview is not displaying.
My code:
//Make background box:
CGRect screenRect = [[UIScreen mainScreen] bounds];
CGFloat screenWidth = screenRect.size.width;
CGFloat graphWidth = screenWidth-40;
CGFloat graphHeight = 160;
CGRect graphBounds =CGRectMake(20, 200, graphWidth, graphHeight);
float tableStartY = graphBounds.origin.y;
UIView *graphBox = [[UIView alloc] initWithFrame:graphBounds];
graphBox.backgroundColor = [UIColor colorWithRed:200.0/255.0 green:200.0/255.0 blue:200.0/255.0 alpha:0.2]; graphBox.layer.borderWidth = 1;
graphBox.layer.borderColor = [UIColor blueColor].CGColor;
//Make Bar
CGFloat barWidth = 20;
CGFloat barHeight = 100;
CGRect aBar = CGRectMake(20, tableStartY+1, barWidth, barHeight);
UIView *barView = [[UIView alloc] initWithFrame:aBar];
barView.backgroundColor = [UIColor blueColor];
barView.layer.borderWidth = 1;
barView.layer.borderColor = [UIColor redColor].CGColor;
// [graphBox addSubview:barView];
[self.view addSubview: graphBox];
If I run the above code, it displays the graphBox. If I add the bar directly to the view as a subView instead of the graphBox, the bar displays. However, if I uncomment out the line shown and add the barView first to the graphBox and then add the graphBox to the view, the barView does not display.
Thanks in advance for any suggestions.
If I understand correctly what you need to do, you should replace
CGRect aBar = CGRectMake(20, tableStartY+1, barWidth, barHeight);
with
CGRect aBar = CGRectMake(20, 1, barWidth, barHeight);
[edit: and obviously uncomment the addSubview line]
Perhaps this is an accident in your posted code, but you have specifically commented out where the barView would be added to the screen.
// [graphBox addSubview:barView];
In addition, as another answer lists, your offset is incorrect if you are adding barView to graphBox. If you add it to self.view instead, your offset is correct.
So, you've got two choices, depending on the containment you desire in your view hierarchy:
CGRect aBar = CGRectMake(20, 1, barWidth, barHeight);
// ...
[graphBox addSubview:barView];
or
CGRect aBar = CGRectMake(20, tableStartY+1, barWidth, barHeight);
// ...
[self.view addSubview: graphBox];
[self.view addSubview:barView];
Note that in the second option, the order is important to get the barView to display over top of the graphBox as they will be siblings.

How to find the center of a UIView after superviews have been transformed?

NOTE: code written in browser; probably not perfectly accurate, but it should give the general idea.
I've got a stack of views, something like this:
CGRect theFrame = CGRectMake(0, 0, 10, 10);
UIView *v1 = [UIView alloc] initWithFrame: theFrame];
UIView *v2 = [UIView alloc] initWithFrame: theFrame];
UIView *v3 = [UIView alloc] initWithFrame: theFrame];
// Set all the background colors, so I can see them: snip
// set all the clipsToBounds = NO, so I can place however I want: snip
v2.center = CGPointMake(100, 80);
[v1 addSubview: v2];
v3.center = CGPointMake (57, 42);
[v2 addSubview: v3];
v1.center = CGPointMake (193, 44);
[self.view addSubview: v1];
// etc., time passes, user presses TEST button
CGAffineTransform sXfrm = CGAffineTransformMakeScale(3.5, 4.7);
CGAffineTransform rXfrm = CGAffineTransformMakeRotation(M_PI / 3.);
v1.transform = CGAffineTransformConcat(sXfrm, rXfrm);
// etc., rotate & scale v2, while we're at it. snip
// Leave v3 unrotated & unscaled.
Ok, at this point, everything is displaying on the screen exactly as desired. My question is:
Where (that is: where, in self.view's coordinate space) is v3.center?
Here's some code that gives the CLOSE answer, but not-quite right, and I can't seem to figure out what's wrong with it:
CGRect testRect = CGRectMake(0, 0, 20, 20);
UIView *testView = [[UIView alloc] initWithFrame: testRect];
testView.backgroundColor = [UIColor greebColor];
[self.view addSubview: testView];
CGPoint center = v1.center;
#if 1 // Apply Transform
CGPoint ctr2 = CGPointApplyAffineTransform(v2.center, v1.transform);
#else // use layer
CGPoint ctr2 = CGPointMake ((v2.layer.frame.origin.x + (v2.layer.frame.size.width / 2.)),
(v2.layer.frame.origin.y + (v2.layer.frame.size.height / 2.)) );
ctr2 = CGPointApplyAffineTransform(ctr2, v1.transform);
#endif
center.x += ctr2.x;
center.y += ctr2.y;
CGPoint ctr3 = CGPointApplyAffineTransform(v3.center, CGAffineTransformConcat(v2.transform, v1.transform));
center.x += ctr3.x;
center.y += ctr3.y;
testView.center = center; // I want this to lay on top of v3
[self.view addSubview: testView];
NOTE: In my actual code, I put in-between test-views at all the intermediate centers. What I get is: testView1 (center) is correct, testView2 (ctr2) is off by a little, testView3 (ctr3) is off by a little more.
The #if is because I was experimenting with using ApplyAffine vs layer. Turns out they both give the same result, but it's a tad off.
Hopefully clarifying image:
You can use the UIView's convert points methods:
convertPoint:toView:
convertPoint:fromView:
convertRect:toView:
convertRect:fromView:
However once you apply a transformation, you should stop using frames and use center + bounds instead (which might be the reason your code is not working), from apple docs (for frame):
Warning: If the transform property is not the identity transform, the
value of this property is undefined and therefore should be ignored.
The only other thing you have to be conscious about is, which view invoques the convert to what other view since the results change. (source coordinate system -> target coordinate system.)
EDIT:
Thanks to this answer and Chiquis' comments in the question, my (Olie's) final (working) code looks like this:
CGRect testRect = CGRectMake(0, 0, 20, 20);
UIView *testView[3];
for (int ii = 0 ; ii < 3 ; ++ii)
{
testView[ii] = [[UIView alloc] initWithFrame: testRect];
testView[ii].backgroundColor = [UIColor redColor];
[self.view addSubview: testView[ii]];
}
testView[0].center = v0.center;
testView[1].center = [v0 convertPoint: v1.center toView: self.view];
testView[2].center = [v1 convertPoint: v2.center toView: self.view];
To clarify some, v1 is a subview of v0, and v2 is a subview of v1; this dictates the receivers in the covertPoint: calls.
I try to let iOS do the heavy lifting for me when I need to transform points between different layers.
CGPoint point = v3.center;
CGPoint ctr3 = [v3.layer.presentationLayer convertPoint:point toLayer:self.layer.presentationLayer];
The presentation layer object represents the state of the layer as it currently appears onscreen. This can help avoid timing issues if animations are involved.
Just noticed the comment that came in while I was composing: yes, I use this to account for rotation transforms similar to what you describe.

CALayer gets redrawn frequently despite shouldRasterize --> why?

I have some fairly complex layers in a part of my app and I want them to rasterize as their content changes close to never. The frame of the view these layers are in can be changed by dragging a "spacerbar" around with your finger.
I made a simple test-app to visualise my problem and show you some code:
#interface ViewController () {
UIView* m_MainView;
UIView* m_TopView;
UIView* m_BottomView;
UIView* m_MidView;
int m_Position;
}
#end
#implementation ViewController
- (void)loadView {
m_MainView = [[UIView alloc] init];
m_TopView = [[UIView alloc] init];
m_TopView.backgroundColor = UIColor.blueColor;
m_TopView.translatesAutoresizingMaskIntoConstraints = false;
m_BottomView = [[UIView alloc] init];
m_BottomView.backgroundColor = UIColor.blueColor;
m_BottomView.translatesAutoresizingMaskIntoConstraints = false;
m_MidView = [[UIView alloc] init];
m_MidView.backgroundColor = UIColor.blackColor;
m_MidView.translatesAutoresizingMaskIntoConstraints = false;
[m_MainView addSubview:m_TopView];
[m_MainView addSubview:m_BottomView];
[m_MainView addSubview:m_MidView];
CALayer* targetLayer = [[CALayer alloc] init];
targetLayer.frame = CGRectMake (100, 200, 500, 50);
targetLayer.backgroundColor = UIColor.yellowColor.CGColor;
targetLayer.shouldRasterize = true;
[m_BottomView.layer addSublayer:targetLayer];
UIPanGestureRecognizer* recognizer = [[UIPanGestureRecognizer alloc]
initWithTarget:self action:#selector(handlePan:)];
[m_MainView addGestureRecognizer:recognizer];
m_Position = 400;
super.view = m_MainView;
}
- (void)handlePan:(UIPanGestureRecognizer*)sender {
float touchY = [sender locationInView:m_MainView].y;
m_Position = (int)touchY;
[m_MainView setNeedsLayout];
}
- (void)viewWillLayoutSubviews {
CGRect bounds = super.view.bounds;
m_TopView.frame = CGRectMake (0, 0, bounds.size.width, m_Position - 10);
m_MidView.frame = CGRectMake (0, m_Position - 10, bounds.size.width, 20);
m_BottomView.frame = CGRectMake (0, m_Position + 10, bounds.size.width,
bounds.size.height - m_Position - 10);
}
#end
.
Now ... By using instruments and activating "Color Hits Green and Misses Red" one can see when a layer is redrawn (is then highlighted in red).
In this example the bar (targetLayer) is green (cached) most of the time, but if I start dragging the spacer (which in fact changes the frame of the bottom-view) the layer turns red some times ... especially if I reverse the direction of the drag or if I drag slowly.
In my app this causes flickering as redrawing these layers is expensive.
Why does this happen??
I never change any property of the layer but it gets redrawn anyways?
I am sure I am missing something :-)
As a workaround I could make a snapshot of the layers and use the image ... that would work I think and if there is no solution I will have to use this approach but I would like to understand what is the problem here.
Anyone?

How to attach 2 views inorder for them to pan as one view

I want 2 views to act as if they were "one view" - meaning if view 1 moves on the 'x' n pixels I want view 2 to move on the x axis the same amount of pixels (in any arbitrary direction) - without having to calculate all sorts of offsets and so on.
I thought using the new UIDynamicAnimator would be a great candidate so I tried this
UIView *v1 = [[UIView alloc] initWithFrame:CGRectMake(320-150, 150, 150, 100)];
v1.backgroundColor = [UIColor blackColor];
[self.view addSubview:v1];
self.v1 = v1;
UIView *v2 = [[UIView alloc] initWithFrame:CGRectMake(self.view.bounds.size.width,
0,
self.view.bounds.size.width,
self.view.bounds.size.height)];
v2.backgroundColor = [UIColor redColor];
[self.view addSubview:v2];
self.v2 = v2;
UIPanGestureRecognizer *p =
[[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(pan:)];
[v1 addGestureRecognizer:p];
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
UIAttachmentBehavior *att =
[[UIAttachmentBehavior alloc] initWithItem:self.v1
offsetFromCenter:UIOffsetZero
attachedToItem:self.v2
offsetFromCenter:UIOffsetMake(0,
self.v1.center.y - self.v2.center.y)];
att.damping = 0;
[self.animator addBehavior:att];
self.att = att;
-(void)pan:(UIPanGestureRecognizer *)gesture {
if(gesture.state == UIGestureRecognizerStateChanged) {
CGPoint p = [gesture locationInView:self.view];
// move only on x axis but keep on same y point
self.v1.center = CGPointMake(p.x, self.v1.center.y);
[self.animator updateItemUsingCurrentState:self.v1];
}
}
But they are interacting with each other - the little view tilts and changes its rotation and y location.
I would have hoped - only on 1 axis and "hard" connected to each other - any way to do this?
Given that no further details about the context is provided, I assume that you want the two views' relative distance to each other to remain constant. If this is indeed the case, I am not sure why you consider UIDynamicAnimator. Instead, you could embed the two views in a containerview and then manipulate the origin of the container instead of the two views individually. By embracing this approach you don't have to worry about re-calculating the origin of one view when moving the other, and vice versa.
self.containerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 100)];
UIView *v1 = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
UIView *v2 = [[UIView alloc] initWithFrame:CGRectMake(100 0, 100, 100)];
[self.containerView addSubview:v1];
[self.containerView addSubview:v2];
// now, changing the origin of containerView will move v1 and v2 simultaniously
- (void)pan:(UIPanGestureRecognizer *)gesture
{
if(gesture.state == UIGestureRecognizerStateChanged)
{
CGPoint p = [gesture locationInView:self.view];
// move only on x axis but keep on same y point
self.containerView.center = CGPointMake(p.x, self.containerView.center.y);
}
}
As an alternative, you could use autolayout and relate the two views in such way that your requirements are met. Then, when moving one of the views, the other will move accordingly. However, if you are actually using UIDynamics to manipulate views in that view hierarchy, autolayout is not the way to go since autolayout and UIDynamics tend to interfere with each other.
Set v1 origin 'X' to v2 origin 'X in panGesture.
-(void)pan:(UIPanGestureRecognizer *)gesture {
CGPoint point = [gesture locationInView:self.view];
CGRect boundsRect = CGRectMake(15, 40, 285, self.view.frame.size.height-60);
if (CGRectContainsPoint(boundsRect, point))
{
v1.center = point;
}
v2.frame = CGRectMake(v1.frame.origin.x, v2.frame.origin.y, v2.frame.size.width, v2.frame.size.height);
}
One easy way to solve this could be to add one of the views as a subview to the other or add both as subviews to a new view, which is only used to group the views. Depending on how you intend to use the views this might not be applicable to you though.
If you want to make one view a subview of the other but want the subview to be visible outside the bounds of the superview you can set clipToBounds to NO on the superview.

Ios moving a label ( song title ) from right side to left side?

How to move a label?
i would like to show a song title, moving from right side to left side with a duration that i can set. as you can see on a car radio. when the label if off the screen it should reappear from the right side
thank you
Iphone dont provide such feature for UILabels, you need to animate labels for that.
Refer this link
https://github.com/cbpowell/MarqueeLabel
Just drag and drop MarqueeLabel.h & MarqueeLabel.m files and create Label as follows:
MarqueeLabel *rightLeftLabel = [[MarqueeLabel alloc] initWithFrame:CGRectMake(10, 260, self.view.frame.size.width-20, 20) rate:50.0f andFadeLength:10.0f];
rightLeftLabel.numberOfLines = 1;
rightLeftLabel.shadowOffset = CGSizeMake(0.0, -1.0);
rightLeftLabel.textAlignment = NSTextAlignmentRight;
rightLeftLabel.textColor = [UIColor colorWithRed:0.234 green:0.234 blue:0.234 alpha:1.000];
rightLeftLabel.backgroundColor = [UIColor clearColor];
rightLeftLabel.font = [UIFont fontWithName:#"Helvetica-Bold" size:17.000];
rightLeftLabel.marqueeType = MLRightLeft;
rightLeftLabel.text = #"This text is not as long, but still long enough to scroll, and scrolls the same speed but to the right first!";
[self.view addSubview:rightLeftLabel];
They have created UIView subclass and animating UILabels that are subviews of UIView.
Hope this helps you :)
UILabel *toastLabel = [[UILabel alloc]init*];
toastLabel.text = #"Our toast text";
[toastLabel setHidden:TRUE];
[toastLabel setAlpha:1.0];
CGPoint location;
location.x = 0;
location.y = 400;
toastLabel.center = location;
location.x = 500;
location.y = 400;
[toastLabel setHidden:FALSE];
[UIView animateWithDuration:8 animations:^{
toastLabel.alpha = 0.0;
toastLabel.center = location;
}];

Resources