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
Related
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...
I have two views in my app and a plist file to store some values.
In the first view I've created a button called frequenciesButton that opens the second view and another button to restore the default values.
In the second view there is a pickerView and a "Done" button.
On the .m of the first view:
- (void)viewDidLoad {
[super viewDidLoad];
//
self.gameSettings = [[NSMutableDictionary alloc] initWithContentsOfFile:gameSettingsFilePath];
}
-(void)viewWillAppear:(BOOL)animated {
[self refreshView];
}
- (void)refreshView {
[self.frequenciesButton setTitle:[NSString stringWithFormat:#"%# hz and %# hz", [self.gameSettings objectForKey:#"freq-freq1"], [self.gameSettings objectForKey:#"freq-freq2"]] forState:UIControlStateNormal];
...
}
- (IBAction)setDefaultValues:(UIButton *)sender {
[self.gameSettings setValue:#880 forKey:#"freq-freq1"];
[self.gameSettings setValue:#1122 forKey:#"freq-freq2"];
...
[self.gameSettings writeToFile:gameSettingsFilePath atomically:YES];
[self refreshView];
}
When the first view is loaded, the button title is changed to the default values stored in the gameSettings dictionary. The method setTitle: works.
When I click on the frequenciesButton it opens the second view with the pickerView, I select the two new values for the freq-freq1 and freq-freq2 and it saves to the plist file on done button.
The problem is that the frequenciesButton title is not changed when the second view is dissmissed and the first view appears. The refreshView method is called but the button setTitle: does not work.
In this case, if I go back one screen, and return to this view, the button title is updated.
And when I click on defaultValuesButton, the frequenciesButton title changes. The method setTitle: also works.
Any ideas of what must be happening?
HaHA! I love that you added a link to your project.
SO!! The problem was that you have separate properties in each view to hold the data from the saved plist file, self.settings. This is fine, don't mesh them together. The requirement you had to do with this, when switching views, is to keep the ivar or properties updated as the data updates too :D
Here is how I fixed the problem:
- (void)viewWillAppear:(BOOL)animated {
self.settings = [NSMutableDictionary dictionaryWithContentsOfFile: filePath];
[self updateView];
}
I checked out the file and that was updated, but the dictionary in the TestViewController.h was not updated
I hope this was the problem :)
One problem there, not sure if it will fix it, is the fact that you have used ViewWillAppear incorrectly, you have this:
-(void)viewWillAppear:(BOOL)animated {
[self refreshView];
}
but it should be this:
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self refreshView];
}
You need to invoke "[super viewWillAppear:animated];" or you will have side effects, fix that first and see what happens.
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 added a button to my storyboard, and assigned a music to be played when the button is clicked. I used the event touch up inside
My question is, how can I make the button to toggle the music on and off when it is clicked.
Thanks
==================================================================================
For those of you who are interested in seeing the App, I made a repository on github. Here's the link https://github.com/edwinlimantara/IncrediboxV2. Please contribute, if you feel it is an interesting project.
I tried to make something similar to http://incredibox.com/v2/. Check it out! It is a very cool website.
If you are using AVAudioPlayer you can toggle the music player on button tap like:
if(audioPlayer.isPlaying){
[audioPlayer stop];
audioPlayer = nil;
}else{
// initialize audio player and play
}
You can do like below, say below method is called on button tap event then,
-(IBAction)pushClicked:(id)sender
{
UIButton *mButton = (UIButton*)sender;
if (mButton.selected==NO)
{
[mButton setSelected:YES];
//do your stuff here;
}
else if (mButton.selected==YES)
{
[mButton setSelected:NO];
// do your stuff here
}
}
Better you can use UISwitch rather than button to toggle.
If you want to use UIButton then you can use tags and and two different images.
if(soundButton.tag == 1)
{
if(audioPlayer.isPlaying){
[audioPlayer stop];
audioPlayer = nil;
soundButton.tag = 0;
// Change the button image to Off
}
else{
// initialize audio player and play
}
}
else
{
soundButton.tag = 1;
// Play the music & Change the button image to ON
}
In Keynote (and other apps), I've noticed the "standard" interface of doing Undo/Redo is by providing an Undo button on the tool bar.
Clicking the button (that is always enabled) Undos the recent operation.
(If there is not recent operation to undo, it will show the Undo/Redo menu).
Long-clicking the Undo button opens an Undo/Redo menu.
I searched for methods of implementing this, and the best answer I found so far is at the following link.
I wonder if anyone knows of a simpler way?
Thanks!
After reviewing all methods and discussing with friends, below is the solution I used, for a UIBarButtonItem the responds to both taps and long-press (TapOrLongPressBarButtonItem).
It is based on the following principals:
Subclass UIBarButtonItem
Use a custom view (so it's really trivial to handle the long-press - since our custom view has no problem responding to a long-press gesture handler...)
... So far - this approach was in the other SO thread - and I didn't like this approach since I couldn't find and easy enough way of making the custom view appear like an iPad navigation bar button... Soooo...
Use UIGlossyButton by Water Lou (thanks water!). This use is encapsulated within the subclass...
The resulting code is as follows:
#protocol TapOrPressButtonDelegate;
#interface TapOrPressBarButtonItem : UIBarButtonItem {
UIGlossyButton* _tapOrPressButton;
__weak id<TapOrPressButtonDelegate> _delegate;
}
- (id)initWithTitle:(NSString*)title andDelegate:(id<TapOrPressButtonDelegate>)delegate;
#end
#protocol TapOrPressButtonDelegate<NSObject>
- (void)buttonTapped:(UIButton*)button withBarButtonItem:(UIBarButtonItem*)barButtonItem;
- (void)buttonLongPressed:(UIButton*)button withBarButtonItem:(UIBarButtonItem*)barButtonItem;
#end
#implementation TapOrPressBarButtonItem
- (void)buttonLongPressed:(UILongPressGestureRecognizer*)gesture {
if (gesture.state != UIGestureRecognizerStateBegan)
return;
if([_delegate respondsToSelector:#selector(buttonLongPressed:withBarButtonItem:)]) {
[_delegate buttonLongPressed:_tapOrPressButton withBarButtonItem:self];
}
}
- (void)buttonTapped:(id)sender {
if (sender != _tapOrPressButton) {
return;
}
if([_delegate respondsToSelector:#selector(buttonTapped:withBarButtonItem:)]) {
[_delegate buttonTapped:_tapOrPressButton withBarButtonItem:self];
}
}
- (id)initWithTitle:(NSString*)title andDelegate:(id<TapOrPressButtonDelegate>)delegate {
if (self = [super init]) {
// Store delegate reference
_delegate = delegate;
// Create the customm button that will have the iPad-nav-bar-default appearance
_tapOrPressButton = [UIGlossyButton buttonWithType:UIButtonTypeCustom];
[_tapOrPressButton setTitle:title forState:UIControlStateNormal];
[_tapOrPressButton setNavigationButtonWithColor:[UIColor colorWithRed:123.0/255 green:130.0/255 blue:139.0/255 alpha:1.0]];
// Calculate width...
CGSize labelSize = CGSizeMake(1000, 30);
labelSize = [title sizeWithFont:_tapOrPressButton.titleLabel.font constrainedToSize:labelSize lineBreakMode:UILineBreakModeMiddleTruncation];
_tapOrPressButton.frame = CGRectMake(0, 0, labelSize.width+20, 30);
// Add a handler for a tap
[_tapOrPressButton addTarget:self action:#selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside];
// Add a handler for a long-press
UILongPressGestureRecognizer* buttonLongPress_ = [[UILongPressGestureRecognizer alloc] initWithTarget:self
action:#selector(buttonLongPressed:)];
[_tapOrPressButton addGestureRecognizer:buttonLongPress_];
// Set this button as the custom view of the bar item...
self.customView = _tapOrPressButton;
}
return self;
}
// Safe guards...
- (id)initWithImage:(UIImage *)image style:(UIBarButtonItemStyle)style target:(id)target action:(SEL)action {
NSLog(#"%s not supported!", __FUNCTION__);
return nil;
}
- (id)initWithImage:(UIImage *)image landscapeImagePhone:(UIImage *)landscapeImagePhone style:(UIBarButtonItemStyle)style target:(id)target action:(SEL)action {
NSLog(#"%s not supported!", __FUNCTION__);
return nil;
}
- (id)initWithTitle:(NSString *)title style:(UIBarButtonItemStyle)style target:(id)target action:(SEL)action {
NSLog(#"%s not supported!", __FUNCTION__);
return nil;
}
- (id)initWithBarButtonSystemItem:(UIBarButtonSystemItem)systemItem target:(id)target action:(SEL)action {
NSLog(#"%s not supported!", __FUNCTION__);
return nil;
}
- (id)initWithCustomView:(UIView *)customView {
NSLog(#"%s not supported!", __FUNCTION__);
return nil;
}
#end
And all you need to do is:
1. Instantiate is as follows:
TapOrPressBarButtonItem* undoMenuButton = [[TapOrPressBarButtonItem alloc] initWithTitle:NSLocalizedString(#"Undo", #"Undo Menu Title") andDelegate:self];
2. Connect the button to the navigation bar:
[self.navigationItem setLeftBarButtonItem:undoMenuButton animated:NO];
3. Implement the TapOrPressButtonDelegate protocol, and you're done...
-(void)buttonTapped:(UIButton*)button withBarButtonItem:(UIBarButtonItem*)barButtonItem {
[self menuItemUndo:barButtonItem];
}
-(void)buttonLongPressed:(UIButton*)button withBarButtonItem:(UIBarButtonItem*)barButtonItem {
[self undoMenuClicked:barButtonItem];
}
Hope this helps anyone else...
If you are using IB (or in Xcode4 the designer...i guess it is called) then you can select "Undo" from the First responder and drag that action to a button. I can give you more specific instructions if that doesn't cover it.
Here's what it looks like
It's on the left underneath the column "Received actions" at the bottom
I believe the key is actually in the UINavigationBar itself. Unlike UIButtons or other normal touch tracking objects, I suspect UIBarItems don't handle their own touches. They don't inherit UIResponder or UIControl methods. However UINavigationBar of course does. And I've personally added gestures straight to a UINavigationBar many times.
I suggest you override touch handling in a UINavigationBar subclass and check the touches against its children. If the child is your special Undo button you can handle it accordingly.
UIButton* undoButton = [UIButton buttonWithType:UIButtonTypeCustom];
[undoButton addTarget:self action:#selector(undoPressStart:) forControlEvents:UIControlEventTouchDown];
[undoButton addTarget:self action:#selector(undoPressFinish:) forControlEvents:UIControlEventTouchUpInside];
UIBarButtonItem* navButton = [[[UIBarButtonItem alloc] initWithCustomView:undoButton] autorelease];
self.navigationItem.rightBarButtonItem = navButton;
You don't necessarily have to add the UIBarButtonItem as the rightBarButtonItem, this is just and easy way to show you how to create your UIBarButtonItem with a custom view that is the UIButton you want to handle events.
You'll need to implement the undoPressStart: and undoPressFinish: by maintaining state. I'd say on start, store the current NSDate or some granular representation of the time. On finish, if check the time elapsed and if it is beyond a certain threshold, show the menu - otherwise (as well as if the start date was never captured) perform the undo.
As an improvement, you'll likely want to observe the UIControlEventTouchDragExit event as well to cancel the long press.