Cocoa moving overlapping NSViews - objective-c

I have several views inside one main view. Those views are all behind one another so that only the top one is currently seen. What I would like to do is, upon some particular user input, have one of the views in the back move lower on the screen, so it can be seen below the top view. I've been trying to do it by determining if the two views are currently overlapping, lower the one in back, if they are still overlapping, lower it more, and so on until they no longer overlap and are both entirely visible. Then I'd like for the main view to resize so that both views can be seen but I think that'd just be using setBounds or setFrame, haven't gotten there yet.
For the first problem I haven't found a way to literally see if two views overlap, if that can't be done I thought maybe by drawing rects on the view bounds and checking if those overlap, but I don't get how I can do either of these. I think bounds or frame is needed, but I'm not really sure how these are different.
Basically, I want to check if two NSViews overlap, if they do, I want to lower one of them.
EDIT: This part of the problem is pretty much solved, but now a new problem arose. I have three views, one main view, and two subviews. The main view doesn't have anything, it only contains several subviews, whatever is inside the main view is what should be seen. The two subviews are stacked on top of the main view, so that the frontmost one is the visible one, the one behind it is set to hidden. When the user hits start, the view in the back becomes visible and I set a repeating timer with a selector that will check if the views overlap, like #Vince said, if they do, it offset's the one in the back down a little and checks again until they don't overlap anymore. Using this:
- (void)updateViews {
CGRect viewFrame = [view frame]; //view is the frontmost view that will not move
CGRect backroundViewFrame = [backgroundView frame]; //the view in the back
CGRect rectIntersection = CGRectIntersection (viewFrame,
backgroundFrame);
//if the intersect is NOT null, it'll offset down by 2
if (!CGRectIsNull(rectIntersection)) {
CGRect newFrame = CGRectOffset (
rectIntersection,
0, //doesn't move in x
-2 //lowers by 2 in y
);
[backgroundView setFrame:newFrame]; //lowers the view to the new offset rect
} else{
[viewsUpdater invalidate]; //stops the timer when rects no longer intersect
viewsUpdater = nil;
}
The problem is that when I lower the backgroundView, it's only visible in the space where it intersects with the front view. Even if I expand the main view lower (which is the idea, they will all start one on top of the other with the same dimensions and when the backgroundView lowers, the main view will adjust and display both). But right now it doesn't matter how large the main view is, the backgroundView is only visible in the area where it intersects with the front view. I think it's moving to where it has to, but by the time it stops it's completely invisible, so I can't tell.
Thanks for the help.

Basically, you will be able to know if two NSView objects overlaps by getting the intersection of the two frame properties (CGRectIntersection()) and then testing for the result to be null using CGRectIsNull().
Here is the doc for it.
Now, about putting one view over another, it sounds like you need to use this method from NSView : -(void)setSubviews:(NSArray *)newSubviews;, which according to the documentation, lets you reorder the subviews.
Or simply using -(void)replaceSubview:(NSView *)oldView with:(NSView *)newView;, that said, since the oldView will be released, be careful to retain a reference to it before.
If you just attempted to move a view lower, you only have to change the origin of the reciever's frame, for example :
CGRect actualFrame = [aView frame];
CGPoint framesOrigin = actualFrame.origin;
CGRect newFrame = CGRectMake(framesOrigin.x,
(framesOrigin.y)+50,
actualFrame.width,
actualFrame.height);
[aView setFrame:newFrame];
This will move aView lower, by changing the y coordinate of its frame's origin.
Finally, about the difference between bounds and frame properties, this post on SO Difference between view's frame and view's bound + iPhone, should answer your question ;)

Related

NSStackView does not layout views properly

