Setting UIView.transform to arbitrary translate CGAffineTransform does nothing - ios

I have a UIView called container that I want to move (offset) using affine transfrom. This view contains UIImageView and is a subview of UICollectionViewCell.
So it should be simple:
container.transform = CGAffineTransformMakeTranslation(100, 200) //render container 100 points right and 200 points down
Instead it is very hard, because theat code does not do anything. The view is rendered excatly on the same place as if I delete that line. So I added 'print' to verify what affine translation was set:
container.transform = CGAffineTransformMakeTranslation(100, 200)
print(container.transform) //prints: CGAffineTransform(a: 1.0, b: 0.0, c: 0.0, d: 1.0, tx: 100.0, ty: 200.0)
That seems all right. So I tried rotating the container view instead with CGAffineTransformMakeRotation and it rotates the view just not around its center as it should according to documentation. I tried different combinations of translate, rotation and scale transforms just to find that the affine transformation matrixes set are OK, but attributes tx and ty seems to be ignored and a, b, c and d seems to be using different anchor point then the centre of the view (cannot say what that point is).
Any ideas on what can be causing this and how to fix it?

There must be something like auto layout messing things up for you. In the absence of outside influence, setting a view's affine transform to CGAffineTransformMakeTranslation(100, 200) will shift it right 100 points and down 200. I verified this by making a new Single View Project in Xcode and changing the viewDidLoad method in the ViewController.swift class to:
override func viewDidLoad()
{
super.viewDidLoad()
view.backgroundColor = UIColor.blueColor();
let container = UIView(frame: CGRectMake(0,0,100,100));
container.backgroundColor = UIColor.greenColor();
container.transform = CGAffineTransformMakeTranslation(100, 200);
view.addSubview(container);
}
As expected this makes the green container view appear 100 points to the right and 200 points down, even though its frame is (0,0,100,100).
So please check for auto layout and other such things that might influence the placement of this view, and if you can't find anything please post more code. Also, if your container view doesn't have a background color, please give it one so that you can see its position directly, instead of deducing its position by looking at the image view.
n.b. Setting a view's transform doesn't actually move the view itself, it just changes how/where it draws its content.

Related

UILabel text doesn't appear when using ARKit

I'm programmatically generating a set of UILabels, attaching them to SCNNodes and then placing them in a scene.
The problem is that the text on some of the labels doesn't appear. This occurs (seemingly) randomly.
Here's the code:
var labels = [SCNNode]()
var index: Int
var x: Float
var y: Float
let N = 3
for i in 0 ... N-1 {
for k in 0 ... N-1 {
let node = label()
labels.append(node)
index = labels.count - 1
x = Float(i) * 0.5 - 0.5
y = Float(k) * 0.5 - 0.5
sceneView.scene.rootNode.addChildNode(labels[index])
labels[index].position = SCNVector3Make(x, y, -1)
}
}
and the method to create the label node:
func label() -> SCNNode {
let node = SCNNode()
let label = UILabel(frame: CGRect(x: CGFloat(0), y: CGFloat(0),
width: CGFloat(100), height: CGFloat(50)))
let plane = SCNPlane(width: 0.2, height: 0.1)
label.text = "test"
label.adjustsFontSizeToFitWidth = true
plane.firstMaterial?.diffuse.contents = label
node.geometry = plane
return node
}
The labels themselves always appear correctly, it's just that some of them are blank, with no text.
I've tried playing around with the size of the label, the size of the plane it is attached to, the font size etc - nothing seems to work.
I've also tried enclosing the label creation in DispatchQueue.main.async { ... }, which didn't help either.
I'm moderately new to Swift and iOS, so could easily have missed something very obvious.
Thanks in advance!
EDIT:
(1) Setting label.backgroundColor = UIColor.magenta makes it clear that in fact the label is not being created, but the node / plane is.
Some of the labels are left white (i.e only the SCNNode is being rendered), however after a short delay they sometimes then become magenta and the text will appear. Some of the labels will remain missing though.
(2) It further appears that it's related to the position and orientation of the node (label) relative to the camera. I created a large (10x10) grid of labels, then tested placing the camera at different initial positions in the grid. The likelihood that a node appeared seemed directly related to the distance of the node from the initial camera position. Those nodes directly in front of the camera were always rendered, and those far away almost never were.
(3) workaround / hack is to convert the labels to images, and use them instead - code is at https://github.com/Jordan-Campbell/uiimage-arkit-testing if anyone is interested.
If you are labeling things in AR, 99% of the time it is better to do so in "Screen Space" rather than in "Perspective".
Benefits of labels in Screen Space:
ALWAYS readable, regardless of user's distance from the label
You can use regular UILabels, no need to draw them to an image and then map the image to an SCNPlane.
Your app will have a first party feel to it because Apple uses Screen Space for their labels in all of their AR apps (see Measure).
You will be able to use standard animations on your UILabel, animations are much more complex to set up when working with content in Perspective.
If you are sold on Screen Space, let me know and I'll be happy to put up some code showing you the basics.
Use main thread for creating and adding labels to scene. This will make things faster, and avoid coupling this addition to scene with plane detection this really slows down the rendering. this works at times.
DispatchQueue.main.async {
}
Use a UIView SuperView as Parent to your label this would make things smoother.

