Cocoa NSView subview blocking drag/drop - objective-c

I have an NSView subclass which registers for drag files in init method like this:
[self registerForDraggedTypes:[NSArray arrayWithObject:NSFilenamesPboardType]];
The drag drop works perfectly fine, but if I add a subview to this view with the exact same frame, it doesn't work any more. My guess is that the subview is block the drag event to go to super view. Can can I avoid that? Thanks
Also, I know I am asking two questions, but I don't want to create a new topic just for this: When I am dragging, my cursor doesn't change to the "+" sign like with other drags, how can I do that?
Thanks again.
UPDATE:
Here's the how I have it set up in my IB:
The DrawView is the custom class I was talking about that registered for draggedtypes. And the Image view simply is a subview, I dragged an image from the media section...
If it helps, here's my relevant code for DragView:
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender {
return NSDragOperationCopy;
}
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender {
NSPasteboard *pboard;
pboard = [sender draggingPasteboard];
NSArray *list = [pboard propertyListForType:NSFilenamesPboardType];
if ([list count] == 1) {
BOOL isDirectory = NO;
NSString *fileName = [list objectAtIndex:0];
[[NSFileManager defaultManager] fileExistsAtPath:fileName
isDirectory: &isDirectory];
if (isDirectory) {
NSLog(#"AHH YEA");
} else {
NSLog(#"NOO");
}
}
return YES;
}

The answer to the second part of your question is this:
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender{
return NSDragOperationCopy;
}
if you return NSDragOperationCopy, you get the mouse badge for a copy operation. (You can and should, of course, not just return NSDragOperationCopy unconditionally; check the objects on the pasteboard to see if you can accept them.)
I'm not sure what the answer to the first part of your question is, because I'm unable to recreate the subview blocking effect.
Okay, the answer is unfortunately that you can't. The image you dragged is contained in an NSImageView, and NSImageViews accept drag events themselves, so it's grabbing the drag event and not doing anything with it. If your subview was a custom class, you could either a) not implement drag and drop, in which case the drags would be passed through; b) implement drag and drop to accept drags for the subview. In this case, you're using a class over which you don't have any control. If all you want it to do is display an image, you should be able to make another NSView subclass that does nothing but draw the image in drawRect:

As mentioned in the comments, NSImageViews have their own drag and drop enabled by default (used for accepting images that are dragged onto the NSImageView). If you don't want to use this behavior and instead want to use the super view's drag and drop behavior, you'll want to unregister the dragging behavior in the NSImageView.
Objc:
[imageView unregisterDraggedTypes];
Swift:
imageView.unregisterDraggedTypes()
(in case anyone else stumbled across this question first and not the linked one in the comments)

Related

Change Label With Tag Only

