CALayer is not positioned correctly within the View - ios

Context
I am using this simple library to make an intro/walkthrough for my app (TL;DR, horizontal paging view controllers). I currently have 3 pages setup.
In the 3rd walkthrough page, I have a custom CALayer that animates a circle in an endless loop. I'd like to add that layer to a UIView in order to lay it out the way I want in IB (via auto layout).
In viewDidLoad (for the 3rd page) I create the circle layer and set its frame to be the same as the view I positioned, assuming the circle would be in the same spot as the view:
for v:UIView in [view1!, view2!] {
var pulse = PulseLayer()
pulse.frame = v.frame
pulse.cornerRadius = v.frame.width / 2.0
pulse.masksToBounds = false
view.layer.insertSublayer(pulse, above: v.layer)
}
Problem
When I run the app in the iPhone 6 simulator, the CALayers show up OUTSIDE their UIViews (see below).
One thing I noticed immediately is the layers are not misplaced the same way--one is above its view, and the other is to the left. I am assuming this is related to the constraints on the views, but I cannot figure out how to fix it.
Equally baffling to me is that when run on the iPhone 5 simulator, the layers appear exactly as I expect them to (see below).
I feel like I am misunderstanding some of the concepts at work here. How can I get the positioning to act the same? (Like the iPhone5 gif.)
Or, is there a better way to do what I am trying to do?

viewDidLoad is too soon. Remember, viewDidLoad is way early; the view is not in the interface yet and nothing has its ultimate size/position. If you're going to add the layers to view rather than as sublayers of the little views, you will have to run your layer-creation code much later, in order to get the position right. viewDidAppear: or viewDidLayoutSubviews will be safe - but of course you must use a bool flag so you don't do it too many times.
Personally I don't see why you don't add the layers to the little views. So you would just set pulse.frame = v.bounds and add as a sublayer to v, not to your view. It solves the positioning and gets the relationships right. And doing it in viewDidLoad would work, because when the views move, the layers move with them.

Related

Embedding Intrinsically Sized View Breaks Animation

I have built a test project to show what the goal is vs. what I currently have happening. The gif on the left shows exactly what I want the ending appearance to be. It is constructed with a single traditional view hierarchy. I need to achieve this with the pink view being an embedded/contained view. My attempts so far have only gotten me to the gif on the right.
The way the (pink) contained view grows is possibly an important detail: the blue subview changes it's height, and the whole apparatus gets a new intrinsic size because of all the connected vertical constraints. As you would expect, this is a simplification of my actual app, but I think it has all the important bits.
The main things I see that are strange:
The yellow/orange "other" view is not animating at all.
The pink contained view is animating nicely for it's own part, but it is animating it's position, even though it's frame has the same origin before and after the animation as shown here:
Here is the Storyboard of the right gif. Both the container view in the "parent" scene and the top view in the "child" scene have translatesAutoresizingMaskIntoConstraints set to false with runtime attributes.
The question then: **What must I change about my configuration to get all affected layout changes to animate (properly) when I have a size change in an intrinsically-sized and contained view? **
Edit: Tried manual embed
Since posting the question, I have tried a manual View Controller Containment strategy, and I got the exact same results as with the Storyboard technique, which is ultimately a good sign for the platform. There was 1 fewer view in the total hierarchy, but it didn't seem to make a difference.
Edit: Bounty and project
I have added a 100 point bounty to attract attention. I have also uploaded my sample project to this github repo. Check it out!
Changing your animation block in InnerViewController as follows does the trick.
var isCollapsed = false {
didSet {
let factor:CGFloat = isCollapsed ? 1.5 : 0.66
let existing = innerViewHeightConstraint.constant
UIView.animate(withDuration: 1.0) {
self.innerViewHeightConstraint.constant = existing * factor
self.view.layoutIfNeeded()
self.parent?.view.layoutIfNeeded()
}
}
}
The key difference is self.parent?.view.layoutIfNeeded(), which tells the embedding view controller to update the constraints as part of the animation, instead of immediately before the start of the animation.

How is the Instagram Stories cube transition done in iOS?

