I am writing a tone generator UI and I need to change UIImageView image every time generator starts generating and finishes generating.
To do so I have
#property (weak, nonatomic) IBOutlet UIImageView *headphonesImage;
in my UIViewController. And I add default image to it like so
- (void) viewDidLoad
{
...
headphonesImagesArray = [NSArray arrayWithObjects:
[UIImage imageNamed: #"Both Headphones"],
[UIImage imageNamed: #"Both Headphones Playing"],
nil];
[self.headphonesImage setAlpha: 0.5];
[self.headphonesImage setImage: headphonesImagesArray[0]];
...
}
My tone generator sends messages into this method
- (void) toneGeneratorControllerStateHasChangedWithNotification: (NSNotification *) notification
{
if([notification.name isEqualToString: ToneGenerationHasFinished])
[self.headphonesImage setImage: headphonesImagesArray[0]];
else
[self.headphonesImage setImage: headphonesImagesArray[1]];
}
The problem is that though headphonesImage image changes to [1] and then nothing happens. I can see in variables inspector that headphonesImage image is changed every time the method is invoked, but these changes are not present on screen of neither simulator nor iPhone. I can't even hide this damn UIImageView. setHidden: true does nothing.
Please help!
"My tone generator sends messages into this method"
If you are writing the tone generator, it sounds like it is running on a background thread. You should send notifications on the main thread, especially if they trigger UI changes (such as setting an image), e.g.:
dispatch_async(dispatch_get_main_queue(),^{
[[NSNotificationCenter defaultCenter] postNotificationName:#"toneGeneratorControllerStateHasChanged"
object:nil
userInfo:imageDict];
});
Alternatively, you can send the image change to the main thread as Igor suggests. The disadvantage there is having to do any time you use such notifications, as opposed to doing it in one place.
Do this:-
- (void) toneGeneratorControllerStateHasChangedWithNotification: (NSNotification *) notification
{
if([notification.name isEqualToString: ToneGenerationHasFinished]) {
self.headphonesImage.image = [UIImage imageNamed:#"Both Headphones"];
}
else {
self.headphonesImage.image = [UIImage imageNamed:#"Both Headphones Playing"];
}
}
instead of setImage.
in VDL
headphonesImagesArray = [NSArray arrayWithObjects:
#"Both Headphones",
#"Both Headphones Playing",
nil];
And do this :
- (void) toneGeneratorControllerStateHasChangedWithNotification: (NSNotification *) notification
{
if([notification.name isEqualToString: ToneGenerationHasFinished]) {
self.headphonesImage.image = [UIImage imageNamed: headphonesImagesArray[0]];
}
else {
self.headphonesImage.image = [UIImage imageNamed:headphonesImagesArray[1]];
}
}
It looks very shady, but it seems that I've found the solution:
- (void) toneGeneratorControllerStateHasChangedWithNotification: (NSNotification *) notification
{
BOOL finished = FALSE;
if([notification.name isEqualToString: ToneGenerationHasFinished])
finished = TRUE;
dispatch_async(dispatch_get_main_queue(),
^{
if(finished)
[self.headphonesImage setImage: headphonesImagesArray[0]];
else
[self.headphonesImage setImage: headphonesImagesArray[1]];
});
}
As far as I know all the UI drawing happens in the main thread. Looks like notification method does not work in the main thread unless you explicitely tell it to do so...
Related
In my program I used a void method and I want to stop this when the button action is done.
in viewDidLoad
[self runningaction];
and out side of the viewDidLoad
-(void)runningaction
{
some code
}
another method for UIButton action
-(void)clicktosee
{
//here i need to write code for stop the -(void)runningaction
}
Thanks in advance.
Put check that method is implemented then just return that's what i tried
-(void)clicktosee
{
if( [self respondsToSelector:#selector(runningaction)] ) {
return;
}
}
Ok, if I understood your question (and comment) correctly, you want to start some animations on button images, and then stop them on user action.
This can be achieved like this :
-(void)runningaction {
myButton.imageView.animationImages =
[NSArray arrayWithObjects:[UIImage imageNamed:#"image1.png"],
[UIImage imageNamed:#"image2.png"],
nil];
myButton.imageView.animationDuration = 0.5; //whatever you want (in seconds)
myButton.imageView.animationRepeatCount = 0; //0 means repeat forever here
[myButton.imageView startAnimating];
}
Then in -(void)clicktosee you simple write this line : [myButton.imageView stopAnimating].
Hope I understood you correctly.
NOTE : The animation code is taken from this answer by #ender.
I am using setNeedsDisplay on my GUI, but there update is sometimes not done. I am using UIPageControllView, each page has UIScrollView with UIView inside.
I have the following pipeline:
1) application comes from background - called applicationWillEnterForeground
2) start data download from server
2.1) after data download is finished, trigger selector
3) use dispatch_async with dispatch_get_main_queue() to fill labels, images etc. with new data
3.1) call setNeedsDisplay on view (also tried on scroll view and page controller)
Problem is, that step 3.1 is called, but changes apper only from time to time. If I swap pages, the refresh is done and I can see new data (so download works correctly). But without manual page turn, there is no update.
Any help ?
Edit: code from step 3 and 3.1 (removed _needRefresh variables pointed in comments)
-(void)FillData {
dispatch_async(dispatch_get_main_queue(), ^{
NSString *stateID = [DataManager ConvertStateToStringFromID:_activeCity.actual_weather.state];
if ([_activeCity.actual_weather.is_night boolValue] == YES)
{
self.contentBgImage.image = [UIImage imageNamed:[NSString stringWithFormat:#"bg_%#_noc", [_bgs objectForKey:stateID]]];
if (_isNight == NO)
{
_bgTransparencyInited = NO;
}
_isNight = YES;
}
else
{
self.contentBgImage.image = [UIImage imageNamed:[NSString stringWithFormat:#"bg_%#", [_bgs objectForKey:stateID]]];
if (_isNight == YES)
{
_bgTransparencyInited = NO;
}
_isNight = NO;
}
[self.contentBgImage setNeedsDisplay]; //refresh background image
[self CreateBackgroundTransparency]; //create transparent background if colors changed - only from time to time
self.contentView.parentController = self;
[self.contentView FillData]; //Fill UIView with data - set labels texts to new ones
//_needRefresh is set to YES after application comes from background
[self.contentView setNeedsDisplay]; //This do nothing ?
[_grad display]; //refresh gradient
});
}
And here is selector called after data download (in MainViewController)
-(void)FinishDownload:(NSNotification *)notification
{
dispatch_async(dispatch_get_main_queue(), ^{
[_activeViewController FillData]; //call method shown before
//try call some more refresh - also useless
[self.pageControl setNeedsDisplay];
//[self reloadInputViews];
[self.view setNeedsDisplay];
});
}
In AppDelegate I have this for application comes from background:
-(void)applicationWillEnterForeground:(UIApplication *)application
{
MainViewController *main = (MainViewController *)[(SWRevealViewController *)self.window.rootViewController frontViewController];
[main UpdateData];
}
In MainViewController
-(void)UpdateData
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(FinishForecastDownload:) name:#"FinishDownload" object:nil]; //create selector
[[DataManager SharedManager] DownloadForecastDataWithAfterSelector:#"FinishDownload"]; //trigger download
}
try this:
[self.view performSelectorOnMainThread:#selector(setNeedsLayout) withObject:nil waitUntilDone:NO];
or check this link:
http://blackpixel.com/blog/2013/11/performselectoronmainthread-vs-dispatch-async.html
setNeedsDisplay triggers drawRect: and is used to "redraw the pixels" of the view , not to configure the view or its subviews.
You could override drawRect: and modify your labels, etc. there but that's not what it is made for and neither setNeedsLayout/layoutSubviews is.
You should create your own updateUI method where you use your fresh data to update the UI and not rely on specialized system calls meant for redrawing pixels (setNeedsDisplay) or adjusting subviews' frames (drawRect:).
You should set all your label.text's, imageView.image's, etc in the updateUI method. Also it is a good idea to try to only set those values through this method and not directly from any method.
None of proposed solutions worked. So at the end, I have simply remove currently showed screen from UIPageControllView and add this screen again. Something like changing the page there and back again programatically.
Its a bit slower, but works fine.
I am working on a memory based matching puzzle game. For that I need to glow buttons randomly from sequence. I have been googling sample puzzles,games & code snippets since a week or so, yet I was unable to trace out an appropriate sample project or some source to get started.
I am glowing the lights(buttons) by changing its background images(imitation), I have already set the default images for buttons(coloured bulb when in stable state) in story board. I have used NSMutableArray of IBOutletCollection i.e. tapLightButtons. Here is what I tried to get the sequence of lights glowing in a random mode to get the game experience.
-(void)startGlowingLights
{
[self shuffleValuesInArray:_tapLightButtons];
[self shuffleValuesInArray:lightArray];
[self shuffleValuesInArray:_initialButtonImages];
[self performSelector:#selector(animateButtonSequence) withObject:self afterDelay:0.2];
}
- (void) animateButtonSequence
{
__block UIButton *tapLight;
[UIView animateWithDuration:0.15
delay:0.0
options:UIViewAnimationOptionCurveLinear
animations:^{
// button flash animation
} completion:^(BOOL finished) {
if (finished) {
if (i < _tapLightButtons.count) {
i++;
[self performSelector:#selector(animateButtonSequence) withObject:self afterDelay:1.0];
tapLight = [self.tapLightButtons objectAtIndex:i-1];
UIImage *glownLight = [UIImage imageNamed:[lightArray objectAtIndex:i-1]];
[tapLight setBackgroundImage:glownLight forState:UIControlStateNormal];
[[TTLMusicPlayer sharedInstance] playGlowLightSound:[[NSBundle mainBundle] pathForResource:#"beep" ofType: #"mp3"]];
//Reset the completed glowed button to previous state(off mode)
double delayInSeconds = 1.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
if (i != 0 && i < _initialButtonImages.count) {
[tapLight setBackgroundImage:[self.initialButtonImages objectAtIndex:i-1] forState:UIControlStateNormal];
}
});
}
else {
i = 0;
}
NSLog(#"i value is %d",i);
}
}];
}
-(NSMutableArray *)shuffleValuesInArray:(NSMutableArray *)shuffledArray
{
for (NSInteger x=0;x<[shuffledArray count];x++)
{
NSInteger randomNumber = (arc4random() % ([shuffledArray count] - x)) + x;
[shuffledArray exchangeObjectAtIndex:x withObjectAtIndex:randomNumber];
}
return shuffledArray;
}
tapLightButtons is the array which holds puzzled buttons(lights) which glows automatically one after another in random fashion.
lightArray is an array holding all the appropriate colour images(flash to produce glowed affect)
initialButtonImages contains array of all default(initial) images of buttons(off mode lights)
Some how I could partially achieve what I wanted to, surprisingly even after shuffling all arrays, still I see variations in the order because the glowed image should be the corresponding of stable(off mode coloured light) and then after resetting, the stable image should match the one before the light was actually glowed.
Posted the question on game dev site from Stack Exchange as well!!
Game Screen for better understanding
After flashing of coloured light
How to properly glow the buttons randomly from sequence?
I would do this a different way that leads to simpler code. I created an array of 9 custom buttons in IB and added them to an outlet collection. I subclassed the buttons, and added a flashWithOnTime: method, so the button itself would take care of switching between the normal and highlighted states.
This is the method in the button subclass,
-(void)flashWithOnTime:(CGFloat)onTime {
[self setHighlighted:YES];
[self performSelector:#selector(setHighlighted:) withObject:#NO afterDelay:onTime];
}
This is the code in the view controller that starts the sequence (from a button tap). Rather than shuffling, I pick a random index out of an array, and then delete that entry so it can't be picked again. The methods offImageWithSolidColor and onImageWithSolidColor, are just methods I used to create the images.
#interface ViewController ()
#property (strong, nonatomic) IBOutletCollection(RDFlashingButton) NSArray *buttons;
#property (strong,nonatomic) NSArray *indexes;
#property (strong,nonatomic) NSMutableArray *mutableIndexes;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.indexes = #[#0,#1,#2,#3,#4,#5,#6,#7,#8];
for (RDFlashingButton *aButton in self.buttons) {
[aButton setBackgroundImage:[self offImageWithSolidColor] forState:UIControlStateNormal];;
[aButton setBackgroundImage:[self onImageWithSolidColor] forState:UIControlStateHighlighted];
}
}
-(void)flashRandomly {
if (self.mutableIndexes.count > 0) {
int index = arc4random_uniform(self.mutableIndexes.count);
int value = [self.mutableIndexes[index] intValue];
RDFlashingButton *aButton = self.buttons[value];
[aButton flashWithOnTime:0.4];
[self.mutableIndexes removeObjectAtIndex:index];
[self performSelector:#selector(flashRandomly) withObject:nil afterDelay:.4];
}
}
-(IBAction)startFlashing:(id)sender {
self.mutableIndexes = [self.indexes mutableCopy];
[self flashRandomly];
}
I'm developing a radio streaming app with XCode 4.5 for iOS 6 and mainly using storyboards.
I successfully made it to be able to play in background. So wherever I go from my app, whether to other tabs or even after clicking the Home button on the simulator,it keeps playing.
I'm using Matt Gallagher's audio streamer, which I include in my app delegate .m file below
#pragma mark - audio streaming
- (void)playAudio:(indoRadio *)np withButton:(NSString *)currentButton
{
if ([currentButton isEqual:#"playbutton.png"])
{
[self.nowplayingVC.downloadSourceField resignFirstResponder];
[self createStreamer:np.URL];
[self setButtonImageNamed:#"loadingbutton.png"];
[audioStreamer start];
}
else
{
[audioStreamer stop];
}
}
- (void)createStreamer:(NSString *)url
{
if (audioStreamer)
{
return;
}
[self destroyStreamer];
NSString *escapedValue = (__bridge NSString *)CFURLCreateStringByAddingPercentEscapes(nil,(CFStringRef)url,NULL,NULL,kCFStringEncodingUTF8);
NSURL *streamurl = [NSURL URLWithString:escapedValue];
audioStreamer = [[AudioStreamer alloc] initWithURL:streamurl];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(playbackStateChanged:)
name:ASStatusChangedNotification
object:audioStreamer];
}
- (void)playbackStateChanged:(NSNotification *)aNotification
{
NSLog(#"playback state changed? %d",[audioStreamer isWaiting]);
if ([audioStreamer isWaiting])
{
[self setButtonImageNamed:#"loadingbutton.png"];
}
else if ([audioStreamer isPlaying])
{
[self setButtonImageNamed:#"stopbutton.png"];
}
else if ([audioStreamer isIdle])
{
[self destroyStreamer];
[self setButtonImageNamed:#"playbutton.png"];
}
}
- (void)destroyStreamer
{
if (audioStreamer)
{
[[NSNotificationCenter defaultCenter]
removeObserver:self
name:ASStatusChangedNotification
object:audioStreamer];
[audioStreamer stop];
audioStreamer = nil;
}
}
- (void)setButtonImageNamed:(NSString *)imageName
{
if (!imageName)
{
imageName = #"playButton";
}
self.nowplayingVC.currentImageName = imageName;
UIImage *image = [UIImage imageNamed:imageName];
[self.nowplayingVC.playBtn.layer removeAllAnimations];
[self.nowplayingVC.playBtn setImage:image forState:0];
if ([imageName isEqual:#"loadingbutton.png"])
{
[self.nowplayingVC spinButton];
}
}
The problem I got is that when I click the play button, it starts the audio streamers but doesn't change into either loading button or stop button. This makes me unable to stop the audio since the button image is not changed. And since I put an NSLog inside the playbackStateChanged: method,I know that the playback state did change. It seems like the setButtonImagedNamed: method is not firing. Any thoughts?
Please have look of my answer:Disabled buttons not changing image in
For a button there is normal, selected, highlighted and disabled state.
So in your case you have to handle the normal and selected state.
In the xib, set image to a button for normal and selected state.
Now in the function of playAudio instead of checking the image name check the button state as below:
- (void)playAudio:(indoRadio *)np withButton:(NSString *)currentButton
{
if (playButton.selected)
{
//Stop the streaming
//set selection NO
[playButton setSelected:NO];
}
else
{
//Start the streaming
//set selection YES
[playButton setSelected:YES];
}
}
playButton is the IBOutlet of play button.
As you have set the images in xib, so the image automatically get changed as per the state of the image
In case you have added the playBtn programmatically, check if the button is declared as weak or its getting released somewhere. If it is weak, make it strong.
In case the button is in the nib file, then the problem is in the IBOutlet. Redo the connection properly.
It turned out that the delegate didn't know which nowplayingVC I was calling. #sarp-kaya's question about Calling a UIViewController method from app delegate has inspired me.
I actually have put this code in my view controller viewDidLoad:
myAppDelegate* appDelegate = [UIApplication sharedApplication].delegate;
but I forgot to add this line:
appDelegate.nowplayingVC = self;
So, all I need to do is adding that one line and it works now. okay, so silly of me for not noticing such a basic thing. But thanks for your helps :D
All,
I am attempting to load a set of sounds asynchronously when I load a UIViewController. At about the same time, I am (occasionally) also placing a UIView on the top of my ViewController's hierarchy to present a help overlay. When I do this, the app crashes with a bad exec. If the view is not added, the app does not crash. My ViewController looks something like this:
- (void)viewDidLoad
{
[super viewDidLoad];
__soundHelper = [[SoundHelper alloc] initWithSounds];
// Other stuff
}
- (void)viewDidAppear:(BOOL)animated
{
// ****** Set up the Help Screen
self.coachMarkView = [[FHSCoachMarkView alloc] initWithImageName:#"help_GradingVC"
coveringView:self.view
withOpacity:0.9
dismissOnTap:YES
withDelegate:self];
[self.coachMarkView showCoachMarkView];
[super viewDidAppear:animated];
}
The main asynchronous loading method of SoundHelper (called from 'initWithSounds') looks like this:
// Helper method that loads sounds as needed
- (void)loadSounds {
// Run this loading code in a separate thread
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
NSBlockOperation *loadSoundsOp = [NSBlockOperation blockOperationWithBlock:^{
// Find all sound files (*.caf) in resource bundles
__soundCache = [[NSMutableDictionary alloc]initWithCapacity:0];
NSString * sndFileName;
NSArray *soundFiles = [[NSBundle mainBundle] pathsForResourcesOfType:STR_SOUND_EXT inDirectory:nil];
// Loop through all of the sounds found
for (NSString * soundFileNamePath in soundFiles) {
// Add the sound file to the dictionary
sndFileName = [[soundFileNamePath lastPathComponent] lowercaseString];
[__soundCache setObject:[self soundPath:soundFileNamePath] forKey:sndFileName];
}
// From: https://stackoverflow.com/questions/7334647/nsoperationqueue-and-uitableview-release-is-crashing-my-app
[self performSelectorOnMainThread:#selector(description) withObject:nil waitUntilDone:NO];
}];
[operationQueue addOperation:loadSoundsOp];
}
The crash seems to occur when the block exits. The init of FHSCoachMarkView looks like this:
- (FHSCoachMarkView *)initWithImageName:(NSString *) imageName
coveringView:(UIView *) view
withOpacity:(CGFloat) opacity
dismissOnTap:(BOOL) dismissOnTap
withDelegate:(id<FHSCoachMarkViewDelegate>) delegateID
{
// Reset Viewed Coach Marks if User Setting is set to show them
[self resetSettings];
__coveringView = view;
self = [super initWithFrame:__coveringView.frame];
if (self) {
// Record the string for later reference
__coachMarkName = [NSString stringWithString:imageName];
self.delegate = delegateID;
UIImage * image = [[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:imageName ofType:#"png"]];
// ****** Configure the View Hierarchy
UIImageView *imgView = [[UIImageView alloc] initWithImage:image];
[self addSubview:imgView];
[__coveringView.superview insertSubview:self aboveSubview:__coveringView];
// ****** Configure the View Hierarchy with the proper opacity
__coachMarkViewOpacity = opacity;
self.hidden = YES;
self.opaque = NO;
self.alpha = __coachMarkViewOpacity;
imgView.hidden = NO;
imgView.opaque = NO;
imgView.alpha = __coachMarkViewOpacity;
// ****** Configure whether the coachMark can be dismissed when it's body is tapped
__dismissOnTap = dismissOnTap;
// If it is dismissable, set up a gesture recognizer
if (__dismissOnTap) {
UITapGestureRecognizer * tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(coachMarkWasTapped:)];
[self addGestureRecognizer:tapGesture];
}
}
return self;
}
I have tried invoking the asynchronous block using both NSBlockOperation and dispatch_async and both have had the same results. Additionally, I've removed the aysnch call altogether and loaded the sounds on the main thread. That works fine. I also tried the solution suggested by #Jason in: NSOperationQueue and UITableView release is crashing my app but the same thing happened there too.
Is this actually an issue with the view being added in FHSCoachMarkView, or is it possibly related to the fact that both access mainBundle? I'm a bit new to asynch coding in iOS, so I'm at a bit of a loss. Any help would be appreciated!
Thanks,
Scott
I figured this out: I had set up a listener on the SoundHelper object (NSUserDefaultsDidChangeNotification) that listened for when NSUserDefaults were changed, and loaded the sounds if the user defaults indicated so. The FHSCoachMarkView was also making changes to NSUserDefaults. In the SoundHelper, I was not properly checking which defaults were being changed, so the asynch sound loading method was being called each time a change was made. So multiple threads were attempting to modify the __soundCache instance variable. it didn't seem to like that.
Question: Is this the correct way to answer your own question? Or should I have just added a comment to the question it self?
Thanks.