I am creating an application with multiple buttons and corresponding labels.
The layout is in a nib (.xib) file which is connected to the class I have the code below in. I'm trying to avoid using an IBOutlet for every label, since this would create a lot of extra code.
I know there is a way to reference a label using the tag:
UILabel *countLabel = (UILabel *)[self.view viewWithTag:numOfTag];
I called this within my buttonPressed method. However, when I try to change the text
[countLabel setText:#"newLabel"];
the app crashes. Am I accessing it wrong? I'm new to XCode..Help!
Thanks :)
This is not the correct way of doing it. Firstly whenever you creating UILabel in your XIB file, you can name them there only so technically, there is no need for you to set the label values in the connected view controller. Secondly, even if you want to reset the labels at the later stage, connecting them using IBOutlets is the best bet. Scanning through all sub views and then finding a particular subview with its tag is more tedious and should be avoided in this case.
EDIT:
For your case where you need to create more than 50 buttons and labels, I would suggest you to create a custom UIView with UIButton and UILabel in it. And then in the view where you want to display them, create objects in a for loop and display them. And when a UIButton is tapped it will be easy for you to fetch the associated UILabel though the custom UIView object.
Your label must not be found with viewWithTag:, so it will be nil. And when you try to access property of nil control, it might give you a crash.
Check if your label is not nil and check if it is label using
if(countLabel != nil && [countLabel isKindOfClass:[UILabel class]])
[countLabel setText:#"newLabel"];
else
NSLog(#"countLabel is not found");
viewWithTag: will search the view in the receiver’s hierarchy whose tag property matches the value in the tag parameter. This will return first control it will encounter with tag in hierarchy. So make sure your tag needs to so unique in entire view. Just make sure you are giving immediate parent of Label instead of self.view.
Edit:
To can try this way also if you are not able to track issue.
for (UIView *child in self.view.subviews) {
if([child isKindOfClass:[UILabel class]] && [child tag] == numOfTag)
{
[child setText:#"newLabel"];
break;
}
}

Changing view repositions them at (0,0)

I created a simple window with the purpose of being something like a "Wizard" (I know Apple guidelines basically forbid you to, I tried to convince the customer, but whatever.
It is just a simple view with two Custom Views inside, one in the bottom part which contains a "previous" and "next" button, and a bigger view at the top which takes most of the space.
I called the bottom view "NavigationView" and the top one "ContainerView".
I created an array to hold a series of views the user is supposed to navigate through with the "next" and "previous" buttons.
So, here's my code.
- (IBAction) next:(id)sender{
currentViewIndex++;
[self animatePushView:YES];
}
- (IBAction)previous:(id)sender{
currentViewIndex--;
[self animatePushView:NO];
}
- (void) animatePushView:(BOOL)forward{
NSView *nextView = [viewCollection objectAtIndex:currentViewIndex];
for (NSView *subView in [containerView subviews]) {
[subView removeFromSuperview];
}
[containerView addSubview:nextView];
[nextView setFrame:containerView.bounds];
[containerView setNeedsDisplay:YES];
}
It's pretty straightforward I think. I have an array which contains the next view to be displayed.
What happens is that I find the next view centered in the lower left part of the ContainerView. Why does this happen?
Also, as you may have guessed, I'm a newbie at managing views, even though I've been working on objective-c for quite some time, so if there's some best practice I'm missing I'm open to suggestions.
Thanks!
EDIT:
I forgot to add:
Some of these views have different sizes, and I would like to be able to change the window size according to the view size.
[nextView setFrame:containerView.bounds];
You are assigning container view bounds to the next view frame (doc).
What you probably want is assigning the current view frame to the next view frame, and possibly adjust width and height.
Keep a reference to the current displayed view, something like this (_currentView is an ivar of type NSView *) :
- (IBAction) next:(id)sender{
currentViewIndex++;
[self animatePushView:YES];
}
- (IBAction)previous:(id)sender{
currentViewIndex--;
[self animatePushView:NO];
}
- (void) animatePushView:(BOOL)forward{
NSView *nextView = [viewCollection objectAtIndex:currentViewIndex];
[nextView setFrame:_currentView.frame];
[_currentView removeFromSuperview]; // _currentView is retained in the collection
[containerView addSubview:nextView];
_currentView = nextView;
[containerView setNeedsDisplay:YES];
}
Ok, I figured it out.. Finally..
The problem was that the first view I had to show was already contained in the container view in the .xib file.
I don't really know why, but it probably caused some problem with the retain count of the container view, because it was released on the first click.
Releasing the container view would reposition the view on (0,0) probably because its frame was null, and the view would flash because it wasn't retained correctly.
Removing the view from the .xib file and adding it via code works properly anyway.

How to get a reference to the view controller of a superview?

Is there a way to get a reference to the view controller of my superview?
There were several instances that I needed this on the past couple of months, but didn't know how to do it. I mean, if I have a custom button on a custom cell, and I wish to get a reference of the table view controller that controls the cell I`m currently in, is there a code snippet for that? Or is it something that I should just solve it by using better design patterns?
Thanks!
Your button should preferably not know about its superviews view controller.
However, if your button really needs to message objects that it shouldn't know the details about, you can use delegation to send the messages you want to the buttons delegate.
Create a MyButtonDelegate protocol and define the methods that everyone that conforms to that protocol need to implement (the callback). You can have optional methods as well.
Then add a property on the button #property (weak) id<MyButtonDelegate> so that any class of any kind can be set as the delegate as long as it conforms to your protocol.
Now the view controller can implement the MyButtonDelegate protocol and set itself as the delegate. The parts of the code that require knowledge about the view controller should be implemented in the delegate method (or methods).
The view can now send the protocol messages to its delegate (without knowing who or what it is) and the delegate can to the appropriate thing for that button. This way the same button could be reused because it doesn't depend on where it is used.
When I asked this question I was thinking of, in a situation where I have custom cells with buttons on them, how can the TableViewController know which cell's button was tapped.
More recently, reading the book "iOS Recipes", I got the solution:
-(IBAction)cellButtonTapped:(id)sender
{
NSLog(#"%s", __FUNCTION__);
UIButton *button = sender;
//Convert the tapped point to the tableView coordinate system
CGPoint correctedPoint = [button convertPoint:button.bounds.origin toView:self.tableView];
//Get the cell at that point
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:correctedPoint];
NSLog(#"Button tapped in row %d", indexPath.row);
}
Another solution, a bit more fragile (though simpler) would be:
- (IBAction)cellButtonTapped:(id)sender
{
// Go get the enclosing cell manually
UITableViewCell *parentCell = [[sender superview] superview];
NSIndexPath *pathForButton = [self.tableView indexPathForCell:parentCell];
}
And the most reusable one would be to add this method to a category of UITableView
- (NSIndexPath *)prp_indexPathForRowContainingView:(UIView *)view
{
CGPoint correctedPoint = [view convertPoint:view.bounds.origin toView:self];
return [self indexPathForRowAtPoint:correctedPoint];
}
And then, on your UITableViewController class, just use this:
- (IBAction)cellButtonTapped:(id)sender
{
NSIndexPath *pathForButton = [self.tableView indexPathForRowContainingView:sender];
}
If you know which class is the superview of your view controller, you can just iterate through the subviews array and typecheck for your superclass.
eg.
UIView *view;
for(tempView in self.subviews) {
if([tempView isKindOfClass:[SuperViewController class] ])
{
// you got the reference, do waht you want
}
}

What could I add to this code to make it not remove the UIToolbar?

NSArray *subviewsList = [[NSArray alloc] initWithArray:[self.view subviews]];
for (UIView *aView in subviewsList) {
NSLog(#"%#",subviewsList);
if (![aView isEqual:sender]) {
[aView removeFromSuperview];
}
}
[subviewsList release];
I have it not remove the UIButton that you click to actually call this code, however, I haven't figured out how to get it not to remove the UIToolbar that I added to the screen via IB. Any suggestions?
EDIT: I should have been more clear, I'm sorry. The code was done to remove the ton of UIImageViews from the screen. I didn't want it to remove the uibutton that calls the method, or the toolbar.
EDIT:
This works. :)
if (![aView isEqual:sender] && ![aView isKindOfClass:[UIToolbar class]]) {
If ([aView isKindOfClass:[UIToolBar class]]) {
// the view is a uitoolbar
} else {
[aView removeFromSuperView];
}
Send from iphone, maybe syntax errors :) but this let you check if your subview is from a specific class
Hope this helps
Any number of things could be happening. Is your UIToolbar a subview or sublayer of one of the aView's you are removing?
Without knowing what all else is happening in your views (in code or in IB), it's hard to say, but also make sure that if you're adding any views or layers that they're not covering anything up. (Even if something like a UIButton is visible, it may not respond if covered by another view.)

Detect horizontal panning in UITableView

I'm using a UIPanGestureRecognizer to recognize horizontal sliding in a UITableView (on a cell to be precise, though it is added to the table itself). However, this gesture recognizer obviously steals the touches from the table. I already got the pangesturerecognizer to recognize horizontal sliding and then snap to that; but if the user starts by sliding vertical, it should pass all events from that touch to the tableview.
One thing i have tried was disabling the recognizer, but then it wouldn't scroll untill the next touch event. So i'd need it to pass the event right away then.
Another thing i tried was making it scroll myself, but then you will miss the persistent speed after stopping the touch.
Heres some code:
//In the viewdidload method
UIPanGestureRecognizer *slideRecognizer = [[UIPanGestureRecognizer alloc]initWithTarget:self action:#selector(sliding:)];
[myTable addGestureRecognizer:slideRecognizer];
-(void)sliding:(UIPanGestureRecognizer *)recognizer
{
if (recognizer.state == UIGestureRecognizerStateBegan)
{
CGPoint translation = [recognizer translationInView:favoritesTable];
if (sqrt(translation.x*translation.x)/sqrt(translation.y*translation.y)>1) {
horizontalScrolling = YES; //BOOL declared in the header file
NSLog(#"horizontal");
//And some code to determine what cell is being scrolled:
CGPoint slideLocation = [recognizer locationInView:myTable];
slidingCell = [myTable indexPathForRowAtPoint:slideLocation];
if (slidingCell.row == 0) {
slidingCell = nil;
}
}
else
{
NSLog(#"cancel");
}
if (recognizer.state == UIGestureRecognizerStateEnded || recognizer.state == UIGestureRecognizerStateCancelled)
{
horizontalScrolling = NO;
}
if (horizontalScrolling)
{
//Perform some code
}
else
{
//Maybe pass the touch from here; It's panning vertically
}
}
So, any advice on how to pass the touches?
Addition: I also thought to maybe subclass the tableview's gesture recognizer method, to first check if it's horizontal; However, then i would need the original code, i suppose... No idea if Apple will have problems with it.
Also: I didn't subclass the UITableView(controller), just the cells. This code is in the viewcontroller which holds the table ;)
I had the same issue and came up with a solution that works with the UIPanGestureRecognizer.
In contrast to Erik I've added the UIPanGestureRecognizer to the cell directly, as I need just one particular cell at once to support the pan. But I guess this should work for Erik's case as well.
Here's the code.
- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer
{
UIView *cell = [gestureRecognizer view];
CGPoint translation = [gestureRecognizer translationInView:[cell superview]];
// Check for horizontal gesture
if (fabsf(translation.x) > fabsf(translation.y))
{
return YES;
}
return NO;
}
The calculation for the horizontal gesture is copied form Erik's code – I've tested this with iOS 4.3.
Edit:
I've found out that this implementation prevents the "swipe-to-delete" gesture. To regain that behavior I've added check for the velocity of the gesture to the if-statement above.
if ([gestureRecognizer velocityInView:cell].x < 600 && sqrt(translate...
After playing a bit on my device I came up with a velocity of 500 to 600 which offers in my opinion the best user experience for the transition between the pan and the swipe-to-delete gesture.
My answer is the same as Florian Mielke's, but I've simplified and corrected it some.
How to use:
Simply give your UIPanGestureRecognizer a delegate (UIGestureRecognizerDelegate). For example:
UIPanGestureRecognizer *panner = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(panDetected:)];
panner.delegate = self;
[self addGestureRecognizer:panner];
Then have that delegate implement the following method:
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
CGPoint translation = [(UIPanGestureRecognizer *)gestureRecognizer translationInView:gestureRecognizer.view.superview];
return fabsf(translation.x) > fabsf(translation.y);
}
Maybe you can use the UISwipeGestureRecognizer instead? You can tell it to ignore up/down swipes via the direction property.
You may try using the touch events manually instead of the gesture recognizers. Always passing the event back to the tableview except when you finally recognize the swipe gesture.
Every class that inherits from UIResponder will have the four touch functions (began, ended, canceled, and moved). So the simplest way to "forward" a call is to handle it in your class and then call it explicitly on the next object that you would want to handle it (but you should make sure to check if the object responds to the message first with respondsToSelector: since it is an optional function ). This way, you can detect whatever events you want and also allow the normal touch interaction with whatever other elements need it.
Thanks for the tips! I eventually went for a UITableView subclass, where i check if the movement is horizontal (in which case i use my custom behaviour), and else call [super touchesMoved: withEvent:];.
However, i still don't really get why this works. I checked, and super is a UITableView. It appears i still don't fully understand how this hierarchy works. Can someone try and explain?