Goal:
I want to use the Observer Pattern so that when one uiimageview receives a different background image, then 2 other uiimageviews will listen for that change, and then change themselves.
Strategy:
Based on what I read about observer pattern in objective-c, I decided to implement the nsnotificationcenter.
Code:
self refers to the RemoteViewManagerController, updateButtons is the method that will be called when the ImageSwap event is fired, and object refers to the "main" uiimageview, that is, the uiimageview that when changed will cause changes in other uiimageviews.
- (void)registerButtonObserver:(UIView *)currentView
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(updateButtons:) name:#"ImageSwap" object:[self.view viewWithTag:1]];
}
setDefaultButtons is invoked, and we iterate through the buttons and target buttons based on tag. The "main" uiviewimage has a tag of 1. So we call setImageChange to change the background image of that button, and as a result, I want to fire the ImageSwap event, to change the other two uiimageview buttons, and I pass in those buttons part of the userinfo dictionary. The idea is when updateButtons is invoked, I can reference those buttons in the userinfo dictionary.
- (void)setDefaultButtons:(UIView *)currentView
{
for( UIView *view in currentView.subviews ) {
if( [view isKindOfClass:[UIButton class]] ) {
if( view.tag == 1 ){
[self setImageChange:#"fence" forButton:view];
NSArray *keys = [NSArray arrayWithObjects:#"subview1", #"subview2", nil];
NSArray *objects = [NSArray arrayWithObjects:[self.view viewWithTag:4], [self.view viewWithTag:5], nil];
NSDictionary *items = [NSDictionary dictionaryWithObjects:objects
forKeys:keys];
NSLog(#"But we sure to get here right");
[[NSNotificationCenter defaultCenter]postNotificationName:#"ImageSwap" object:view userInfo:items];
}
else if(view.tag == 2){
[self setImageChange:#"siren" forButton:view];
}
else if(view.tag == 3){
[self setImageChange:#"auxiliary" forButton:view];
}
}
}
}
Note that I know that we get to the postNotificationName line, because this line does fire: NSLog(#"But we sure to get here right");
I don't get any errors. But this line in RemoteViewManagerController.m:
- (void)updateButtons:(NSNotification*)notification
{
NSLog(#"Do we get here?");
}
is never called.
I believe that when two subviews have the same tag, -viewWithTag: just returns the first one that it finds. So if there happen to be two views with tag=1, it's quite possible that you're observing the wrong one. Try changing the object parameter in you -addObserver... call to nil, which will indicate that you want to observe that notification for all objects.
Related
I've done the following thing:
buttonPlaylistView = [[UIButton alloc] initWithFrame:CGRectMake(self.view.frame.size.width *(urlList.count+1), 0, self.view.frame.size.width, self.view.frame.size.height)];
buttonPlaylistView.tag = 0;
UITapGestureRecognizer *doubleTap3 = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleDoubleTap:)];
[doubleTap3 setNumberOfTapsRequired:2];
[buttonPlaylistView addGestureRecognizer:doubleTap3];
[doubleTap3 release];
-(void) handleDoubleTap:(UITapGestureRecognizer *)sender{
if(sender.state == UIGestureRecognizerStateEnded)
int x = [sender tag];
return;
}
But I get SIGAGRT at this line: int x = [sender tag]; saying:
[UITapGestureRecognizer tag]: unrecognized selector sent to instance 0x61280b0
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[UITapGestureRecognizer tag]: unrecognized selector sent to instance 0x61280b0'
NOW:What's the problem and what's the solution for this?Thanks
-(void) handleDoubleTap:(UITapGestureRecognizer *)sender
{
if(sender.state == UIGestureRecognizerStateEnded)
{
int x = [sender.view tag];
}
}
Will fix the issue.
An UITapGestureRecognizer does not have a property named tag - as you see, the sender you get is NOT the button. You have to access the buttonPlayListView directly, like
int x = [buttonPlayListView tag];
or otherwise remember which button you want to access.
Even though I'm quite sure that you're going about this the wrong way, adding a double tap gesture recognizer to a UIButton, there is a way you can still perform the task you require that shouldn't be too much work for you.
You've made the comment
and how could I remember if I create let say 100 buttons
to one of the answers, the one which highlights what the issue is that's causing your SIGBART. UIGestureRecognizer does not have a tag property.
Here's what you could do, is to iterate through all the subviews of your [self view] and find the one that has the same UIGestureRecognizer, it's not the prettiest solution, and the more subview's you have the longer the loop will take. But it'll do what you seem to be looking for, so if you're adding .
In your handleDoubleTap function you could do the following
-(void) handleDoubleTap:(UITapGestureRecognizer *)sender
{
if(sender.state == UIGestureRecognizerStateEnded)
{
int iButtonTag = -1 //This is important later to escape the below for loop as we don't need to needlessly go around in circles
for(UIView* psubView in [[self view] subviews])
{
if( [psubView isKindOfClass:[UIButton class]] )
{
UIButton* pButton = (UIButton*)psubView;
for(UIGestureRecognizer* pGesture in [pButton gestureRecognizers] )
{
if( pGesture == sender )//this is the button we're after
{
iButtonTag = [pButton tag];
break;
}
}
if( iButton != -1 )//found what we came for
{
break;
}
}
}
//do what ever it was you needed to do now that you have the views tag, or you could have kept a reference to the button etc.
}
}
That should solve your problem. Alternatively if you're going to be adding buttons to subviews of subviews it would be better to keep track of your UIButtons in an NSMutableArray , you would do this by creating a class property (or member variable) and adding the buttons to this using the 'addObject:' function of NSMutableArray. Then instead of the line
for(UIView* psubView in [[self view] subviews])
above you could exchange that for
for( UIButton* pButton in m_pMutableButtonArray )
where "m_pMutableButtonArray" is the variable name you gave to your NSMutableArray you were storing the UIButtons in. This also means you would do away with the following if isKindOfClass test on the following line.
That should fix your problem.
Why are you putting a UITapGestureRecognizer in a button? The button already handles that for you and will send you a callback, you can add a target to a button using this UIControl method
- (void)addTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents
Reposting with more concise and focused question after original question went unanswered. Also adding more insight into the problem after another day of research:
In my app delegate (didFinishLaunching), I set up a callEventHandler on CTCallCenter.
The idea is that when a callState changes, I post a notification with a userInfo dict
containing the call.callState. In my view, I observe this notification, and when the
userInfo dict contains a value of CTCallDisconnected, I want to unhide a view.
The problem I'm having is that the unhiding aspect is taking, almost consistenly, ~ 7 seconds.
Everything else is working fine, and I know this because I NSLog before and after the unhiding,
and those logs appear immediately, but the darned view still lags for 7 seconds.
Here's my code:
appDidFinishLaunching:
self.callCenter = [[CTCallCenter alloc] init];
self.callCenter.callEventHandler = ^(CTCall* call) {
// anounce that we've had a state change in our call center
NSDictionary *dict = [NSDictionary dictionaryWithObject:call.callState forKey:#"callState"];
[[NSNotificationCenter defaultCenter] postNotificationName:#"CTCallStateDidChange" object:self userInfo:dict];
};
I then listen for this notification when a user taps a button that dials a phone number:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(ctCallStateDidChange:) name:#"CTCallStateDidChange" object:nil];
Then, in ctCallStateDidChange:
- (void)ctCallStateDidChange:(NSNotification *)notification
{
NSLog(#"121");
NSString *callInfo = [[notification userInfo] objectForKey:#"callState"];
if ([callInfo isEqualToString:CTCallStateDisconnected]) {
NSLog(#"before show");
[self.view viewWithTag:kNONEMERGENCYCALLSAVEDTOLOG_TAG].hidden = NO;
NSLog(#"after show");
}
}
I've tracked the problem down to the if condition in the above code sample:
if ([[userInfo valueForKey:#"userInfo"] valueForKey:#"callState"] == CTCallStateDisconnected) {
If I simply replace that with:
if (1 == 1) {
Then the view appears immediately!
The thing is, those NSLog statements are logging immediately, but the view is
lagging in it's unhiding. How could that condition cause only part of it's block
to execute immediately, and the rest to wait ~ 7 seconds?
Thanks!
Try changing your code to this:
- (void)ctCallStateDidChange:(NSNotification *)notification
{
NSLog(#"121");
NSString *callInfo = [[notification userInfo] objectForKey:#"callState"];
if ([callInfo isEqualToString:CTCallStateDisconnected]) {
NSLog(#"before show");
[self.view viewWithTag:kNONEMERGENCYCALLSAVEDTOLOG_TAG].hidden = NO;
NSLog(#"after show");
}
}
Note:
The parameter is an NSNotification, not an NSDictionary
I would not compare strings with ==
No need to cast the view to change the hidden property
Use NO instead of false
Update: Got an idea: Could you try the following, please, in between the NSLogs?
dispatch_async(dispatch_get_main_queue(), ^{
[self.view viewWithTag:kNONEMERGENCYCALLSAVEDTOLOG_TAG].hidden = NO;
});
Reading the CTCallCenter doc, it seems the callEventHandler is dispatched on "the default priority global dispatch queue", which is not the main queue where all the UI stuff happens.
Looks like there is no problem with your hidden code. If I were you, I would comment out all the code after the call ends, and uncomment them one by one to see what is the problem.
Hm... try to call [yourViewController.view setNeedsDisplay] after you change hidden property. Or avoid hidden, use alpha or addSubview: and removeFromSuperview methods instead.
djibouti33,
Where you put this sentence to listen when a user taps a button that dials a phone number?on WillResignActive function?
this sentence --> [[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(ctCallStateDidChange:) name:#"CTCallStateDidChange" object:nil];
Thanks for your time,
Willy.
Reposting with more concise and focused question after original question went unanswered. Also adding more insight into the problem after another day of research:
In my app delegate (didFinishLaunching), I set up a callEventHandler on CTCallCenter.
The idea is that when a callState changes, I post a notification with a userInfo dict
containing the call.callState. In my view, I observe this notification, and when the
userInfo dict contains a value of CTCallDisconnected, I want to unhide a view.
The problem I'm having is that the unhiding aspect is taking, almost consistenly, ~ 7 seconds.
Everything else is working fine, and I know this because I NSLog before and after the unhiding,
and those logs appear immediately, but the darned view still lags for 7 seconds.
Here's my code:
appDidFinishLaunching:
self.callCenter = [[CTCallCenter alloc] init];
self.callCenter.callEventHandler = ^(CTCall* call) {
// anounce that we've had a state change in our call center
NSDictionary *dict = [NSDictionary dictionaryWithObject:call.callState forKey:#"callState"];
[[NSNotificationCenter defaultCenter] postNotificationName:#"CTCallStateDidChange" object:self userInfo:dict];
};
I then listen for this notification when a user taps a button that dials a phone number:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(ctCallStateDidChange:) name:#"CTCallStateDidChange" object:nil];
Then, in ctCallStateDidChange:
- (void)ctCallStateDidChange:(NSNotification *)notification
{
NSLog(#"121");
NSString *callInfo = [[notification userInfo] objectForKey:#"callState"];
if ([callInfo isEqualToString:CTCallStateDisconnected]) {
NSLog(#"before show");
[self.view viewWithTag:kNONEMERGENCYCALLSAVEDTOLOG_TAG].hidden = NO;
NSLog(#"after show");
}
}
I've tracked the problem down to the if condition in the above code sample:
if ([[userInfo valueForKey:#"userInfo"] valueForKey:#"callState"] == CTCallStateDisconnected) {
If I simply replace that with:
if (1 == 1) {
Then the view appears immediately!
The thing is, those NSLog statements are logging immediately, but the view is
lagging in it's unhiding. How could that condition cause only part of it's block
to execute immediately, and the rest to wait ~ 7 seconds?
Thanks!
Try changing your code to this:
- (void)ctCallStateDidChange:(NSNotification *)notification
{
NSLog(#"121");
NSString *callInfo = [[notification userInfo] objectForKey:#"callState"];
if ([callInfo isEqualToString:CTCallStateDisconnected]) {
NSLog(#"before show");
[self.view viewWithTag:kNONEMERGENCYCALLSAVEDTOLOG_TAG].hidden = NO;
NSLog(#"after show");
}
}
Note:
The parameter is an NSNotification, not an NSDictionary
I would not compare strings with ==
No need to cast the view to change the hidden property
Use NO instead of false
Update: Got an idea: Could you try the following, please, in between the NSLogs?
dispatch_async(dispatch_get_main_queue(), ^{
[self.view viewWithTag:kNONEMERGENCYCALLSAVEDTOLOG_TAG].hidden = NO;
});
Reading the CTCallCenter doc, it seems the callEventHandler is dispatched on "the default priority global dispatch queue", which is not the main queue where all the UI stuff happens.
Looks like there is no problem with your hidden code. If I were you, I would comment out all the code after the call ends, and uncomment them one by one to see what is the problem.
Hm... try to call [yourViewController.view setNeedsDisplay] after you change hidden property. Or avoid hidden, use alpha or addSubview: and removeFromSuperview methods instead.
djibouti33,
Where you put this sentence to listen when a user taps a button that dials a phone number?on WillResignActive function?
this sentence --> [[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(ctCallStateDidChange:) name:#"CTCallStateDidChange" object:nil];
Thanks for your time,
Willy.
I have a problem with removing a subview, or more precisely with checking if it is still there after having deleted it.
My app first adds a subview to self.view.
[self.view addSubview:tabsClippedView];
Then it adds another subview to this subview (to which it adds several buttons as subviews, but I guess this is unimportant in this context):
[tabsClippedView addSubview:tabsView];
Finally, I have a method which allows the tabsView to be deleted and then created again. I need to do this so as to update the number of buttons in that tabsView (as the user can delete buttons). The method looks basically like this:
[self.tabsView removeFromSuperview];
After that I call a method called showTabs (which I already called in the very beginning of the app in order to add the subViews). This is where it all becomes problematic and where my app crashes (I get no error in the debug console, so I don't really know what the issue is...):
if ([tabsClippedView isDescendantOfView:self.view]) {
NSLog(#"There is already a tabsClippedView.");
} else {
NSLog(#"There is no tabsClippedView. I'll add one...");
[self initTabsClippedView];
}
This is where the app crashes: when trying to assess if tabsView isDescendantOfView (I don't get any of the following logs):
if ([tabsView isDescendantOfView:tabsClippedView]) {
NSLog(#"There is already a tabsView");
} else {
NSLog(#"There is no tabsView for the buttons. I'll add one including buttons.");
[self initTabs];
}
I'd be grateful for any suggestions where the problem could be.
EDIT:
These are the methods to set up my views:
-(void) initTabsClippedView { // sets up tabsClippedView
NSLog(#"initTabsClippedView method started...");
CGRect tabsClippedFrame = CGRectMake(258,30,70,81*6);
tabsClippedView = [[[UIView alloc] initWithFrame:tabsClippedFrame] autorelease];
tabsClippedView.clipsToBounds = true;
[self.view addSubview:tabsClippedView];
NSLog(#"initTabsClippedView method ended.");
}
-(void) initTabs {
NSLog(#"initTabs started. Adding buttons to tabsClippedView...");
CGRect tabsFrame = CGRectMake(-30,0,50,480);
tabsView = [[[UIView alloc] initWithFrame:tabsFrame] autorelease];
[tabsClippedView addSubview:tabsView];
// sets up buttons in tabsClippedView
And this is where I delete the tabsClippedView (triggered by a button found in tabsClippedView):
-(void)tabDelete:(id)sender
{
UIButton *button = (UIButton *)sender;
[UIView animateWithDuration:0.75
delay:0
options:UIViewAnimationOptionCurveEaseIn
animations:^{
button.transform = CGAffineTransformMakeTranslation(-30, 0);
}
completion:^(BOOL finished){
[UIView animateWithDuration:0
delay:0
options:UIViewAnimationOptionCurveEaseIn
animations:^{
[self.tabsView removeFromSuperview];
//...
}
completion:^(BOOL finished){
NSLog(#"tabsView removed from Superview. Objects Deleted.");
[self showTabs];
NSLog(#"TabDelete finished. Button removed and tabsView updated accordingly.");
}
];
}];
And this is the showTabs method which was already called when I started the app:
-(void)showTabs {
NSLog(#"showTabs started...");
currentView = #"Tabs";
if ([tabsClippedView isDescendantOfView:self.view]) {
NSLog(#"There is already a tabsClippedView.");
} else {
NSLog(#"There is no tabsClippedView. I'll add one...");
[self initTabsClippedView];
}
if ([tabsView isDescendantOfView:tabsClippedView]) {
NSLog(#"There is already a tabsView");
} else {
NSLog(#"There is no tabsView for the buttons. I'll add one including buttons.");
[self initTabs];
}
Is it possible that you are getting EXC_BAD_ACCESS? Is it possible that the app is crashing because tabsView is deallocated when you send isDescendantOfView: to it. If you run with breakpoints enabled it should tell you the reason for the crash. If it is an EXC_BAD_ACCESS problem you should try NSZombie.
To activate NSZombie do the following:
Get info of the executable.
Go to the arguments tab.
In the "Variables to be set in the environment:" section add:
Name: NSZombieEnabled
Value: YES
Then run your app as usual and when it crashes it should tell you which deallocated object received what message.
EDIT: Just saw your edit. I think I nailed it. You're autoreleasing the views when you create them, so when they are removed from their superviews they are no longer retained and thus deallocated. You're app crashes because you're trying to run methods on deallocated views.
EDIT 2: Thought I should tell you that there is a better solution than the one posted by Praveen S.
Change your code as follows:
[tabsClippedView release];
tabsClippedView = [[UIView alloc] initWithFrame:tabsClippedFrame];
and
[tabsView release];
tabsView = [[UIView alloc] initWithFrame:tabsFrame];
The above code does the same thing as the code posted by Praveen S, but without the autorelease. An autorelease is more expensive than a regular release and should only be used when needed and in this case it isn't.
Rather than releasing before you allocate a new view you probably want to release the view when you're done with it:
[tabsView removeFromSuperview];
[tabsView release];
tabsView = nil;
or simply
[tabsView removeFromSuperview];
self.tabsView = nil;
and then instead of:
if ([tabsView isDescendantOfView:tabsClippedView]) ...
you can use:
if (tabsView) ...
As you might have noticed, there really is no need for you to retain the view. You could just as well do the following:
tabsView = [[UIView alloc] initWithFrame:tabsFrame];
[tabsClippedView addSubview:tabsView]; // This retains tabsView
[tabsView release];
and then to remove the view you would use:
[tabsView removeFromSuperview]; // This will release the tabsView
tabsView = nil;
Also remember to set the views to nil in viewDidUnload.
EDIT 3: Why self made such a difference:
What you need to understand is how properties and reference counting works. There are books and such you could read about it. I'm sure Google can provide you with some good references as well.
The difference between
self.tabsView = [[UIView alloc] initWithFrame:frame];
and
tabsView = [[UIView alloc] initWithFrame:frame];
is that self.tabsView is accessing the properties setter, while tabsView is accessing the instance variable directly.
A nonatomic, retain property's implementation looks something like the following:
- (void)setTabsView:(UIView *)view
{
if (view != tabsView) {
[tabsView release];
tabsView = [view retain];
}
}
So the property is taking care of the memory management for you. In my solution I take care of the memory management myself and thus I don't need the property to do it for me, so I don't use it.
I hope this explains why self made such a difference.
Change your code as follows:
self.tabsClippedView = [[[UIView alloc] initWithFrame:tabsClippedFrame] autorelease];
and
self.tabsView = [[[UIView alloc] initWithFrame:tabsFrame] autorelease];
I have implemented an app that shows a map with a lot of pins on it.
If you push one pin you get on a second view that shows the data behind the pin.
A button takes you back to the map.
My problem is that by the third touch on a pin the program crashes with a EXC_BAD_ACCESS in this method:
- (void) switchViews {
if(self.details == nil){
Kundendetails *detailAnsicht = [[Kundendetails alloc] initWithNibName:#"ViewList" bundle:nil];
detailAnsicht.rootViewController = self;
self.details = detailAnsicht;
detailAnsicht.map = self.map;
}
if(self.details.view.superview == nil) {
[map.view removeFromSuperview];
[self.view addSubview:details.view];
[details viewDidLoad];
} else {
[details.view removeFromSuperview];
[details release];
[self.view addSubview:map.view];
}
}
How do I isolate which line of code causes the crash? Why would it always crash only on the third touch?
I hope you could help me.
Put NSLog statements in each branch of the ifs. You will almost assuredly see that this statement causes the problem:
[details viewDidLoad];
This is because at some point you execute this:
[details release];
effectively making details inaccessible. By the way you should also almost NEVER call viwewDidLoad directly.