When swiping between stories in Instagrams new feature "Stories" (you know that cube-like transition when going from one story to another) I can't manage to understand how they do it!
First of all, if you dig deeper into the functionality you find that it works exactly like the UIPageViewControllers transition:
- It bounces when swiping fast from one view to another.
- You can pause the swipe in the middle of the transition by touching the screen.
The developing team couldn't have used a solution based on the more known workarounds out there, e.g:
https://www.appcoda.com/custom-view-controller-transitions-tutorial/
Because as far as I know my two statement above is not possible to achieve with anything else than the PageViewController.
This leaves me thinking that the Instagram Developer Team gained access to a new transition style for the PageViewController, also known as Cube-scroll, or is it a workaround that I'm not aware of?
Any ideas?
I took a shot at recreating this functionality a while back. You can check the source code on GitHub: https://github.com/oyvind-hauge/OHCubeView
I'm using a scroll view (with paging enabled) and, for each subview I'm manipulating these as a function of the given view's current x-offset in the scroll view. The actual animations are done on each subview's layer using Core Animation (more specifically, transforming an identity matrix, given by CATransform3DIdentity, using the method CATransform3DRotate).
The shadow effects are also applied to the subview's layers (view.layer.opacity), with the amount of shadow determined by how much of the view is showing on screen.
My implementation solves both of your concerns (bounces when swiping, can pause swipes). I'm sure this could have also been implemented using the a UIPageViewController, but I hate working with those.
I think you are overthinking the controller's part here. The effect can easily be achieved using a CATransformLayer and three-sided cube-like view structure, where there is one view which aligns with the screen plane, and two others rotated -90 and 90 degrees on their y axis. Then, getting a pan gesture to rotate the scene. After a successful 90 degree turn (in either direction), you can either quickly reset the scene (so that keeping on rotating appears as if continues, but actually the camera shifted back to initial position) or you can have a full 360 degree rotation, and just update previous and next "pages". A single controller can handle this scene. If you prefer to have each page as a controller, it is possible, you can still use one controller for the scene, and then use the page controllers as child controllers, and setting their views as described above.
See this article for more information on CATransformLayer. Their example already creates something that is quite close to what you need.

UIGestureRecognizer not called after setting UIViews frame inside UIScrollView

i am having some very strange issue. Basically i am implementing a drag and drop view with a snap to the horizontal 1D grid. Well, so when i drag a view and its center coordinate X is bigger or smaller then a different view, the non-dragged view should be animated to the left or right of its original position.
This works fine most of the time. But in some special cases its not working. In some situations specific situations the view does not receive any gesture callbacks anymore. I can reproduce this issue and have found out that when i remove the code that applies the animation, everything its working fine.
Basically this is the code that is called when the dragged view is at a position where the view below should be moved to the left or right
/**
* Animate element to its saved position
*/
- (void)switchElement:(unsigned int)draggedIndex with:(unsigned int)otherIndex
{
// first animate
UIView *view = views[draggedIndex];
UIView *otherView = views[otherIndex];
// IF I COMMENT THIS OUT, EVERYTHING WORKS FINE
otherView.frame = [self getImageRectForIndex:draggedIndex];
// now switch internally
if(draggedIndex != otherIndex)
{
// switch views
views[draggedIndex] = otherView;
views[otherIndex] = view;
}
}
Any idea if there is something to have in mind if i animate UIViews and have gesture recognizers attached to them?
If somebody is willing, i can paste the whole class here to test it.
SOLUTION
I am having some "highlight" views in my design. And i have moved the relevant views behind those transparent background views by accident. So now i am not using addSubview: but insertSubview:atIndex: instead.
But marking #Anthonin C. answers as the right one. Because it pointed me in the correct direction (I found it out by overriding the hitTest: method)
Would you please track the otherView.bounds property to verify that your touch isn't out of bounds. I faced this issue recently and Apple documentation provide solution here : Delivering touch events to a view outside the bounds of its parent view
Sorry, I don't have enough reputation to comment your answer.

Finding a CGRect in a complex view hierarchy

