UIImage not updating when called from another object - objective-c

I have an UIImageView with a method to change the image it displays. It works fine, if the method is triggered from a touch event received by the UIImageView itself, but the same method fails to update the UIImageView, if called from another object, which triggered the touch event. I have an NSLog call in the method in question and thus can see, that the method is called in both cases. But only in one case I can see the actual change of the image in the other case the view is not updated.
When it works, I do not need to setNeedsDisplay or setNeedsLayout, but that is what I tried to fix the problem. Setting needsDisplay and needsLayout in the UIImageView as well as its superview. To no avail. I can see, that the image is actually changed, if I rotate the device, which causes a refresh and I see, that the UIImageView indeed changed.
The superview calls the method eventAtLocation: on an OutletCollection using makeObjectsPerformSelector:withObject:
It sure looks like an embarrassing mistake on my part, but I can't figure it out since hours. I am running out of ideas what to try :-(
Here's the code in question:
- (void)eventAtLocation:(NSValue *)location
{
CGPoint loc = [self.superview convertPoint:[location CGPointValue] toView:self];
if ([self pointInside:loc withEvent:nil]) {
if (!self.isAnimating) {
[self autoSwapImage];
}
}
else{
if (self.isAnimating) {
[self cancelAnimation];
}
}
}
- (void)cancelAnimation
{
self.isAnimating = NO;
[NSObject cancelPreviousPerformRequestsWithTarget:self];
}
- (void) swapImage
{
currentImage++;
if (currentImage > numberOfImages)
currentImage = 1;
NSLog(#"set image to %i", currentImage);
self.image = [UIImage imageNamed:[NSString stringWithFormat:#"img_%i.jpg", currentImage]];
[self setNeedsDisplay];
}
- (void) autoSwapImage
{
self.isAnimating = YES;
[self swapImage];
[self performSelector:#selector(autoSwapImage) withObject:self afterDelay:0.1];
}
// The following works, but if eventAtLocation: is called form the superview swapImage gets called (-> NSLog output appears), but the view is not updated
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//[self autoSwapImage];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
//[NSObject cancelPreviousPerformRequestsWithTarget:self];
}
#end

It looks like you aren't actually setting the UIImageView's image, you are just setting self.image. You need to do something like
self.myImageView.image = [UIImage imageNamed:[NSString stringWithFormat:#"img_%i.jpg", currentImage]];
Also you'll want to put a check in that [UIImage imageNamed:] actually is returning an image, and that its not nil.
UIImage* image = [UIImage imageNamed:#"someImage"];
if(!image){
NSLog(#"%#",#"Image Not Found");
}

Related

check if a uiView has handled touch

What I want to do is put a UIView on top of another UIView, and both of them are screen size. The top UIView includes lots cocos nodes and will respond when I touch them. But when I touch a place that has no cocos node, the bottom UIView should respond.
I don't know how to do this. My imagine is check if top uiView is handled touch, do nothing. other wise let the bottom UIView start respond. But I don't know how to check that. I only know how to check touch but it seems the UIView will also be touched when I touch some place it can't handle.
I think you not requires 2 views.
Just take one view only and add all your node to that view.
in .h
IBOutlet UIView *bgView;//your view
UITapGestureRecognizer *viewTapRecognizer;// view tap recognizer
in .m
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
viewTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handelGesture:)];
[bgView addGestureRecognizer:viewTapRecognizer];
for (UIView *subView in [bgView subviews]) {
UITapGestureRecognizer *nodeTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handelGesture:)];
[subView addGestureRecognizer:nodeTapRecognizer];
}
}
- (void)handelGesture:(UITapGestureRecognizer*)sender {
if (sender == viewTapRecognizer) {
// your view is tapped
NSLog(#"........Tapped view..........");
}
else {
// your node is tapped
NSLog(#"........Tapped node..........");
}
}
Try this. it might work for you.
You can achieve it by 2 ways: using pointInside... method or hitTest... method.
If you will use pointInside... you can use only your views (the top view and the bottom view). You will subclass UIView with the top view and override pointInside... method. You will return YES if user's tapped a cocoa node else returns NO.
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
BOOL inside = [self didUserTapCocoaNode:point];
return inside;
}
If you want more complex logic, you should use third view, subclass of UIView (the container view), that will be contain the top view and the bottom view. Override method hitTest..., and return the bottom or the top view.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView* result = nil;
if ([self touchedCocoaNode:point])
{
result = self.topView;
}
else
{
result = self.bottomView;
}
return result;
}
UPD
Example implementation of didUserTapCocoaNode with pseudo code:
- (BOOL)didUserTapCocoaNode:(CGPoint)pointInSelf
{
__block BOOL tappedSomeNode = NO;
[self.nodes enumerateObjectsUsingBlock:^(NodeType* obj, NSInteger idx, BOOL* stop){
CGPoint pointInNode = [obj convertPoint:pointInSelf fromView:self];
tappedSomeNode = [obj pointInside:pointInNode withEvent:nil];
*stop = tappedSomeNode;
}]
return tappedSomeNode;
}

draggingEntered not firing

I have a subclass of NBox that I would like to drag around a canvas called dragBox. I don't understand why draggingEntered isn't being fired on the following code. I get a nice slideback image, but none of the destination delegates are getting fired. Why?
-(void) awakeFromNib
{
[[self superview] registerForDraggedTypes:[NSArray arrayWithObject:NSFilenamesPboardType]];
}
-(void) mouseDown:(NSEvent *)theEvent
{
[self dragImage:[[NSImage alloc] initWithContentsOfFile:#"/Users/bruce/Desktop/Untitled-1.png"] at:NSMakePoint(32, 32) offset:NSMakeSize(0,0) event:theEvent pasteboard:[NSPasteboard pasteboardWithName:NSDragPboard] source:self slideBack:YES];
}
-(NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender // validate
{
NSLog(#"Updated");
return [sender draggingSourceOperationMask];
}
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender {
NSLog(#"Drag Entered");
return [sender draggingSourceOperationMask];
}
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender {
NSLog(#"Move Box");
[self setFrameOrigin:[sender draggingLocation]];
return YES;
}
-(BOOL) prepareForDragOperation:(id<NSDraggingInfo>)sender
{NSLog(#"Prepared");
return YES;
}
In your mouseDown method you are not putting anything in the pasteboard before you initiate the drag operation. The documentation for NSView states you need to add your data on the pasteboard before sending it to that message.
Whatever destination views you have are, or should be, registering for a certain drag type. If your pasteboard does not have any matching data for that type, the destinations will not fire any of the NSDraggingProtocol messages.
Solved!
I was using the NSBox as both a destination and source. The events weren't being fired when this was the case. I moved registerDragTypes to the superview, the canvas, and implemented the draggingEntered and performDrag there. It works now...
Bruce

Reading touch events in a QLPreviewController

I've got a QuickLook view that I view some of my app's documents in. It works fine, but I'm having my share of trouble closing the view again. How do I create a touch event / gesture recognizer for which I can detect when the user wants to close the view?
I tried the following, but no events seem to trigger when I test it.
/------------------------ [ TouchPreviewController.h ]---------------------------
#import <Quicklook/Quicklook.h>
#interface TouchPreviewController : QLPreviewController
#end
//------------------------ [ TouchPreviewController.m ]---------------------------
#import "TouchPreviewController.h"
#implementation TouchPreviewController
- (id)init:(CGRect)aRect {
if (self = [super init]) {
// We set it here directly for convenience
// As by default for a UIImageView it is set to NO
UITapGestureRecognizer *singleFingerDTap = [[UITapGestureRecognizer alloc]
initWithTarget:self action:#selector(handleSingleDoubleTap:)];
singleFingerDTap.numberOfTapsRequired = 2;
[self.view addGestureRecognizer:singleFingerDTap];
[self.view setUserInteractionEnabled:YES];
[self.view setMultipleTouchEnabled:YES];
//[singleFingerDTap release];
}
return self;
}
- (IBAction)handleSingleDoubleTap:(UIGestureRecognizer *) sender {
CGPoint tapPoint = [sender locationInView:sender.view.superview];
[UIView beginAnimations:nil context:NULL];
sender.view.center = tapPoint;
[UIView commitAnimations];
NSLog(#"TouchPreviewController tap!" ) ;
}
// I also tried adding this
- (BOOL)gestureRecognizer:(UIGestureRecognizer *) gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer*) otherGestureRecognizer {
return YES;
}
#end
Edit: For clarification, this is how I instantiate the controller:
documents = [[NSArray alloc] initWithObjects: filename , nil ] ;
preview = [[TouchPreviewController alloc] init];
preview.dataSource = self;
preview.delegate = self;
//set the frame from the parent view
CGFloat w= backgroundViewHolder.frame.size.width;
CGFloat h= backgroundViewHolder.frame.size.height;
preview.view.frame = CGRectMake(0, 0,w, h);
//refresh the preview controller
[preview reloadData];
[[preview view] setNeedsLayout];
[[preview view] setNeedsDisplay];
[preview refreshCurrentPreviewItem];
//add it
[quickLookView addSubview:preview.view];
Also, I've defined the callback methods as this:
- (NSInteger) numberOfPreviewItemsInPreviewController: (QLPreviewController *) controller
{
return [documents count];
}
- (id <QLPreviewItem>) previewController: (QLPreviewController *) controller previewItemAtIndex: (NSInteger) index
{
return [NSURL fileURLWithPath:[documents objectAtIndex:index]];
}
Edit2: One thing i noticed. If I try making swiping gestures, I get the following message. This could shed some light on what is wrong/missing?
Ignoring call to [UIPanGestureRecognizer setTranslation:inView:] since
gesture recognizer is not active.
I think your example code is incomplete. It isn't clear how you are instantiating the TouchPreviewController (storyboard, nib file or loadView.)
I have never used the class so I could be way out in left field.
If you've already instantiated a UITapGestureRecognizer in the parent viewController, it is absorbing the tap events and they aren't passed on to your TouchPreviewController.
I would implement the view hierarchy differently by attaching the UITapGestureRecognizer to the parent viewController and handle presentation and unloading of the QLPreviewController there.
I think you might not have to subclass QLPreviewController by instantiating the viewController from a nib file.
When your parent viewController's UITapGestureRecognizer got an event you would either push the QLPreviewController on the navigation stack or pop it off the navigation stack when done.
Hope this is of some help.

UIControl Subclass - Events called twice

I'm currently working on a custom UIControl Subclass. To track the touches I use the following Method:
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
NSLog(#"Start");
CGPoint location = [touch locationInView:self];
if ([self touchIsInside:location] == YES) {
//Touch Down
[self sendActionsForControlEvents:UIControlEventTouchDown];
return YES;
}
else {
return NO;
}
}
This works as expected and #"Start" is loged exactely once. The next step is that I add a Target and a Selector with UIControlEventTouchDown.
[markItem addTarget:self action:#selector(action:) forControlEvents:UIControlEventTouchUpInside];
This works also and the action: method is called. But that's my problem. The action is called twice. What am I doing wrong? I just use [self sendActionsForControlEvents:UIControlEventTouchDown]; once and the target action is called twice. What's wrong with my code?
Sandro Meier
The first call to the action method happens automatically by the event dispatcher once you've called:
[markItem addTarget:self action:#selector(action:) forControlEvents:UIControlEventTouchUpInside];
to register the handler.
So when you then call:
//Touch Down
[self sendActionsForControlEvents:UIControlEventTouchDown];
you are generating the second call to your handler (and any others that are hooked up).
So it seems like you don't need both the action handler and the beginTracking - use one or the other.
Update:
Given your comment and further thought: since you are a subclass of UIControl, I think you probably don't want to be registering for event handlers for yourself.
Instead you should exclusively use:
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event;
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event;
- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event;
- (void)cancelTrackingWithEvent:(UIEvent *)event; // event may be nil if cancelled for non-event reasons, e.g. removed from window
Also the tracking instance variable.
So I think you should not be posting events or listening to events. Further, is it actually possible to get a beginTrackingWithTouch event if it's not in your view? Doesn't seem like it would be. So I don't think you need the testing to see if it's in your view.
So I think it might be worth stepping back and thinking about what you are trying to do and re-reading UIControl documentation. Specifically:
Subclassing Notes
You may want to extend a UIControl subclass for
either of two reasons:
To observe or modify the dispatch of action messages to targets for
particular events To do this, override sendAction:to:forEvent:,
evaluate the passed-in selector, target object, or UIControlEvents bit
mask, and proceed as required.
To provide custom tracking behavior (for example, to change the
highlight appearance) To do this, override one or all of the following
methods: beginTrackingWithTouch:withEvent:,
continueTrackingWithTouch:withEvent:, endTrackingWithTouch:withEvent:.
The first part is for having your UIControl subclass do non-standard handling of target action processing for clients or users of your control (that doesn't sound like what you are trying to do, though you didn't really give a high-level description).
The second part sounds more like what you are wanting to do - custom tracking within your UIControl subclass.
I had this same problem as well. When I would register an action with
[button addTarget:self action:#selector(action:) forControlEvents:UIControlEventTouchDown];
the action method would be fired once, but when I registered it with
[button addTarget:self action:#selector(action:) forControlEvents:UIControlEventTouchUpInside];
the action method would be fired twice.
The thing that fixed it for me was to make sure that my - (void) action: method return IBAction instead of void. I am not sure why this seemed to fix it though.
Hm.. Check my code for your aims:
UIContr.h
#interface UIContr : UIControl {
}
#end
UIContr.m
#import "UIContr.h"
#implementation UIContr
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Initialization code.
}
return self;
}
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
NSLog(#"Start");
CGPoint location = [touch locationInView:self];
if (CGRectContainsPoint(self.frame, location)) {
//Touch Down
[self sendActionsForControlEvents:UIControlEventTouchDown];
return YES;
}
else {
return NO;
}
}
- (void)dealloc {
[super dealloc];
}
#end
How to use in UIViewController:
- (void)viewDidLoad {
UIContr *c = [[UIContr alloc] initWithFrame:CGRectMake(20, 20, 100, 100)];
[c addTarget:self action:#selector(action:) forControlEvents:UIControlEventTouchUpInside];
c.backgroundColor = [UIColor redColor];
[self.view addSubview:c];
}
-(void)action:(id)sender{
NSLog(#"123");
}

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.