UIView animated position jumping

I have a transition animation where I need to make it appear that an item on VC1 moves to a new location as VC2 is swiped into position. I've had no problems making this work on other transitions, but for this one I have an issue I can't figure out.
VC1 - has an image in the center at a particular size
VC2 - same image is placed in the top right corner at a smaller size
So I need to move and scale, since I'm doing this in a transition I thought the right way to do this is:
take image from VC2 translate and scale it to match image on VC1 (using transform)
hide image on VC1
animate image from VC2 back to .identity
I've successfully done this exact same thing when moving something to the top left corner. However when I do this particular animation I'm seeing my VC2 image jump as the animation starts to a new location. I only see this jump when I'm working on a device/simulator that is larger or smaller than a iPhone7, and it jumps differently based on the screen width (left for smaller, right for larger).
I've logged out the position of my image at all stages:
transform from: (x:336.0, y:35.5, w:38.0) to: (x:160.0, y:180.5, w:66.0)
xOffset: -176.0 yOffset: 145.0
transform: CGAffineTransform(a: 1.0, b: 0.0, c: 0.0, d: 1.0, tx: -176.0, ty: 145.0)
preanimation: (x: 160.0, y: 180.5
currentPosition: (midX: 105.0, midY: 180.5)
currentPosition: (midX: 112.312, midY: 174.476)
First line of this log shows that I'm moving my image from (336, 35) to (160, 180.5). Second line shows that my math is correct. Third line shows the transform that was created (scaling currently turned off). Fourth line shows that my image is in the correct spot just before the animation starts. Last 2 lines are returned in a cadisplaylink callback showing that my position jumps before the animation starts.
Has anyone run into something like this? Any clues how I can solve this? My images are all placed with LayoutConstraints, my animations are all run using transforms (transforming before animation then animating back to .identity).
EDIT:
Some added information, when starting in landscape I noticed that my original frames are completely wrong:
transform from: (x:336.0, y:35.5, w:38.0) to: (x:368.0, y:147.666666666667, w:66.0)
xOffset: 32.0 yOffset: 112.166666666667
transform: CGAffineTransform(a: 2.66666666666667, b: 0.0, c: 0.0, d: 2.66666666666667, tx: 32.0, ty: 112.166666666667)
preanimation: (x: 368.0, y: 147.666666666667
currentPosition: (midX: 729.0, midY: 127.666666666667)
So at this point I'm thinking the issue is that my frames that I'm attempting to sync up to aren't correct before the animation because of the autolayout constraints. And when the animation starts all of the frames are re-calculated by the animation using the layout constraints, then my transforms are applied making the jump appear.
EDIT2:
I think I've got it figured out. The issue is that the view I'm animating in hasn't laid out it's subviews yet so the frames are off. By calling .setNeedsLayout() and .layoutIfNeeded() on my views during the animation setup I get the correct locations to animate between.
For the curious here are the same logs from my first edit with the addition of when the viewlifecycle methods are called:
viewDidLoad <Chalkboard.SwipeToMoveViewController: 0x7fb1f760fb70>
viewWillAppear <Chalkboard.SwipeToMoveViewController: 0x7fb1f760fb70>
transform from: (x:336.0, y:35.5, w:38.0) to: (x:368.0, y:147.666666666667, w:66.0)
xOffset: 32.0 yOffset: 112.166666666667
transform: CGAffineTransform(a: 2.66666666666667, b: 0.0, c: 0.0, d: 2.66666666666667, tx: 32.0, ty: 112.166666666667)
preanimation: (x: 368.0, y: 147.666666666667
animation Block: (to.x: 368.0, to.y: 147.666666666667 - (from.x: 368.0, from.y: 147.666666666667
viewWillLayoutSubviews <Chalkboard.SwipeToMoveViewController: 0x7fb1f760fb70>
viewDidLayoutSubviews <Chalkboard.SwipeToMoveViewController: 0x7fb1f760fb70>
currentPosition: (midX: 729.0, midY: 127.666666666667)
Just incase people don't want to read through the wall of text to find the solution the problem was that I was using the positions of views in different view controllers before all views had been laid out. I'd assumed that the entire view lifecycle had processed in my presenting view controller before the animation logic started, I was wrong.
Animation Call Order:
viewDidLoad
viewWillAppear
animateTransition
viewWillLayoutSubviews
viewDidLayoutSubviews
So the fix was to manually call setNeedsLayout() and layoutIfNeeded() as the start of my animateTransition so that the positions of my views were all known and could be used for transformations.

Ios Swift Animate a view in non linear path

I am trying to animate a UIView through non linear path(i'm not trying to draw the path itself) like this :
The initial position of the view is determinated using a trailing and bottom constraint (viewBottomConstraint.constant == 100 & viewTrailingConstraint.constant == 300)
I am using UIView.animatedWithDuration like this :
viewTrailingConstraint.constant = 20
viewBottomConstraint.constant = 450
UIView.animateWithDuration(1.5,animation:{
self.view.layoutIfNeeded()
},completition:nil)
But it animate the view in a linear path.
You can use keyFrame animation with path
let keyFrameAnimation = CAKeyframeAnimation(keyPath:"position")
let mutablePath = CGPathCreateMutable()
CGPathMoveToPoint(mutablePath, nil,50,200)
CGPathAddQuadCurveToPoint(mutablePath, nil,150,100, 250, 200)
keyFrameAnimation.path = mutablePath
keyFrameAnimation.duration = 2.0
keyFrameAnimation.fillMode = kCAFillModeForwards
keyFrameAnimation.removedOnCompletion = false
self.label.layer.addAnimation(keyFrameAnimation, forKey: "animation")
Gif
About this function
void CGContextAddQuadCurveToPoint (
CGContextRef _Nullable c,
CGFloat cpx,
CGFloat cpy,
CGFloat x,
CGFloat y
);
(cpx,cpy) is control point,and (x,y) is end point
Leo's answer of using Core Animation and CAKeyframeAnimation is good, but it operates on the view's "presentation layer" and only creates the appearance of moving the view to a new location. You'll need to add extra code to actually move the view to it's final location after the animation completes. Plus Core Animation is complex and confusing.
I'd recommend using the UIView method
animateKeyframesWithDuration:delay:options:animations:completion:. You'd probably want to use the option value UIViewKeyframeAnimationOptionCalculationModeCubic, which causes the object to move along a curved path that passes through all of your points.
You call that on your view, and then in the animation block, you make multiple calls to addKeyframeWithRelativeStartTime:relativeDuration:animations: that move your view to points along your curve.
I have a sample project on github that shows this and other techniques. It's called KeyframeViewAnimations (link)
Edit:
(Note that UIView animations like animateKeyframes(withDuration:delay:options:animations:completion:) don't actually animate your views along the path you specify. They use a presentation layer just like CALayer animations do, and while the presentation layer makes the view look like it's moving along the specified path, it actually snaps from the beginning position to the end position at the beginning of the animation. UIView animations do move the view to its destination position, where CALayer animations move the presentation layer while not moving the layer/view at all.)
Another subtle difference between Leo's path-based UIView animation and my answer using UIView animateKeyframes(withDuration:delay:options:animations:completion:)is that CGPath curves are cubic or quadratic Bezier curves, and my answer animates using a different kind of curve called a Katmull-Rom spline. Bezier paths start and end at their beginning and ending points, and are attracted to, but don't pass through their middle control points. Catmull-Rom splines generate a curve that passes through every one of their control points.

SpriteKit: What's up with the coordinate system?

I'm teaching myself how to do SpriteKit programming by coding up a simple game that requires that I lay out a square "game field" on the left side of a landscape-oriented scene. I'm just using the stock 1024x768 view you get when creating a new SpriteKit "Game" project in XCode - nothing fancy. When I set up the game field in didMoveToView(), however, I'm finding the coordinate system to be a little weird. First of all, I expected I would have to place the board at (0, 0) for it to appear in the lower-left. Not so -- it turns out the game board has to be bumped up about 96 pixels in the y direction to work. So I end up with this weird code:
let gameFieldOrigin = CGPoint(x:0, y:96) // ???
let gameFieldSize = CGSize(width:560, height: 560)
let gameField = CGRect(origin: gameFieldOrigin, size: gameFieldSize)
gameBorder = SKShapeNode(rect: gameField)
gameBorder.strokeColor = UIColor.redColor()
gameBorder.lineWidth = 0.1
self.addChild(gameBorder) // "self" is the SKScene subclass GameScene
Furthermore, when I add a child to it (a ball that bounces inside the field), I assumed I would just use relative coordinates to place it in the center. However, I ended up having to use "absolute" coordinate and I had to offset the y-coordinate by 96 again.
Another thing I noticed is when I called touch.locationInNode(gameBorder), the coordinates were again not relative to the border, and start at (0, 96) at the bottom of the border instead of (0, 0) as I would have guessed.
So what am I missing here? Am I misunderstanding something fundamental about how coordinates work?
[PS: I wanted to add the tag "SpriteKit" to this question, but I don't have enough rep. :/]
You want to reference the whole screen as a coordinate system, but you're actually setting all the things on a scene loading from GameScene.sks. The right way to do is modify one line in your GameViewController.swift in order to set your scene size same as the screen size. Initialize scene size like this instead of unarchiving from .sks file:
let scene = GameScene(size: view.bounds.size)
Don't forget to remove the if-statement as well because we don't need it any more. In this way, the (0, 0) is at the lower-left corner.
To put something, e.g. aNode, in the center of the scene, you can set its position like:
aNode.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame));

SCNText with SCNLookAtConstraint looking the wrong way

I want to display text alongside my 3D model using SceneKit in iOS. It just has to appear like it is 2D. So the text should always be facing the camera, however I rotate the 3D model. I have an SCNNode with a SCNText geometry and I attached an SCNLookAtConstraint like so:
let constraint = SCNLookAtConstraint(target: cameraNode)
constraint.gimbalLockEnabled = true
textNode.constraints = [constraint]
They seem to be facing the camera, but only the wrong way! I see all text mirrored! Also, sometimes the text rotates, I want it to be aligned horizontally at all times. So this is all the behaviour I don't want.
Can somebody help me out here? I just want to have some text that follows a few nodes in my 3D object. I don't care how. It doesn't have to be SCNText for all I care, if this can be done with a simple UILabel: fine with me! Just tell me how!
From the documentation:
When SceneKit evaluates a look-at constraint, it updates the constrained node’s transform property so that the node’s negative z-axis points toward the constraint’s target node.
You can workaround that by setting the node's pivot or by using an intermediate node. That node would be constrained, and would have the text as a child node (which would be rotated by π along the y axis)
Update
Starting iOS 11.0 the SCNLookAtConstraint class exposes the localFront property which allows you to specify a different front axis. It defaults to (0, 0, -1) and you can change it to (0, 0, 1) to achieve what you want.
Based on #mnuages response, setting the node's pivot to the following should do the trick.
node.pivot = SCNMatrix4Rotate(node.pivot, Float.pi, 0, 1, 0)
I faced this problem on iOS11 beta 5, Swift4:
//1. Rotate your `textNode`, and don't add `textNode` into `sceneView`
textNode.eulerAngles = SCNVector3Make(0, .pi, 0)
//2. Create new wrapper node, add `textNode` as a child node
let textWrapperNode = SCNNode()
textWrapperNode.addChildNode(textNode)
//3. Add constraint for wrapper node
let constraint = SCNLookAtConstraint(target: cameraNode)
constraint.gimbalLockEnabled = true
textWrapperNode.constraints = [constraint]
//4. Add wrapper node into `sceneView`
sceneView.scene.rootNode.addChildNode(textNode)

Resources