I am using a coachmarks/wizard framework called WSCoachMarksView that darkens your entire screen, and lets you create a highlighted area within a given CGRect to illuminate a call to action. I have a somewhat complicated hierarchy of views, and was having trouble properly identifying the CGRect I want to highlight with respect to the parent windows.
Here is my hierarchy. The goal is to find the CGRect for Day1, which is an imageview of about 50 pixels in height, and 25 pixels in width.
-TabViewController
-NavigationViewController
-HomeViewController : ViewController
-Container View (container within homeviewcontroller)
====================Everything below is within this container==============
-
HomeContainerViewController : ViewController (this view is loaded within the container)
-WeekViewController: ViewController
DayView (just a view I dragged onto WeekViewController to hold subviews)
Day1 (this is a rectangle imageview within the above DayView)
So there is a containerview within HomeViewController. Based on the above hierarchy, I've tried a multitude of permutations to correctly highlight Day1, but all have failed. I think my understanding of convertPoint is wrong and wanted to ask for some help in understanding which parameters to pass.
I read somewhere that you can only work off of the relationship between the parent/child but not a grandparent, etc?
Given this:
//assume tabViewController is the TabViewController above
//assume weekViewController is self.view, or WeekViewController
let day1Point = weekViewController.view.convertPoint(day1.frame.origin, fromView: tabViewController.view)
So unfortunately something like this just highlights the area on the top-left of the screen (my day 1 is actually near the middle of the screen).
What's the approach for convertPoint or convertRect? Is it always the leaf node in the hierarchy tree, and the "fromView" parameter is the top-most view (parent node)?
Thanks so much, any clarification / suggestions would be very much appreciated!
edit
so something like this gets me closer:
var myFrame: CGRect = dayView.convertRect(day1.frame, toView: self.view)
but it's still off slightly depending upon iPhone 5, 5s, 6, 6plus (only part of the image appears).
Note by OP:
Fogmeister suggested in the comments to use viewWillAppear, but i'm sure he meant viewDidAppear (which is actually what led me to solve this problem). other than that, everything he suggested was spot on (i.e. the correct usage of convertPoint/Rect below). Thanks so much for your help!
Well, the way convert works is that it will take the absolute point and convert it to different reference views.
So you should be able to do something like this...
From the WeekViewController you should be able to do...
self.view.convertPoint(day1.frame.origin, toView:homeViewController.view)
or...
self.view.convertRect(day1.frame, toView:homeViewController.view)
You will need a reference to the homeViewController to do this. I',m not certain what you're trying to do though. Where is your code that is highlighting the view? Is that code in homeViewController?

Who should manage the UI Elements, the view or the view controller?

I am currently developing an iOS app and I was wondering how to manage the UI elements. I am using a Storyboard to place my views and Autolayout to make everything resolution independent. If it helps, here's a bit of background on how I came up with this question.
Some Backgroud
I have buttons the represent piano keys and I later in code add a subview to these that draws the actual keys on them. I did this by creating #IBOutlets on the ViewController and susbscribed to the UIDeviceOrientationDidChangeNotification. This called a method that adds the subviews based on the current button frames to get the appropriate sized keys.
This worked fine when running in iOS 8.1, but if I ran the app on iOS 7, the frames of the buttons weren't updated to the new orientation by the time I got the UIDeviceOrientationDidChangeNotification, so everything was messed up in landscape. I did a little research (UIInterfaceOrientation not yet updated when UIDeviceOrientationDidChangeNotification caught in UIView) and it seems that a good solution would be to override the layoutSubviews method. Problem is, layoutSubviewsis a UIView method, and I am managing my buttons and UI in my UIViewController.
Impulsively I wanted to subclass my UIView, override layoutSubviews and from there call a method on my Controller to add the keys to the buttons correctly, but that doesn't sound right.
The Questions
My UI is managed by the UIViewController. But I need to update my views based on a method proper of a UIView. I assume, to respect the MVC principles, that my view should't now a thing about my controller, but then this brings up a few questions:
Who should be managing the UI?
Is my strategy wrong and should the
UIView hold the #IBOutlets to the buttons so that it can later
apply the subviews I need them to have?
If you read the background, do you have a suggestion for this particular situation?
I assume the complexity of this problem is relatively small and I don't need to setup notifications in NSNotificationCenter, but I may be wrong. I would really like to hear this is not the only solution.
Thanks in advance. I hope I was clear enough, but if you want/need any additional details on my particular situation, let me know and I will gladly elaborate.
if I understood the question correctly, your ViewController should be laying out the keys within it's view. A good, but not the only, place to do this is in viewDidLayoutSubviews which a method you can override on UIViewController.
Now if your views should maintain their internal layout. i.e if you buttons/keys have any subviews, you should update those in layoutSubviews on UIView as you mentioned.
Both methods will be called in response to changes in the bounds or center of your parent view.
As far as rotation changes are concerned, in iOS 8, you should use viewWillTransitionToSize:withTransitionCoordinator: or willTransitionToTraitCollection:withTransitionCoordinator:
depending on your needs.
I highly recommend that you write your layout independent of orientation. You should just use the containing view's bounds for reference and never hard code in any frames. For example perhaps one of your keys should be 1/56 the width of it's superview instead of a magical number for portrait and landscape.

Resources