objective-c refactor method - objective-c

I have the following code:
-(void)textFieldDidBeginEditing:(UITextField *)textField
{
CGRect textFieldRect = [self.view.window convertRect:textField.bounds fromView:textField];
CGRect viewRect = [self.view.window convertRect:self.view.bounds fromView:self.view];
...
}
As you can see it passes in a UITextField. I also have this code duplicated in the same ViewController but passing in a UITextView.
I want to be able to refactor this into a single method which passes in either the UITextField or UITextView? How can I do this?
Also this code appears in other View Controllers, so ideally I'd like to place it in a helper class, very new to iOS so not sure where to go from here.
I've removed most of the code from the method for brevity, but what it does is it slides the UI controls into view when the iOS keyboard appears.

You can expect UIView, as it seems you don't use any special text properties from those views.
-(void)textFieldDidBeginEditing:(UIView *)textField
{
CGRect textFieldRect = [self.view.window convertRect:textField.bounds fromView:textField];
CGRect viewRect = [self.view.window convertRect:self.view.bounds fromView:self.view];
// ...
}

Call a helper methods, that expects a UIView, the common super class.
-(void)textFieldDidBeginEditing:(UITextField *)textField
{
return [self textBeginEditing:textField];
}
-(void)textViewDidBeginEditing:(UITextView *)textView
{
return [self textBeginEditing:textView];
}
-(void)textBeginEditing:(UIView *)view
{
//and if you need to do something, where you need to now, if it is a textView or a field, use
if([view isKindOfClass:[UITextField class]]){
//…
} else if([view isKindOfClass:[UITextView class]]){
//…
}
}

Related

Drag and Drop from the Finder to a NSTableView weirdness

