Is there any way to subscribe to volume buttons press events?
After the recent rejections from Apple
Do not use this. Apple now uses some patch which would reject your app straightaway if it uses any of the private APIs - though should note here that quite some apps on the App Store use this already and are still there!
The only way to do this now is to have an AVAudioPlayer prepared to play but not playing ([player prepareToPlay]). This seems to take care of adjusting the app's volume according to the rocker buttons.
There's no other published way currently to handle this.
PLEASE READ THE ABOVE NOTE
Yes, Use the MPVolumeView
MPVolumeView *volume = [[[MPVolumeView alloc] initWithFrame:CGRectMake(18.0, 340.0, 284.0, 23.0)] autorelease];
[[self view] addSubview:volume];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(volumeChanged:)
name:#"AVSystemController_SystemVolumeDidChangeNotification"
object:nil];
for (UIView *view in [volume subviews]){
if ([[[view class] description] isEqualToString:#"MPVolumeSlider"]) {
volumeViewSlider = view; //volumeViewSlider is a UIView * object
}
}
[volumeViewSlider _updateVolumeFromAVSystemController];
-(IBAction)volumeChanged:(id)sender{
[volumeViewSlider _updateVolumeFromAVSystemController];
}
This will give you a slider (same as one used in ipod) whose value will change acc to volume of the phone
You will get a compile-time warning that view may not respond to _updateVolumeFromAVSystemControl, but just ignore it.
If you just want to get the notifications, I think it is like this:
Please correct me if I am wrong, but I don't believe this uses any internal API.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(volumeChanged:)
name:#"AVSystemController_SystemVolumeDidChangeNotification"
object:nil];
Details of this event are here: http://www.cocoadev.com/index.pl?AVSystemController
The other replies here seem to be based on this hack: http://blog.stormyprods.com/2008/09/proper-usage-of-mpvolumeview-class.html which was a workaround for a now-fixed bug.
But I'm pretty sure if all you want to do is GET the notification, and not SET the system volume, you can just use the notification center like with any other event!!
Be advised: since Apple added the volume-up action to the camera, this notification is not posted while a UIImagePickerController is visible.
The easiest and most functionally complete way to do this that I have found in studying all the sources mentioned above and in other threads is: JPSVolumeButtonHandler (I am not involved other than being a user. But thanks a lot to the people responsible!)
EDIT: Release 1.0.2 came with some significant changes/enhancements. I'll leave my prior answer for 1.0.1 below the fold.
I put a sample wrapper class that you can either deploy as-is, or use to learn JPSVolumeButtonHandler's hopefully proper use in a separate Github repository real quick here.
Here's how the wrapper is meant to be used (I'll add this to the repository as soon as I get to it):
The singleton class has two flags: isInUse and isOn. isInUse is meant to be set in some sort of general app settings and switches button support on and off in general.
So, no matter any other values in the class, if this is false nothing will happen when the user presses a volume button and the implementation makes sure as much as possible to keep things clean and not affect system volume level unnecessarily. (Read the issue mentioned in the README for what can happen, when button support is switched on for the first time.)
isOn is meant to be true exactly for the duration that the button is needed. You can switch it on and off without regard to the present value of isInUse.
In whichever view you initialize the action that's supposed to happen when a volume button gets pressed, set the action like so:
PhysicalButton.shared.action = { /* do something */ }
The action has type () -> Void. Until you initialize the action, nothing will break. Just nothing will happen. This defensive functionality was important to me as the view that uses volume button support would only be created after button support is set up.
For seeing things in action, you can download the app that I am using this in real quick for free. The settings manipulate "Physical button support" in general. The main Stopwatch view is the one to actually switch button handling on when entering the view, and off on leaving it. If you find the time, you'll also find an important note there in Settings > User Guide > Option: Physical Button Support:
In exceptional circumstance, the app may not get a chance to properly
switch volume button handling off outside the Stopwatch view...
I'll add the full note to the Github README.md. Feel free to adapt and reuse it, if it's relevant in your case.
The circumstances aren't actually that exceptional and I haven't fully figured out what's wrong. When the user kills the app (or you just stop your app from within Xcode) while volume buttons are on, physical button support may not properly be removed from the OS. Thus, you can end up with two internal handler instances, only one of which you have control over. So, then every button tap results in two or even more calls to the action routine. My wrapper has some guardian code to prevent too rapid an invocation of the button. But that's only a partial solution. The fix need to go into the underlying handler, which I regrettably still have too little an understand of to try to fix things myself.
OLD, FOR 1.0.1:
In particular, my interest was in a Swift solution. The code is in Objective-C. To save someone some research, this is all I did using Cocoapods (for dummies like me):
Add pod 'JPSVolumeButtonHandler' to the podfile
Run pod install on the command line
Add #import <JPSVolumeButtonHandler.h> to the bridging header file
Set up callbacks for the volume up and down buttons like so:
let volumeButtonHandler = JPSVolumeButtonHandler(
upBlock: {
log.debug("Volume up button pressed...")
// Do something when the volume up button is pressed...
}, downBlock: {
log.debug("Volume down button pressed...")
// Do something else for volume down...
})
That's it. The rest is optional.
In my case, I wanted to enable overlaying physical button pushes with virtual on-screen buttons just for select views, while making sure to block as little of the normal button functions as possible (so that the user can run music in the background and adjust its volume in the rest of the app just fine). I ended up with a mostly singleton class as follows:
class OptionalButtonHandler {
static var sharedInstance: OptionalButtonHandler?
private var volumeButtonHandler: JPSVolumeButtonHandler? = nil
private let action: () -> ()
var enabled: Bool {
set {
if !enabled && newValue {
// Switching from disabled to enabled...
assert(volumeButtonHandler == nil, "No leftover volume button handlers")
volumeButtonHandler = JPSVolumeButtonHandler(upBlock: {
log.debug("Volume up button pressed...")
self.action()
}, downBlock: {
log.debug("Volume down button pressed...")
self.action()
})
} else if enabled && !newValue {
log.debug("Disabling physical button...")
// The other way around: Switching from enabled to disabled...
volumeButtonHandler = nil
}
}
get { return (volumeButtonHandler != nil) }
}
/// For one-time initialization of this otherwise singleton class.
static func initSharedInstance(action: () -> ()) {
sharedInstance = OptionalButtonHandler(action: action)
}
private init(action: () -> ()) {
self.action = action
}
}
There is just one common action for both up and down volume buttons here. The initSharedInstance() was necessary, because my action included references to a UI element (a view) that would only be set up at some user-dependent point after app launch.
One-time set up like so:
OptionalButtonHandler.initSharedInstance({
// ...some UI action
})
Enable/disable selectively simply like so:
OptionalButtonHandler.sharedInstance!.enabled = true // (false)
(Notice that my code logic makes sure that .enabled is never accessed before initSharedInstance().)
I am running Xcode 7.3 and iOS 9.3.2 on the (required!) test device.
Looking forward to learning how Apple feels about overloading their precious volume buttons. At least my app makes sure to be minimally invasive and the button use really makes sense. It's not a camera app, but comparable apps have used physical volume buttons before (less nicely even).
If you are willing to dip into the private API, I have a patch to Wolf3d that adds exactly the functionality you are looking for. It uses the private AVSystemController class and some hidden methods on UIApplication
Okay,
So I saw your solutions and don't exactly know whether Apple is going to reject or accept using AVSystemController_SystemVolumeDidChangeNotification. But I have a work around.
Use UISlider of MPVolumeView for registering for any changes in volume by the iPhone hardware like this
MPVolumeView *volumeView = [[MPVolumeView alloc] initWithFrame:CGRectZero];
for (UIView *view in [volumeView subviews]) {
if ([view.class.description isEqualToString:#"MPVolumeSlider"]){
self.volume_slider = (UISlider*)view;
break;
}
}
[volumeView sizeToFit];
#THIS IS THE MAIN LINE. ADD YOUR CALLBACK TARGET HERE
[self.volume_slider addTarget:self action:#selector(volumeListener:) forControlEvents:UIControlEventValueChanged];
[self addSubview:volumeView];
[volumeView setAlpha:0.0f];
-(void)volumeListener:(NSNotification*)notification {
#UPDATE YOUR UI ACCORDING OR DO WHATEVER YOU WANNA DO.
#YOU CAN ALSO GET THE SOUND STEP VALUE HERE FROM NOTIFICATION.
}
Let me know if this helps anyone.
Related
Similar to this question, but I am looking for a generic solution or design pattern or framework.
Q. How to add state management into all UI controls in my iOS app automatically without the need to rewrite the existing controls' class?
Example:
e.g. When I click on a UIButton, it will create a new UIWebView showing Google home page. That is easy, but problem arise when user sometimes.. click the button just too fast, so two webview will be displayed.
To solve this question, I would need to make a singleton class which contain the webview, and have a state variable isOpended and if it is true, reuse the existing webview instead of creating a new one.
But the problem is: If I want this behavior in other controls also, then I would need to create many many singleton classes..I am just thinking if there is better way to handle this without the new to re-invent the wheel.
Thanks.
I think you're solving the wrong problem here. Why don't you disable the button until the UIWebView is done processing. That way the user cannot click it twice.
- (IBAction)showMapHomepage:(UIButton*)sender
{
sender.enabled = NO;
[self taskThatTakesALongTimeWithCompletion:^{
sender.enabled = YES;
// Finish processing
}];
}
You are misinterpreting the best way to go about solving your problem. First of all, you should never find yourself in a situation where you are creating many many singletons. Singletons are a necessary evil, but you should not overuse nor abuse them. Here is a good post about singletons in objective-c.
There are numerous ways you could go about preventing a second UIWebView from being displayed when the user clicks your button.
As someone else stated, one solution would be to disable the button so that the user cannot "double-click" it. You do this using:
button.enabled = NO;
You could also hide your button using:
button.hidden = YES;
Or, in the header of the class that contains your UIButton, you could create a boolean that will handle the logic of whether or not the button has been pressed;
// declare this in your header
BOOL buttonPressed;
// this is the IBAction that your button hooks up to
- (IBAction)createWebViewButtonPressed:(id)sender {
if(!buttonPressed) {
buttonPressed = YES;
// insert code here to create your UIWebView
}
}
Again, there are numerous ways to accomplish what you are trying to do. You just have to determine which method is the best for you.
I agree with other answers that you should probably disable the control if you don't want it to be activated twice. However, if you do want an answer for your actual question about a generic pattern that you can use on all controls then you can use associated objects...
- (IBAction)buttonAction:(UIButton*)sender
{
NSString* webViewKey = #"AssociatedWebView";
// See if there is web view already
id webView = objc_getAssociatedObject(sender, webViewKey);
if(webView == nil)
{
// There is no existing web view, create it
webView = [self theWebView];
// Associate it with the button
objc_setAssociatedObject(sender, webViewKey, webView, OBJC_ASSOCIATION_RETAIN);
// Add the web view
[self.view addSubview:webView];
}
}
The above shows a generic way to associate an object to an instance of UIButton so you can check if it is already associated and re-use the existing one. I provide this answer in case you intend to use this in some other way that isn't fully described in your question, but in practice, you could use a property of your controller for the webView that lazy-loads the webView if it isn't already loaded.
If you really want to simulate the singleton style that you discuss in your question (so that you can have many UIButton instances that all share the same webView object if it already exists) then you could associate the webView onto the [UIButton class] object or even the [UIControl class] object instead of your specific instance. You would do that by replacing the sender with [UIControl class] in the code above.
One possible solution is to store a pointer to the webview in a property of the viewController. In the getter for the webview, create the webview if it doesn't exist already. The button action just need to display the webview, as it will just redisplay the webview if it already exist, and it will create the webview if it doesn't. When you are done with the webview, just set it to nil.
I've built an app that uses a UITableView inside a UINavigationController, inside a UITabBarController. Every entry in the UITableView opens up a view that contains some basic text, buttons, but most importantly, an MPMoviePlayerController that plays audio when started. A user can click this MPMoviePlayerController and continue to browse around the rest of the app (different tabs, or moving back in the navcontroller, opening other views from the tableview) and continue to hear the audio.
I'd like the user to be able to return to the view with the active MPMoviePlayerController at any time. I understand how I would go about allowing the user to return to a certain view from any view, but I'm struggling with how to prevent that view from being reloaded when the user tries accessing the same view.
Is there any way I can save a view in memory? Or save the active MPMoviePlayerController as some type of global object, so that I can at least access that from anywhere?
I appreciate any and all help. Thanks!
I'd recommend you create a property for the MPMoviePlayerController in your app's UIApplicationDelegate (which you can then access from anywhere in the code with [UIApplication sharedApplication].delegate but you will need to cast to your UIApplicationDelegate subclass).
When you come to enter the screen which plays content, check whether your movie player property in the app delegate is nil, if it is create it, otherwise re-use it.
Don't forget to release the reference to your MPMoviePlayerController when the media stops playing, or when the media has already stopped and you get a memory warning or when your app shuts down.
The down side of this approach is it causes coupling between most of your view controllers and your app delegate. You could mitigate this with the use of a protocol however.
You should simply retain it. Like this [myView retain] and keep a pointer to it in where you need. When you want myView to appear, just add it as a subview to current visible view like[myController.view addSubview:myView].
Hope that will help, Good luck!
I've found that even adding a retain doesn't do the trick. I've actually found the best success with overriding the setView (since part of unloading the view involves calling setView:nil. I have a BOOL that gets set the FIRST time the VC loads and once thats set it will never allow setView to be called again.
- (void) setView: (UIView*) view{
NSLog(#"MainViewController: setView");
// this is our attempt to stop iOS from unloading our view.. when iOS tries to unload your view they call setView:nil.. so, no!
if(!viewDidAppear) [super setView:view];
}
A little bit of a hack, but you can override setView: in your subclass so that it never allows to set the view to nil:
-(void)setView:(UIView *)view
{
if (view == nil) return;
[super setView:view];
}
After reading the iPhone Human Interface Guidelines, I notice there's little mention of checkboxes in the style that one may encounter them on the desktop or web.
Checkboxes are generally handled by UISwitchs on the iPhone, but for an app I'm presently working on, they're really not the right control. Instead, the control you'll see in Mail is a much better fit:
Actual mail blanked out. Obviously.
How would I go about using these checkbox controls in my app? Are they standard, or will I need to imitate them with a custom control?
Cheers friends.
You'll need to create a custom control. It won't be difficult since UIControl already has 'selected', 'highlighted' and 'state' properties at your disposal. You'll just need to draw and toggle appropriately.
Don't subclass UIControl. What you want is a UIButton of "custom" type. Load it with your "unlit" image in IB (or programmatically in -viewDidLoad--you can set it appropriate to its data there too, if you came here with that property already "checked").
Point its touchUpInside event at a method called -(void)toggleCheckBox, and in that method, toggle whatever setting you're toggling (probably a BOOL property of the objects you're listing), and toggle the "lit/unlit" status of the button image by using its -setImage: forState: method. Use the control state UIControlStateNormal.
I do something similar where I let people poke a button to toggle the "favorite" status of the thing ("thisEvent"--a member of an array of local cultural/arts events) they're looking at:
- (IBAction)toggleFavorite {
if (self.thisEvent.isFavorite == YES) {
self.thisEvent.isFavorite = NO;
[self.favoriteButton setImage:[UIImage imageNamed:#"notFavorite.png"] forState:UIControlStateNormal];
}
else {
self.thisEvent.isFavorite = YES;
[self.favoriteButton setImage:[UIImage imageNamed:#"isFavorite.png"] forState:UIControlStateNormal];
}
}
I'm pretty certain there is no standard way to do this. However it's fairly simple to achieve, all you need is two images, one for each state. I would probably do something simple like subclass UIImageView and add a setState:(BOOL)theState method, which would then simply select the relevant image.
I'd rather subclass UITableViewCell then UIImageView. UITableViewCell allready comes with selected/unselected states and handlers for editmodes etc.
As said before, you'll need to subclass UIControl. The actual process was discussed here w little while ago.
I also found a description of another way to do this using the same image/method that the Mail app uses:
http://networkpx.blogspot.com/2009/07/multiple-row-selection-with-uitableview.html
but as this implements undocumented features of the iOS SDK, it may not be best for apps intended for the official App Store.
So i've created a custom TableViewCell, and in the nib I put a UISwitch. Before I had even hooked it up to anything, if I ran it in the simulator and clicked on it, it would switch from off to on with the animation and such.
I'm trying to add a feature where the user is only allowed to change from off to on when a certain condition is true. I want it so, when the user touches the switch, it checks if the condition is true, and if it isn't it the switch doesn't move.
I've set up an IBAction where if the user Touches Up Inside, it'll run my function. My function is this:
if([on_switch isOn])
{
if([my_switch canSwitchOn])
{
NSLog(#"SWITCHED ON SUCCESSFULLY");
[on_switch setOn:TRUE animated:TRUE];
}
else
{
NSLog(#"SWITCHED ON UNSUCCESSFULLY");
//Put in popup here
}
}
else
{
[[ClassesSingleton sharedSingleton] classSwitchedOff:cell_index];
[on_switch setOn:FALSE animated:TRUE];
}
However, no matter what I do, that switch will flip, even though I gave it no directions to do so. I'm pretty sure it's the auto-flip that cause it to do so even before I'd hooked anything up to it. Is there a way to turn that off?
Thanks!
What you need to do is set userInteractionEnabled property to False on your UISwitch.
If you had allready made the connection in Interface Builder to an IBOutlet you delared in your owning class, you would be able to set it in code like this:
mySwitch.userInteractionEnabled = NO;
You could also set the property directly in Interface Builder by selecting the checkbox, as shown below (However in your app, you are going to need to wire the button up to an IBOutlet anyway to implement your conditional logic.)
alt text http://www.clixtr.com/photo/ef06c1f7-8cca-40cd-a454-5ca534ceb9fe
I think you will need something like the following:
// Somewhere just after the creation of _switch
[_switch addTarget:self action:#selector(switchValueDidChange:) forControlEvents:UIControlEventValueChanged];
// Target/Action method
- (void)switchValueDidChange:(UISwitch *)sender {
if(sender.on && [self canSwitchOn] == NO){
[sender setOn:NO animated:YES];
// Popup
}
}
Problem you're having is that the switch has already committed it's on state on touch up. When that's not the case (I'm not sure, never tested) you have to check whether the switch is currently not on. This bit of code will revert the state when the user was not allowed to switch the switch.
A better way is to disable the control, but maybe that's not what you want in this case.
I'm developing a puzzle game application - watch on youtube - for iPhone, and the actual "in-game" part is almost done. It is a separate Class (subclass of UIView) what initializes with a puzzle clue, puzzle pieces, and is ready to send a message for somebody if the puzzle has solved ("completeness" check invoked on every touchesEnded).
Now I'm troubled how to design the whole application pattern programatically.
The game needs a main menu view, a puzzle selector view, fromwhich I can "create" puzzleLevel instances, I have to store the actual puzzle data in a separate class (I suppose), maybe in archive files, and need a preferences view, inwhich I can change the "global" variables that every puzzleLevel instance should use (angular snap values, skins, etc.).
I can feel that I have to do something with the main viewController what controls all the views I've mentioned above, but I don't know how to do it exactly. Where should I store global variables? Where should I store puzzle data? How should I report the "puzzle completeness", and who for should I report? How should I design the view hierarchy?
I'm wondering if somebody could show me some concept, or just a link where I can get along. I'm interestend in concepts mainly, the actual coding part can be "googled" after.
Typically my games have an App object at the top, which owns one of several AppStates (menu, selector, preferences, etc) and switches between them as required, in a pretty typical usage of the State pattern. These states handle their own rendering and input and store whatever resources they need. The App object also owns any global application-wide settings and objects that are shared across the states (eg. rendering, sound). These may be passed in to the states individually, or the states could request the relevant interfaces back from the App at some point.
One of the AppStates will be the game playing state, and that will contain the definition of the current puzzle, plus the current state of this play session (eg. how completed it is). I would tend to still have a separate Game class that is owned by the relevant GamePlayingState, since the former would contain only game logic information and the latter handles input/output.
I think I should use the NSNotification class. It is simply set up a "listener" in the object (viewController) that contains the subviews, then subviews can send notifications to the controller. Then the notification handler can invoke any methods.
viewController part:
-(void) viewDidLoad
{
//Set up a listener.
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(notificationHandler:) name:#"finishedCurrentLevel" object:nil];
...
}
-(void) notificationHandler: (NSNotification*) notification
{
//Notification handling.
if ([notification name] == #"finishedCurrentLevel") [self finishedCurrentLevel];
}
-(void) finishedCurrentLevel
{
//View managing code here...
}
The notification, the listening and the "responds" for the notifications set up this was. The actual notifying goes like this (can be executed from any subviews):
[[NSNotificationCenter defaultCenter] postNotificationName:#"finishedCurrentLevel" object:nil];
It solves my "communication" problem, I think.
About globals I simply created a separate globals.m file with the coressponding globals.h without defining any class. They just "attach" some extern variables, so I can reach them from any file having globals.h imported.