Given following view hierarchy:
root (e.g. view of a view controller)
|_superview: A view where we will draw a cross using core graphics
|_container: Clips subview
|_subview: A view where we will show a cross adding subviews, which has to align perfectly with the cross drawn in superview
|_horizontal line of cross
|_vertical line of cross
Task:
The crosses of superview and subview have to be always aligned, given a global transform. More details in "requirements" section.
Context:
The view hierarchy above belongs to a chart. In order to provide maximal flexibility, it allows to present chart points & related content in 3 different ways:
Drawing in the chart's base view (superview) draw method.
Adding subviews to subview. subview is transformed on zoom/pan and with this automatically its subviews.
Adding subviews to a sibling of subview. Not presented in view hierarchy for simplicity and because it's not related with the problem. Only mentioning it here to give an overview. The difference between this method and 2., is that here the view is not transformed, so it's left to the implementation of the content to update "manually" the transform of all the children.
Maximal flexibility! But with this comes the cost that it's a bit tricky to implement. Specifically point 2.
Currently I got zoom/pan working by basically processing the transforms for superview core graphics drawing and subview separately, but this leads to redundancy and error-proneness, e.g. repeated code for boundary checks, etc.
So now I'm trying to refactor it to use one global matrix to store all the transforms and derive everything from it. Applying the global matrix to the coordinates used by superview to draw is trivial, but deriving the matrix of subview, given requirements listed in next section, not so much.
I mention "crosses" in the view hierarchy section because this is what I'm using in my playgrounds as a simplified representation of one chart point (with x/y guidelines) (you can scroll down for images and gists).
Requirements:
The content can be zoomed and panned.
The crosses stay always perfectly aligned.
subview's subviews, i.e. the cross line views can't be touched (e.g. to apply transforms to them) - all that can be modified is subview's transform.
The zooming and panning transforms are stored only in a global matrix matrix.
matrix is then used to calculate the coordinates of the cross drawn in superview (trivial), as well as the transform matrix of subview (not trivial - reason of this question).
Since it doesn't seem to be possible to derive the matrix of subview uniquely from the global matrix, it's allowed to store additional data in variables, which are then used together with the global matrix to calculate subview's matrix.
The size/origin of container can change during zoom/pan. The reason of this is that the labels of the y-axis can have different lengths, and the chart is required to adapt the content size dynamically to the space occupied by the labels (during zooming and panning).
Of course when the size of container changes, the ratio of domain - screen coordinates has to change accordingly, such that the complete original visible domain continues to be contained in container. E.g if I'm displaying an x-axis with a domain [0, 10] in a container frame with a width of 500pt, i.e. the ratio to convert a domain point to screen coordinates is 500/10=50, and shrink the container width to 250, now my [0, 10] domain, which has to fit in this new width, has a ratio of 25.
It has to work also for multiple crosses (at the same time) and arbitrary domain locations for each. This should happen automatically by solving 1-7 but mentioning it for completeness.
What I have done:
Here are step-by-step playgrounds I did to try to understand the problem better:
Step 1 (works):
Build hierarchy as described at the beginning, displaying nothing but crosses that have to stay aligned during (programmatic) zoom & pan. Meets requirements 1, 2, 3, 4 and 5:
Gist with playground.
Particularities here:
I skipped container view, to keep it simple. subview is a direct subview of superview.
subview has the same size as superview (before zooming of course), also to keep it simple.
I set the anchor point of subview to the origin (0, 0), which seems to be necessary to be in sync with the global matrix.
The translation used for the anchor change has to be remembered, in order to apply it again together with the global matrix. Otherwise it gets overwritten. For this I use the variable subviewAnchorTranslation. This belongs to the additional data I had in mind in the bullet under requirement 5.
Ok, as you see everything works here. Time to try the next step.
Step 2 (works):
A copy of step 1 playground with modifications:
Added container view, resembling now the view hierarchy described at the beginning.
In order for subview, which is now a subview of container to continue being displayed at the same position, it has to be moved to top and left by -container.origin.
Now the zoom and pan calls are interleaved randomly with calls to change the frame position/size of container.
The crosses continue to be in sync. Requirements met: All from step 1 + requirement 6.
Gist with playground
Step 3 (doesn't work):
So far I have been working with a screen range that starts at 0 (left side of the visible playground result). Which means that container is not fulfilling it's function to contain the range, i.e. requirement 7. In order to meet this, container's origin has to be included in the ratio calculation.
Now also subview has to be scaled in order to fit in container / display the cross at the correct place. Which adds a second variable (first being subviewAnchorTranslation), which I called contentScalingFactor, containing this scaling, that has to be included in subview's matrix calculation.
Here I've done multiple experiments, all of them failed. In the current state, subview starts with the same frame as container and its frame is adjusted + scaled when the frame of container changes. Also, subview being now inside container, i.e. its origin being now container's origin and not superview's origin, I have to set update its anchor such that the origin is not at (0,0) but (-x,-y), being x and y the coordinates of container's origin, such that subview continues being located in relation to superview's origin. And it seems logical to update this anchor each time that container changes its origin, as this changes the relative position from content's origin to superview's origin.
I uploaded code for this - in this case a full iOS project instead of only a playground (I thought initially that it was working and wanted to test using actual gestures). In the actual project I'm working on the transform works better, but I couldn't find the difference. Anyway it doesn't work well, at some point there are always small offsets and the points/crosses get out of sync.
Github project
Ok, how do I solve this such that all the conditions are met. The crosses have to stay in sync, with continuous zoom/pan and changing the frame of container in between.
The present answer allows for any view in the Child hierarchy to be arbitrarily transformed. It does not track the transformation, merely converts a transformed point, thus answers the question:
What are the coordinates of a point located in a subview in the coordinate system of another view, no matter how much that subview has been transformed.
To decouple the Parent from the clipping Container and offer a generic answer, I propose place them at the same level conceptually, and in different order visually (†):
Use a common superview
To apply the scrolling, zooming or any other transformation from the Child to the Parent, go through common superview (named Coordinator in the present example).
The approach is very similar to this Stack Overflow answer where two UIScrollView scroll at different speed.
Notice how the red hairline and black hairline overlap, regardless of the position, scrolling, transform of any and all off the views in the Child hierarchy, including that of Container.
↻ replay animation
Code
Coordinate conversion
Simplified for clarity, using an arbitrary point (50,50) in the coordinate system of the Child view (where that point is effectively drawn), and convert it in the Parent view system looks like this:
func coordinate() {
let transfer = theChild.convert(CGPoint(x:50, y:50), to: coordinator)
let final = coordinator.convert(transfer, to: theParent)
theParent.transformed = final
theParent.setNeedsDisplay()
}
Zoom & Translate Container
func zoom(center: CGPoint, delta: CGPoint) {
theContainer.transform = theContainer.transform.scaledBy(x: delta.x, y: delta.y)
coordinate()
}
func translate(delta: CGPoint) {
theContainer.transform = theContainer.transform.translatedBy(x: delta.x, y: delta.y)
coordinate()
}
(†) I have renamed Superview and Subview to Parent and Child respectively.
Depending on where the anchorPoint is with UIAttachmentBehavior, the view can be quite rotated, so it's in more of a diamond shape than a square. In these situations, where it's rotated say 90°, how do I find what the lowest or highest point of this UIView is (in relation to the window)?
It's easy enough when the UIView is a (non-rotated) square, as I can just use CGRectGetMaxY (or min) and the x value doesn't matter, and then use convertPoint, but with the rotation the x value seems to have a real importance, as if I choose maxX, it will tell me the bottom right's point, while minX will give me the bottom left's.
I just want the lowest point that exists in the UIView. How do I get this?
EDIT: Here's some code showing how I'm attempting it currently:
CGPoint lowestPoint = CGPointMake(CGRectGetMinX(weakSelf.imageView.bounds), CGRectGetMaxY(weakSelf.imageView.bounds));
CGPoint convertedPoint = [weakSelf.imageView convertPoint:lowestPoint toView:nil];
The tracking of convertedPoint's y value completely changes depending on what I supply for the x value in lowestPoint's CGPointMake.
The view's frame is its bounding box. Normally you should be careful about using the frame of a transformed view, which is precisely what a rotated-by-UIKit-Dynamics view is. But it does give the info you are after.
I have a view to whose layer I applied transformations - altered the m34 field, rotated it on the x axis and scaled it on x and y. Then I added this view to a bigger superview. My issue is that every other view I add to the bigger superview gets hidden or overlapped by the transformed one (if the new view's frame intersects the transformed view's frame), even though the new views stand higher in the hierarchy than the transformed one and those new views get added to the end of the subviews array of the bigger superview. Any ideas what is the reason behind this behavior? :-) Thanks a lot!
3D transformations are used to make pseudo-3D. And in this 3D space your layer overlaps others. To change its z-pozition use CAlayer's property
#property CGFloat zPosition;
The layer’s position on the z axis. Animatable. The default value of
this property is 0. Changing the value of this property changes the
the front-to-back ordering of layers onscreen. This can affect the
visibility of layers whose frame rectangles overlap. The value of this
property is measured in points.
The view hierarchy is like a series of sheets of paper - they can be "on top" of each other, but there is no real depth. As soon as you put a 3D transform on a layer, this will have some depth and, if you've rotated it around the x axis, this will be sticking out in front of the other views.
You could try adjusting the z position of the layer to move it behind other views.
I'm working with CorePlot to generate a bar graph with horizontal bars (CPTXYGraph and CPTBarPlot).
There are only 10 bars being plotted at any given time, but the labels for them are arbitrarily long. As a result of that the Y axis labels rotation is set to 0.5. For the longest labels there are clipping issues with the label going outside of the left and/or bottom of the plot area.
I've fixed this in the X direction by determining the width of the longest label (as rendered in the appropriate font) and rotated the appropriate way to set the left padding in the plotAreaFrame.
Due to the rotation of the labels, if the plot at the bottom of the graph has a long name it can still extend off the bottom edge of the graph and get clipped there.
I know how tall vertically the rotated label is, but where I'm stuck is trying to determine where in the plot area the axis label is being plotted so that I can determine how much bottom padding is required.
I'm not sure if I should be trying to determine how tall the X axis label section is and then subdividing the remaining height to determine where the ticks should fall, or if there's some other better way to go about it.
The label is offset from end of the nearest tick mark by the labelOffset. The position of the end of the tick mark depends on the tickDirection, tickLabelDirection, and majorTickLength.
There are methods that allow you to convert a point from the plot space coordinate system to the view coordinate system. I've ended up solving the problem with code like the following:
// Tell the graph to lay itself out if it needs to.
[self.graph layoutIfNeeded];
// Create a point that represents 0,1 as plotted on the graph.
NSDecimal plotPoint[2];
plotPoint[CPTCoordinateX] = CPTDecimalFromInt (0);
plotPoint[CPTCoordinateY] = CPTDecimalFromInt (1);
// Convert that point from the plot coordinate system to the view coordinate system
CGPoint point = [self.graph.defaultPlotSpace plotAreaViewPointForPlotPoint: plotPoint];
// If the difference between the height of the rotated label and the position that it
// will be plotted at is at least the bottom padding, then the height is too large,
// so fix the padding.
if (labelHeight - point.y >= self.graph.plotAreaFrame.paddingBottom)
[self.graph.plotAreaFrame setPaddingBottom: (labelHeight - point.y)];
My graphs have a default bottom padding already set as a minimum, allowing this code to bump it up if there isn't enough already.
I am trying to achieve some effect using imageViews and I need to set the anchorpoint correctly. It is very hard without it being visible.
How would you go about showing it up on the imageview ?
The anchor point is a property specified on the layer (you will need QuartCore to access it). It is specified in the unit coordinate space of the device (both x and y goes from 0 to 1 within the bounds of the layer. You can know the coordinate of the actor point by multiplying the x value with the width and the y value with the height. To draw a point at that location you could just add a new, small layer (bounds 4x4 and corner radius 4 (to get a circle))
Be aware that changing the anchor point will change where the layer (and the view) appears on screen as the frame is calculated relative to the anchor point. To have the frame appear in the same place after the anchor point has been set you could re-set the frame (this will update the layers position so that the frame is what you expect it to be)