7

I want to implement a volume shutter in my camera app. When the user presses the volume button, I should get an event to take a photo.

I'm looking for an implementation that meets the following requirements:

  • It should work even if the volume is currently at the maximum, and the user presses the volume up button.
  • There should be no on-screen UI showing that the volume changed.
  • There should be no known cases of Apple rejecting an app that used this technique.

Other questions and answers exist on this topic, but for older versions of iOS, so I wanted to find one that works on iOS 11.

Camera apps like ProCamera, ProCam and Camera+ have the volume shutter that satisfies all these conditions, so it's clearly possible.

Marina Aguilar
  • 1,151
  • 9
  • 26
Kartick Vaddadi
  • 4,818
  • 6
  • 39
  • 55
  • http://fredandrandall.com/blog/2011/11/18/taking-control-of-the-volume-buttons-on-ios-like-camera/ – Coldsteel48 Apr 08 '17 at 17:37
  • What have you tried so far? Show us some code. – backslash-f Apr 08 '17 at 17:40
  • @backslash-f I wasn't sure what technique to try, since the one(s) I researched all suffered from the above flaws. Before a lot of trial and error, I wanted to see if there's a better solution that has already been found. – Kartick Vaddadi Apr 09 '17 at 01:56
  • @ColdSteel Other stack overflow answers point out that many volume shutter techniques that worked five years back no longer work on modern iOS versions. – Kartick Vaddadi Apr 09 '17 at 02:19
  • Does [this](http://stackoverflow.com/questions/8397170/how-to-implement-a-volume-key-shutter-for-iphone) still work? It's for IOS5+ – Wouter Vanherck Apr 09 '17 at 08:29
  • 1
    No one can guarantee your third condition! :) – matt Apr 11 '17 at 04:27
  • If Apple has approved other apps that use this technique (as opposed to approving some and rejecting some), that's good enough for me. – Kartick Vaddadi Apr 11 '17 at 04:31

2 Answers2

10

So here is the code that will meet all your requirements – I'm not sure whether or not Apple will approve this though.
I've pulled all this code from questions/answers here on StackOverflow.

Tested with iOS 10.2 in Xcode 8.3.1

You need to use the AVFoundation and MediaPlayer frameworks for this to work.

import UIKit
import AVFoundation
import MediaPlayer

class ViewController: UIViewController {

    //keeps track of the initial volume the user had set when entering the app
    //used to reset the volume when he exits the app
    var volume: Float = 0

    override func viewDidLoad() {
        super.viewDidLoad()

        let audioSession = AVAudioSession.sharedInstance()
        volume = audioSession.outputVolume-0.1 //if the user is at 1 (full volume)
        try! audioSession.setActive(true)
        audioSession.addObserver(self, forKeyPath: "outputVolume", options: NSKeyValueObservingOptions.new, context: nil)
        //prevents the volume hud from showing up
        let volView = MPVolumeView(frame: .zero)
        view.addSubview(volView)
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        //when the user changes the volume,
        //prevent the output volume from changing by setting it to the default volume we specified,
        //so that we can continue pressing the buttons for ever
        (MPVolumeView().subviews.filter{NSStringFromClass($0.classForCoder) == "MPVolumeSlider"}.first as? UISlider)?.setValue(volume, animated: false)

        //implement your photo-capturing function here
        print("volume changed")
    }
}

Update

If you want to make sure your code is still working after the user exits the app, use the AppDelegate to install the observer when the app becomes active, like this:

AppDelegate

import UIKit
import AVFoundation
import MediaPlayer

var volume: Float = 0.5

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    let audioSession = AVAudioSession.sharedInstance()

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        return true
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        (MPVolumeView().subviews.filter{NSStringFromClass($0.classForCoder) == "MPVolumeSlider"}.first as? UISlider)?.setValue(volume, animated: false)

        NotificationCenter.default.removeObserver(self)
        NotificationCenter.default.post(Notification(name: Notification.Name(rawValue: "volumeChanged")))
    }

    func applicationDidBecomeActive(_ application: UIApplication) {
        volume = audioSession.outputVolume
        if volume == 0 { volume += 0.1 } else if volume == 1 { volume -= 0.1 }
        try! audioSession.setActive(true)
        audioSession.addObserver(self, forKeyPath: "outputVolume", options: NSKeyValueObservingOptions.new, context: nil)
    }

    func applicationWillResignActive(_ application: UIApplication) {
        audioSession.removeObserver(self, forKeyPath: "outputVolume")
    }
}

ViewController