I'm trying to understand how best to impliment drag and drop of files from the Finder to a NSTableView which will subsequently list those files.
I've built a little test application as a proving ground.
Currently I have a single NSTableView with FileListController as it's datasourse. It's basically a NSMutableArray of File objects.
I'm trying to work out the best / right way to impliment the drag and drop code for the NSTableView.
My first approach was to subclass the NSTableView and impliment the required methods :
TableViewDropper.h
#import <Cocoa/Cocoa.h>
#interface TableViewDropper : NSTableView
#end
TableViewDropper.m
#import "TableViewDropper.h"
#implementation TableViewDropper {
BOOL highlight;
}
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
// Initialization code here.
NSLog(#"init in initWithCoder in TableViewDropper.h");
[self registerForDraggedTypes:#[NSFilenamesPboardType]];
}
return self;
}
- (BOOL)performDragOperation:(id < NSDraggingInfo >)sender {
NSLog(#"performDragOperation in TableViewDropper.h");
return YES;
}
- (BOOL)prepareForDragOperation:(id)sender {
NSLog(#"prepareForDragOperation called in TableViewDropper.h");
NSPasteboard *pboard = [sender draggingPasteboard];
NSArray *filenames = [pboard propertyListForType:NSFilenamesPboardType];
NSLog(#"%#",filenames);
return YES;
}
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
{
highlight=YES;
[self setNeedsDisplay: YES];
NSLog(#"drag entered in TableViewDropper.h");
return NSDragOperationCopy;
}
- (void)draggingExited:(id)sender
{
highlight=NO;
[self setNeedsDisplay: YES];
NSLog(#"drag exit in TableViewDropper.h");
}
-(void)drawRect:(NSRect)rect
{
[super drawRect:rect];
if ( highlight ) {
//highlight by overlaying a gray border
[[NSColor greenColor] set];
[NSBezierPath setDefaultLineWidth: 18];
[NSBezierPath strokeRect: rect];
}
}
#end
The draggingEntered and draggingExited methods both get called but prepareForDragOperation and performDragOperation don't. I don't understand why not?
Next I thought I'll subclass the ClipView of the NSTableView instead. So using the same code as above and just chaging the class type in the header file to NSClipView I find that prepareForDragOperation and performDragOperation now work as expected, however the ClipView doesn't highlight.
If I subclass the NSScrollView then all the methods get called and the highlighting works but not as required. It's very thin and as expected round the entire NSTableView and not just the bit below the table header as I'd like.
So my question is what is the right thing to sublclass and what methods do I need so that when I peform a drag and drop from the Finder, the ClipView highlights properly and prepareForDragOperation and performDragOperation get called.
And also when performDragOperation is successful how can this method call a method within my FileListController telling it to create a new File object and adding it to the NSMutableArray?
Answering my own question.
It seems that subclassing the NSTableView (not the NSScrollView or the NSClipView) is the right way to go.
Including this method in the subclass :
- (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender {
return [self draggingEntered:sender];
}
Solves the problem of prepareForDragOperation and performDragOperation not being called.
To allow you to call a method within a controller class, you make the delagate of your NSTextView to be the controller. In this case FileListController.
Then within performDragOperation in the NSTableView subclass you use something like :
NSPasteboard *pboard = [sender draggingPasteboard];
NSArray *filenames = [pboard propertyListForType:NSFilenamesPboardType];
id delegate = [self delegate];
if ([delegate respondsToSelector:#selector(doSomething:)]) {
[delegate performSelector:#selector(doSomething:)
withObject:filenames];
}
This will call the doSomething method in the controller object.
Updated example project code here.

Setting Bool in different classes

I have the following code where after a bool is true I want to add a drawing to my rect. here is the code I have but for some reason it is either not setting the bool or calling the setNeedsDisplay. Am I referencing to the other class properly? thanks
//in AppController.m
-(IBAction)colorToggle:(id)sender
{
if ([colorFilter state] == NSOnState)
{
CutoutView *theView = [[CutoutView alloc] init];
[theView setFilterEnabled:YES];
}
}
//in cutoutView.m
- (void)drawRect:(NSRect)dirtyRect
{
[[[NSColor blackColor]colorWithAlphaComponent:0.9]set];
NSRectFill(dirtyRect);
//this is what i want to be drawn when my bool is true and update the drawRect
if (filterEnabled == YES) {
NSRectFillUsingOperation(NSMakeRect(100, 100, 300, 300), NSCompositeClear);
[self update];
}
}
-(void)update
{
[self setNeedsDisplay:YES];
}
OK, you know how not every UILabel is the same? Like, you can remove one UILabel from a view without all the others disappearing too? Well, your CutoutView is the same way. When you write CutoutView *theView = [[CutoutView alloc] init]; there, that creates a new CutoutView that isn't displayed anywhere. You need to talk to your existing CutoutView (probably by hooking up an outlet, but there are any number of perfectly valid designs that will accomplish this goal).
You are forgetting to call the drawRect: method, it should looks like this:
CutoutView *theView = [[CutoutView alloc] init];
[theView setFilterEnabled:YES];
[theView setNeedsDisplay];
From the docs:
When the actual content of your view changes, it is your
responsibility to notify the system that your view needs to be
redrawn. You do this by calling your view’s setNeedsDisplay or
setNeedsDisplayInRect: method of the view.

Custom MKMapView callout not responding to touch events

I have a custom UIView that I am displaying as a callout when the user clicks on a custom annotation on an MKMapView.
To achieve this I have subclassed MKAnnotationView and overloaded the -setSelected:selected animated: method as suggested in this answer. Basically, I am adding my custom view as a subview of my MKAnnotationView subclass.
The problem is that I can't interact with the callout, which contains a button and a scrollable webview, at all. What's more, if the callout hides an annotation and I press the callout at the approximate location of that hidden annotation, the callout will get dismissed and a new one will be shown.
// TPMapAnnotationView.m
#implementation TPMapAnnotationView
- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
[super setSelected:selected animated:animated];
if(selected)
{
TPMapAnnotation* anno = ((TPMapAnnotation*)self.annotation);
QuickInfoView* qi = [[QuickTabView alloc] initWithFrame:CGRectMake(0, 0, 440, 300)];
[qi displayDataForAnnotation:anno];
[self addSubview:qi];
// some animiation code that doesn't change things either way
}
else
{
[[self.subviews objectAtIndex:0] removeFromSuperview];
}
}
The code below creates the TPMapAnnotationView.
// this is in the view controller that contains the MKMapView
- (MKAnnotationView *) mapView:(MKMapView *) mapView viewForAnnotation:(id) annotation
{
if ([annotation isKindOfClass:[TPMapAnnotation class]])
{
TPMapAnnotationView *customAnnotationView = (TPMapAnnotationView *)[myMap dequeueReusableAnnotationViewWithIdentifier:#"TPAnn"];
if (customAnnotationView == nil)
{
customAnnotationView = [[TPMapAnnotationView alloc] initWithAnnotation:annotation
reuseIdentifier:#"TPAnn"];
}
[customAnnotationView setImage:annotationImage];
return customAnnotationView;
}
return nil; // blue radar circle for MKUserLocation class.
}
This is a well known problem. Anything you add on AnnotationView will not detect touches. There is good open source project for this problem. http://dev.tuyennguyen.ca/wp-content/uploads/2011/03/CustomMapAnnotationBlogPart1.zip, http://dev.tuyennguyen.ca/wp-content/uploads/2011/03/CustomMapAnnotationBlogPart21.zip
EDIT:
Yes. I also tried hard to add uibuttons to my own custom annotationView but then I stumbled upon this project and found that his custom annotationView is actually a annotation.
Anyway if you want to to change height of annotatioView then you can set
- (MKAnnotationView *) mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>) annotation{
calloutMapAnnotationView.contentHeight = height;
calloutMapAnnotationView.titleHeight = 25;
}
here, titleHeight is property added to CalloutMapAnnotationView which determines height of "gloss" in drawRectMethod
- (void)drawRect:(CGRect)rect {
glossRect.size.height = self.titleHeight;
}
if you are having any difficulty please let me know.
And also the link to original blogpost:
http://dev.tuyennguyen.ca/?p=298
I resolved this problem. Click anything in CalloutView,the map will not get touch.My calloutview is custom have tabbleview
1 - In file MapviewController.h you will add delegate : UIGestureRecognizerDelegate
2 - and in file MapViewController.m implement method - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
-In my mapView when you click 1 time on Map it will go in this method 3 time. So I limit touch will action.the first touch will action.
- In myCalloutView have tabbleView, if tabbleView receive touch It will return false touch for Map, it will make your tabbleview can get touch.It same for your button.
Note : in NSlog hit test View : will have name of view item you want it have touch.
example my view : isEqualToString:#"UITableViewCellContentView"]
static int count=0;
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch{
NSLog(#"hit test view %#",[touch view]);
if(count >0 && count<=2)
{
count++;
count=count%2;
return FALSE;
}
count++;
if ([[[[touch view] class] description] isEqualToString:#"UITableViewCellContentView"]) {
return FALSE;
}
return TRUE;
}

Click through NSView

I have an NSView containing multiple subviews. One of those subviews is transparent and layered on top.
I need to be able to click through this view down to the subviews below (so that the view below gets first responder status), but all the mouse events get stuck on the top view (alpha is 1, because I draw stuff in it - so it should only click through transparent areas).
I actually expected this to work, since normally it does. What's wrong?
Here's another approach. It doesn't require creating a new window object and is simpler (and probably a bit more efficient) than the findNextSiblingBelowEventLocation: method above.
- (NSView *)hitTest:(NSPoint)aPoint
{
// pass-through events that don't hit one of the visible subviews
for (NSView *subView in [self subviews]) {
if (![subView isHidden] && [subView hitTest:aPoint])
return subView;
}
return nil;
}
I circumvented the issue with this code snippet.
- (NSView *)findNextSiblingBelowEventLocation:(NSEvent *)theEvent {
// Translate the event location to view coordinates
NSPoint location = [theEvent locationInWindow];
NSPoint convertedLocation = [self convertPointFromBase:location];
// Find next view below self
NSArray *siblings = [[self superview] subviews];
NSView *viewBelow = nil;
for (NSView *view in siblings) {
if (view != self) {
NSView *hitView = [view hitTest:convertedLocation];
if (hitView != nil) {
viewBelow = hitView;
}
}
}
return viewBelow;
}
- (void)mouseDown:(NSEvent *)theEvent {
NSView *viewBelow = [self findNextSiblingBelowEventLocation:theEvent];
if (viewBelow) {
[[self window] makeFirstResponder:viewBelow];
}
[super mouseDown:theEvent];
}
Here's a Swift 5 version of figelwump's answer:
public override func hitTest(_ point: NSPoint) -> NSView? {
// pass-through events that don't hit one of the visible subviews
return subviews.first { subview in
!subview.isHidden && nil != subview.hitTest(point)
}
}
Here's a Swift 5 version of Erik Aigner's answer:
public override func mouseDown(with event: NSEvent) {
// Translate the event location to view coordinates
let convertedLocation = self.convertFromBacking(event.locationInWindow)
if let viewBelow = self
.superview?
.subviews // Find next view below self
.lazy
.compactMap({ $0.hitTest(convertedLocation) })
.first
{
self.window?.makeFirstResponder(viewBelow)
}
super.mouseDown(with: event)
}
Put your transparent view in a child window of its own.

UIGestureRecognizer blocks subview for handling touch events

I'm trying to figure out how this is done the right way. I've tried to depict the situation:
I'm adding a UITableView as a subview of a UIView. The UIView responds to a tap- and pinchGestureRecognizer, but when doing so, the tableview stops reacting to those two gestures (it still reacts to swipes).
I've made it work with the following code, but it's obviously not a nice solution and I'm sure there is a better way. This is put in the UIView (the superview):
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if([super hitTest:point withEvent:event] == self) {
for (id gesture in self.gestureRecognizers) {
[gesture setEnabled:YES];
}
return self;
}
for (id gesture in self.gestureRecognizers) {
[gesture setEnabled:NO];
}
return [self.subviews lastObject];
}
I had a very similar problem and found my solution in this SO question. In summary, set yourself as the delegate for your UIGestureRecognizer and then check the targeted view before allowing your recognizer to process the touch. The relevant delegate method is:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldReceiveTouch:(UITouch *)touch
The blocking of touch events to subviews is the default behaviour. You can change this behaviour:
UITapGestureRecognizer *r = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(agentPickerTapped:)];
r.cancelsTouchesInView = NO;
[agentPicker addGestureRecognizer:r];
I was displaying a dropdown subview that had its own tableview. As a result, the touch.view would sometimes return classes like UITableViewCell. I had to step through the superclass(es) to ensure it was the subclass I thought it was:
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
UIView *view = touch.view;
while (view.class != UIView.class) {
// Check if superclass is of type dropdown
if (view.class == dropDown.class) { // dropDown is an ivar; replace with your own
NSLog(#"Is of type dropdown; returning NO");
return NO;
} else {
view = view.superview;
}
}
return YES;
}
Building on #Pin Shih Wang answer. We ignore all taps other than those on the view containing the tap gesture recognizer. All taps are forwarded to the view hierarchy as normal as we've set tapGestureRecognizer.cancelsTouchesInView = false. Here is the code in Swift3/4:
func ensureBackgroundTapDismissesKeyboard() {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
tapGestureRecognizer.cancelsTouchesInView = false
self.view.addGestureRecognizer(tapGestureRecognizer)
}
#objc func handleTap(recognizer: UIGestureRecognizer) {
let location = recognizer.location(in: self.view)
let hitTestView = self.view.hitTest(location, with: UIEvent())
if hitTestView?.gestureRecognizers?.contains(recognizer) == .some(true) {
// I dismiss the keyboard on a tap on the scroll view
// REPLACE with own logic
self.view.endEditing(true)
}
}
One possibility is to subclass your gesture recognizer (if you haven't already) and override -touchesBegan:withEvent: such that it determines whether each touch began in an excluded subview and calls -ignoreTouch:forEvent: for that touch if it did.
Obviously, you'll also need to add a property to keep track of the excluded subview, or perhaps better, an array of excluded subviews.
It is possible to do without inherit any class.
you can check gestureRecognizers in gesture's callback selector
if view.gestureRecognizers not contains your gestureRecognizer,just ignore it
for example
- (void)viewDidLoad
{
UITapGestureRecognizer *singleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleSingleTap:)];
singleTapGesture.numberOfTapsRequired = 1;
}
check view.gestureRecognizers here
- (void)handleSingleTap:(UIGestureRecognizer *)gestureRecognizer
{
UIEvent *event = [[UIEvent alloc] init];
CGPoint location = [gestureRecognizer locationInView:self.view];
//check actually view you hit via hitTest
UIView *view = [self.view hitTest:location withEvent:event];
if ([view.gestureRecognizers containsObject:gestureRecognizer]) {
//your UIView
//do something
}
else {
//your UITableView or some thing else...
//ignore
}
}
I created a UIGestureRecognizer subclass designed for blocking all gesture recognizers attached to a superviews of a specific view.
It's part of my WEPopover project. You can find it here.
implement a delegate for all the recognizers of the parentView and put the gestureRecognizer method in the delegate that is responsible for simultaneous triggering of recognizers:
func gestureRecognizer(UIGestureRecognizer, shouldBeRequiredToFailByGestureRecognizer:UIGestureRecognizer) -> Bool {
if (otherGestureRecognizer.view.isDescendantOfView(gestureRecognizer.view)) {
return true
} else {
return false
}
}
U can use the fail methods if u want to make the children be triggered but not the parent recognizers:
https://developer.apple.com/reference/uikit/uigesturerecognizerdelegate
I was also doing a popover and this is how I did it
func didTap(sender: UITapGestureRecognizer) {
let tapLocation = sender.locationInView(tableView)
if let _ = tableView.indexPathForRowAtPoint(tapLocation) {
sender.cancelsTouchesInView = false
}
else {
delegate?.menuDimissed()
}
}
You can turn it off and on.... in my code i did something like this as i needed to turn it off when the keyboard was not showing, you can apply it to your situation:
call this is viewdidload etc:
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self selector:#selector(notifyShowKeyboard:) name:UIKeyboardDidShowNotification object:nil];
[center addObserver:self selector:#selector(notifyHideKeyboard:) name:UIKeyboardWillHideNotification object:nil];
then create the two methods:
-(void) notifyShowKeyboard:(NSNotification *)inNotification
{
tap.enabled=true; // turn the gesture on
}
-(void) notifyHideKeyboard:(NSNotification *)inNotification
{
tap.enabled=false; //turn the gesture off so it wont consume the touch event
}
What this does is disables the tap. I had to turn tap into a instance variable and release it in dealloc though.