I've just started experimenting with NSStackView, and I'm having a very interesting problem which I can't solve. I've scoured the auto layout guides on Apple's website as well as the NSStackView documentation, and can't seem to find anything.
My problem is that I have two identical NSScrollView objects (each with an embedded NSTextView) which are loaded from nib files. When I add these views to my stack view, the one that is added first takes up 100% of the available space, and the second collapses completely down to the bottom with a height of 2 pixels while taking up all available horizontal space. In effect, this looks like the first view is the only one in the window. Here's what it currently looks like:
It's nearly impossible to see in this example because of the background color, but the scroll view ends a couple pixels above the bottom of the window. Here's a better view from the view hierarchy inspector, where I have this 2 pixel high view selected (click here to view larger):
Here's the relevant setup code:
// Load the stack view
self.inputViewController = [[NSViewController alloc] initWithNibName:#"Document_TextInputView" bundle:nil];
self.textView = (NSTextView *)[[(NSScrollView *)self.inputViewController.view contentView] documentView];
self.outputViewController = [[NSViewController alloc] initWithNibName:#"Document_TextOutputView" bundle:nil];
self.outputView = (NSTextView *)[[(NSScrollView *)self.outputViewController.view contentView] documentView];
// Add all views into the stack view
[self.stackView addView:self.inputViewController.view inGravity:NSStackViewGravityTop];
[self.stackView addView:self.outputViewController.view inGravity:NSStackViewGravityBottom];
self.stackView.orientation = NSUserInterfaceLayoutOrientationVertical;
// Load the text into the window.
[self.textView setString:self.cachedText];
[self.outputView setString:#"=== PROGRAM OUTPUT ===\n"];
[self.codeActionSegmentedControl setEnabled:NO forSegment:1];
From what I understand, the intrinsic content size should prohibit the view from getting shrunk this small. I'm not too familiar with NSStackView, so any help would be appreciated.
Alright, I've found the solution to my own problem and I am posting it so everyone who searches for this and finds the question will have the answer.
The issue is that NSScrollView does not have an intrinsic content size, which prohibits the NSStackView which knowing what height it ought to be, hence the second NSScrollView was being collapsed. The constraints created by default in Xcode give the NSScrollView's relation to other elements, but this information does not tell the stack view anything about what its height should be.
The solution is to add a height constraint to the NSScrollView (programmatically or in Interface Builder) so that NSStackView can lay out the views properly. Then, it all just magically works.

Identifying correct window frame size for filling background color

I am developing in Cocoa, and I am currently having problems with filling the background of a NSWindowController.
I understand that subclassing is the way forward if you want to customise your cocoa app. So I created a custom NSView named whiteView and added this view as a subview to my windowController's contentView; however, there are some issues with completely filling the background of the window. Can anyone explain how I can have the color cover the complete surface area of the window's frame pls. Thank you
These are the results that I have so far.
1) This is the window when I leave it as it is, notice the white color only having covered half of the window.
2)Here is the same window again when I adjust the window far to the right and bottom. The white screen seems to stretch enough so that it covers the elements.
This is how I create the custom view
- (void)drawRect:(NSRect)dirtyRect
{
[super drawRect:dirtyRect];
[[NSColor whiteColor] set];
NSRectFill([self bounds]);
}
And this how I achieve plaster the view onto my window.
WhiteView *whiteBackgroundView = [[WhiteView alloc] initWithFrame:self.window.frame];
[self.window.contentView addSubview:whiteBackgroundView positioned:NSWindowBelow relativeTo:self.window.contentView];
What do I need to do to correctly allow for my window's background to be fully covered in white?
First, the simple solution is to use -[NSWindow setBackgroundColor:] to just set the window's background color. No need for a view.
If you're still interested in how to fix the view-based approach, probably what's wrong is that you haven't set the autoresizing mask of the view to make it follow the changes in the window size. For example, you could do [whiteBackgroundView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable].
However, you could also set the whiteBackgroundView as the window's contentView rather than as a subview of it. The window's content view is always kept at the size necessary to fill the window's content rect. All of the other views of your window would be subviews of the white background view. In my opinion, this is better than making it a sibling that just happens to be at the back. Using relative ordering among siblings views to achieve a particular rendering order is a hack.
Finally, there's no reason to invoke super's implementation in your -drawRect: if the superclass is NSView itself. NSView doesn't do any drawing in its -drawRect:. Also, your subclass takes over full responsibility for the entire drawn contents of its bounds, so you'd overdraw whatever super had drawn, anyway. (Also, you need only fill dirtyRect rather than [self bounds].)
While you're at it, since your class fills its bounds, you should override -isOpaque to return YES for optimization.
Update: regarding the frame of the view: if it's not going to be the window's content view, then you want to set its frame to be its prospective superview's bounds. So, you should have used self.window.contentView.bounds if you wanted whiteBackgroundView to fill the content view.
More generally, if you want the content rect of a window, you would do [window contentRectForFrameRect:window.frame]. But if a view is going to be a window's content view, there's no need to set its frame to anything in particular. It will be resized automatically.
Update 2:
To transfer the view hierarchy from the original content view to the new content view (when you're making the white background view the content view):
NSArray* subviews = [self.window.contentView.subviews copy];
[subviews makeObjectsPerformSelector:#selector(removeFromSuperview)];
[whiteBackgroundView setSubviews:subviews];
[subviews release];
(Written for manual retain-release. If using ARC, just drop the -release invocation.)
Regarding the frame to use, as mentioned in the first update: keep in mind that the view's frame should be expressed in the coordinate system of its superview. So, as I said, self.window.contentView.bounds would work if you're putting the new view into the content view. The window's frame and content rect are in screen coordinates. They would be completely incorrect for positioning a view.

UITableViewCell frame isn't updated upon changes

I have a custom UITableViewCell to implement swiping horizontally with UIAttachmentBehavior and a UIPanGestureRecognizer. Here's the relevant portion, in the UITableViewCell subclass:
- (void)panDidChange:(UIPanGestureRecognizer *)gesture {
CGPoint location = [gesture locationInView:[self tableView]];
location.y = CGRectGetMidY(self.frame);
switch (gesture.state) {
case UIGestureRecognizerStateBegan: {
self.panAttachment = [[UIAttachmentBehavior alloc] initWithItem:self attachedToAnchor:location];
[self.animator addBehavior:self.panAttachment];
break;
}
Then, when UIGestureRecognizerStateChanged, I just set the self.panAttachment.anchorPoint as the location.
Now, this works fine from startup, but as soon as I add a cell to the tableView or delete a cell from the tableView and try to swipe, the cell moves to it's previous position before the tableView change (one cell down if a cell was deleted, one cell up if a cell was added). As far, as I can tell, this is because the cell's frame isn't being updated when it's position in the tableView changes, so location's y coordinate is out of sync. I've tried [self setNeedsDisplay] and all other "Update view" methods I could find, to no avail.
I am creating the animator referenced above like so:
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:[self tableView]];
It looks like the issue is in a couple different places:
Your animator is being initialized with your entire table view as the reference view.
You are then creating the attachment behavior with an anchor of midY in the tableView's coordinate system, meaning your attachment behavior is now anchored to a point in the tableView's reference coordinate system that is wherever the center point of the cell is located. I bet if you tried to scroll your tableView, weird things would also happen.
Depending on what your goal is for your dynamics behavior, you need to modify both your animator's reference frame and attachment's anchor point in order to fully define what physics simulation you are trying to achieve. Based on the above, it looks like you are trying to simply move the entire cell on the pan. I would recommend that you try moving only a specific container view inside your view hierarchy instead since the table view is going to be placing your cells directly using frames.
If you are looking to provide a pan with a snap-back effect on cancel, then this effect might be better implemented using a couple of UIAttachmentBehavior instances. One behavior instance to anchor the view you are wanting to swipe to it's resting position, and one to actually perform the panning and move the view like you are doing above during the pan. You can tweak the interaction between the two interactions by changing the damping and frequency. This question is a good reference for dragging in general using dynamic items.
In summary to fix your current issue:
Define your animator's reference bounds to the cell's coordinate system, preferably using cell.contentView
Have your cell manage the animator directly since the animator should only be concerned with a view in it's coordinate system. Your animator should have no knowledge of the table view's view hierarchy. Add/remove UIDynamicBehavior items from your cell's animator.

Using CATransform3D

I'm trying to make a rotation on a tableview to tilt the table (to give the effect of a 3d text crawl similar to the star wars opening crawl).
After looking around I found this question objective-с CALayer UIView real rotation
and the accepted answer seems to do what I want, however when I apply the code to my TableView it does nothing and the table appears as usual.
This is the code I am copying:
float distance = 50;
CATransform3D basicTrans = CATransform3DIdentity;
basicTrans.m34 = 1.0 / -distance;
_tableView.layer.transform = CATransform3DRotate(basicTrans, M_PI_4, 1.0f, 0.0f, 0.0f);
I'm placing this in my viewDidLoad method after creating my array of Strings (that populate the tableView)
I currently only have three other methods in the Controller:
didReceiveMemoryWarning (automatically addd when project created)
tableView: numberOfRowsInSelection (used for setting up the table view)
tableView: cellForRowAtIndexPath (used for setting up the table view and setting the cells text form the array)
My understanding is that the tableview has a CALayer, and that the CATransform3D manipulates this to give the representation of the view in a 3d space. If my understanding is correct then I don't get why the list is shown normally on screen? I appreciate the numbers my not give the effect I want yet but they should at lest effect the appearance of the tableView on screen.
Also I have imported QuartzCore etc and added it in linked frameworks
Solution is to use the code marked as OLD answer in the the - (UITableViewCell *)tableView: cellForRowAtIndexPath: method after the cell is checked for being null.
Since the approach suggested below has not worked, another thing I would try out is applying the transform to the UITableView's subviews. Actually, UITableView is a UIScrollView, so it is just a container for subviews that make up the real content of the table view. I would try something like this:
for (UIView* subview in tableView.subviews) {
subview.layer.transform = ...;
}
I have never inspected a table view subviews hierarchy, so I cannot say whether this will work or you should rather apply the transform to just one of the subviews, but I hope this can lead you in the right direction.
OLD ANSWER:
You could try setting your table view's layer sublayerTransform instead of `transform':
You typically use this property to add perspective and other viewing effects to embedded layers. You add perspective by setting the sublayer transform to the desired projection matrix. The default value of this property is the identity transform.
(source).
I am suggesting this based on the hypothesis that a UITableView has quite a complex structure in terms of subviews, so transforming just the view's layer might have no effect. I haven't tried it, though, so I cannot guarantee it will work.

How do I dynamically add images to a UIScrollView?

I'm working on an an app that updates both a UITableView and a UIScrollView. I like how UITableView updating works. As I add, remove, or update items, I call insertRowsAtIndexPaths:withRowAnimation:, reloadRowsAtIndexPaths:withRowAnimation:, and deleteRowsAtIndexPaths:withRowAnimation: as appropriate. However, updating the UIScrollView has turned out to be more of a challenge.
So how can I do it? My UIScrollView contains images, and I want to be able to insert, update, and remove individual images -- with animation -- efficiently and smoothly, and perhaps in random order. How does one do this sort of thing?
As an example, I have code like this in my UIScrollViewDelegate implementation for adding a new image:
if (newImageindex == currentImageIndex) {
[scrollView setNeedsDisplay];
[scrollView setContentOffset:portalView.contentOffset animated:YES];
} else if (newImageIndex < currentImageIndex) {
currentImageIndex++;
CGPoint offset = portalView.contentOffset;
offset.x += portalView.frame.size.width;
portalView.contentOffset = offset;
}
This is close, I think, but not quite right. I end up with the images added, but the UIScrollView seems to scroll to a position before the first image in the view. If I start scrolling it by hand, the first image appears. It's like it's scrolled to position -1 in my images.
The example may not help to highlight my problem much, but surely it's a common need to dynamically rejigger the images appearing in a UIScrollView. What's the state of the art on this?
A UIScrollView is not so specialized and structured as a UITableView. The former is a generic view that scrolls and zooms anything you put in it, and you can put any subviews into it anywhere, whereas the latter is made especially to display lists of cells stacked on top of each other.
So the answer is: you need to animate the subviews in the scrollview yourself. There are various libraries like Three20 which provide frameworks for creating more advanced views, you'll need to use a suitable component from one of those or roll your own.