import UIKit
import AVFoundation
import MediaPlayer

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        NotificationCenter.default.addObserver(self, selector: #selector(self.volumeChanged), name: Notification.Name(rawValue: "volumeChanged"), object: nil)
        //prevents the volume hud from showing up
        let volView = MPVolumeView(frame: .zero)
        view.addSubview(volView)
    }

    func volumeChanged() {
        print("volume changed")
    }
}
LinusGeffarth
  • 27,197
  • 29
  • 120
  • 174
  • Thanks. When I switch to another app and back, it no longer works — adjusts the ringer volume. – Kartick Vaddadi Apr 12 '17 at 14:37
  • 1
    Keep in mind, though, that this is not a service website. You can't expect people to give complete solutions without even trying anything yourself. The code I've provided is a decent basic solution to your problem which you should start tweaking to fully meet your requirements. – LinusGeffarth Apr 12 '17 at 14:39
  • I wanted to check if someone already found an answer that gets all the edge cases right before spending time figuring it out again, which I often do, BTW. If you have only a partial solution, that's fine, and I appreciate what you've given me. My comment will also be useful to other people seeing your answer. I'll award you the bounty if no better answer comes along by the time it expires. – Kartick Vaddadi Apr 12 '17 at 14:45
  • So, with about 5 minutes of extra coding, I found the solution to the issue you named in your comment. See my updated answer. – LinusGeffarth Apr 12 '17 at 15:06
  • You might want to handle the issue that when the user hits the volume buttons, the press is detected twice, so implement some sort of counter for that. – LinusGeffarth Apr 12 '17 at 15:07
  • Nah it's cool. I just don't like when people (in general) expect others to do the complete work for them ;) – LinusGeffarth Apr 12 '17 at 15:08
  • So feel free to ask if you get stuck somewhere else. – LinusGeffarth Apr 12 '17 at 15:08
  • Ignore if busy. This doesn't work if the volume was already at zero when the app launches (say if you muted the audio in Youtube), and the user presses volume down. I tried setting the MPVolumeView's slider, which works, but shows the HUD. Apparently the HUD suppression code works only when you adjust the slider in response to the user changing the volume. I tried setting the slider on the same MPVolumeView vs a new instance. I tried doing it asynchronously. And after a 3-second delay. I tried waiting for it to be added to a window. Anything else you can think for me to try? – Kartick Vaddadi Apr 14 '17 at 02:03
  • Ugh. The second line in `applicationDidBecomeActive` does not handle that? What if you update the phone's volume in there? And does the ringer setting have anything to do with it? – LinusGeffarth Apr 14 '17 at 06:53
  • The second line stores the volume in a property; doesn't update it. No, the ringer setting seems irrelevant. If I update the phones volume, I get a HUD as my previous comment says. – Kartick Vaddadi Apr 15 '17 at 01:33
  • Ohh gotcha. What if you post the notification (volumeChanged) at the end of `applicationDidBecomeActive`? – LinusGeffarth Apr 15 '17 at 08:34
  • In case you'd like to take a look, here's my code, which I built using yours as a starting point: https://pastebin.com/5QubqvnX and https://pastebin.com/vMVxFt2W – Kartick Vaddadi Apr 20 '17 at 06:02
  • Is this not using Private API by hacking the slider part of a private view? – Deepak Sharma May 07 '17 at 04:27
  • Idk if you can call this "hacking". As I stated in my post, idk whether this will be approved by Apple. You should check back with OP in a while to see whether he's tried to release the app to the AppStore. @DeepakSharma – LinusGeffarth May 07 '17 at 08:24
  • Ok this solution needs modification for my needs. I am able to hide MPVolumeView as you did. But in the second screen, I use AVPlayer to play the recorded video. If the user increases/decreases volume in the player and comes back to the camera, it doesn't works. The MPVolumeView is then unhidden! What to do? – Deepak Sharma Jun 11 '19 at 17:21
  • Can anyone confirm that Apple has approved an app that used this code? – johnklawlor Jun 13 '21 at 01:10
1

As per Apple's App Store Review Guidelines, this would be explicit grounds for rejection.

2.5.9 Apps that alter the functions of standard switches, such as the Volume Up/Down and Ring/Silent switches, or other native user interface elements or behaviors will be rejected.

Source: Is it possible to disable volume buttons in iOS apps?

Community
  • 1
  • 1
metc500
  • 29
  • 3
  • but how snapchat is doing it then? have any idea? – Ahmad May 18 '21 at 17:51
  • Thanks for the relevant primary source. Maybe it's considered a legit, expectable function as Apples Camera App does it likewise and so not "altering" when used to take a photo. – Marcus Rohrmoser Aug 23 '21 at 08:38