Alternative to AVSystemController_AudioVolumeNotificationParameter post iOS 15? - ios

The approved KVO approach for responding to device volume level changes stops detecting volume button presses after min/max outputVolume is reached. I'd like to continue to receive those button press events after min/max, so I assume I need to try this solution, even if it's not supported by Apple. However, I'm very much an amateur iOS programmer so I could use a hint. Here's what I've been doing (using RxSwift):
NotificationCenter.default.rx.notification(Notification.Name(rawValue: "AVSystemController_AudioVolumeNotificationParameter"))
.subscribe(onNext: { [weak self] notification in
guard let my = self else { return }
my.volumeNotification.accept(notification.userInfo!["AVSystemController_AudioVolumeNotificationParameter"] as! Double)
})
.disposed(by: disposeBag)
Should I instead be subscribing to a Notification named "MPVolumeControllerDataSource_SystemVolumeDidChange"?
Thanks in advance!

Three cheers for open source, specifically: JPSVolumeButtonHandler. This component works like a champ, and uses the Apple-approved KVO technique. Be aware that this component sets AVAudioSession options to .mixWithOthers which prevents MPRemoteCommandCenter from receiving/handing any BlueTooth commands. So if you need BT (Swift 5):
let volumeButtonHandler = JPSVolumeButtonHandler(up: {
// handle up press
}, downBlock: {
// handle down press
})
volumeButtonHandler.sessionOptions = [] // allow remote BT
I also found that programmatically setting the device volume to 0.5 before initializing the button handler avoided occasional min/max barriers. If the device initial volume was close to the min or max, the handler would stop after a few button presses:
try AVAudioSession.sharedInstance().setActive(true, options: [])
MPVolumeView(frame: .zero).volumeSlider.value = 0.5

Related

skipPrevious and skipNext buttons inactive even with items in queue Google Cast iOS Sender SDK v4.3.5 and above

I have an iOS sender application for video streaming that supports queueing and using the skipPrevious and skipNext buttons to skip forward and backward between videos in the queue. The app works with the google cast sdk v4.3.3 and v4.3.4 but I need to update the sdk to support iOS 13 changes. When I updated the sdk to v4.4.4 the skipPrevious and skipNext button types on the ExpandedMediaControlsViewController always appear greyed out even when I can see both on the receiver and by printing in the sender app that there are items in the queue. The buttons appear greyed out in all versions of the sdk v4.3.5 and later.
I have looked at the Google Chromecast developer documentation and the skipPrevious and skipNext button types are not deprecated and say that they should update automatically if there is something in the queue. I tried modifying google's iOS sender app tutorial code to change the 30 second ffw and rwd buttons to the skip buttons and had the same results after adding items to the queue and playing.
There is another unanswered question about a similar issue that was created in March here: skipNext skipPrevious Google Cast greyed out
I am using an update function inside of my castViewController class to change the expandedMediaControls to the skipPrevious and skipNext types. I call this method when my castViewController gets initialized
private func updatePlayerMediaControls() {
GCKCastContext.sharedInstance().defaultExpandedMediaControlsViewController.setButtonType(.skipPrevious, at: 1)
GCKCastContext.sharedInstance().defaultExpandedMediaControlsViewController.setButtonType(.skipNext, at: 2)
}
I use a function that follows this logic to cast a video or add a video to the queue. Immediately after adding a video to the cast I will add the next video to the queue by setting the appending bool to true.
func loadSelectedItem(_ media: VideoMediaInformation, byAppending appending: Bool) {
if let remoteMediaClient = sessionManager.currentCastSession?.remoteMediaClient {
let mediaQueueItemBuilder = GCKMediaQueueItemBuilder()
mediaQueueItemBuilder.mediaInformation = media.mediaInfo
mediaQueueItemBuilder.autoplay = true
mediaQueueItemBuilder.preloadTime = 1.0
let queueOptions = GCKMediaQueueLoadOptions()
queueOptions.playPosition = media.currentTime ?? 0.0
if appending {
let request = remoteMediaClient.queueInsert(mediaQueueItemBuilder.build(), beforeItemWithID: kGCKMediaQueueInvalidItemID)
request.delegate = self
} else {
let request = remoteMediaClient.queueLoad([mediaQueueItemBuilder.build()], with: queueOptions))
request.delegate = self
GCKCastContext.sharedInstance().presentDefaultExpandedMediaControls()
}
}
}
I would expect that if there are items in the queue that the user would be able to use the skipNext and skipPrevious to skip forward or backward in the queue as episodes are available. The actual results are that the buttons are always disabled.
I worked extensively with this issue. I was experiencing the exact same thing, as well as the issue referenced in the question. I attempted a full update of the library to 4.6.1. Since this package is a static library I was unable to inspect the cause. BUT Good News! I found a suitable work around. This solution does not take into account whether or not there is something available in the queue to move forward or backwards but it met my needs. In addition with the setup above I was able to use custom buttons that trigger the skip backward or forward.
#IBAction func playPressed(_ sender: UIButton) {
// Create and add custom previous button
let prevButton = UIButton()
prevButton.setImage(Icon.mediaPlayerPrevious, for: .normal)
prevButton.addTarget(self, action: #selector(castButtonPrevAction), for: .touchUpInside)
GCKCastContext.sharedInstance()
.defaultExpandedMediaControlsViewController
.setButtonType(.custom, at: 0)
GCKCastContext.sharedInstance()
.defaultExpandedMediaControlsViewController
.setCustomButton(prevButton, at: 0)
// Create and add custom next button
let nextButton = UIButton()
nextButton.setImage(Icon.mediaPlayerNext, for: .normal)
nextButton.addTarget(self, action: #selector(castButtonNextAction), for: .touchUpInside)
GCKCastContext.sharedInstance()
.defaultExpandedMediaControlsViewController
.setButtonType(.custom, at: 3)
GCKCastContext.sharedInstance()
.defaultExpandedMediaControlsViewController
.setCustomButton(nextButton, at: 3)
// Presents the screen
GCKCastContext.sharedInstance().presentDefaultExpandedMediaControls()
}
// Casting custom button actions
#objc func castButtonNextAction(sender: UIButton!) {
GMCastChannel.shared.remoteMediaClient?.queueNextItem()
}
#objc func castButtonPrevAction(sender: UIButton!) {
GMCastChannel.shared.remoteMediaClient?.queuePreviousItem()
}

Why doesn't my iOS (Swift) app properly recognize some external display devices?

So I have an odd issue and my google-fu utterly fails to even provide me the basis of where to start investigating, so even useful keywords to search on may be of use.
I have an iOS application written in swift. I have a model hooked up to receive notifications about external displays. On some adaptors, I'm able to properly detect and respond to the presence of an external display and programatically switch it out to be something other than a mirror (see code block below). But with another adaptor, instead of just 'magically' becoming a second screen, I'm asked to 'trust' the external device, and it simply mirrors the device screen. Not the intended design at all.
func addSecondScreen(screen: UIScreen){
self.externalWindow = UIWindow.init(frame: screen.bounds)
self.externalWindow!.screen = screen
self.externalWindow!.rootViewController = self.externalVC
self.externalWindow!.isHidden = false;
}
#objc func handleScreenDidConnectNotification( _ notification: NSNotification){
let newScreen = notification.object as! UIScreen
if(self.externalWindow == nil){
addSecondScreen(screen: newScreen)
}
}
#objc func handleScreenDidDisconnectNotification( _ notification: NSNotification){
if let externalWindow = self.externalWindow{
externalWindow.isHidden = true
self.externalWindow = nil
}
}
The worst issue here is that because I'm connecting to an external display to do this, I can't even run this code through the debugger to find out what is going on. I don't know where to even begin.
Any ideas?
Edit:
Thanks to someone pointing out wifi debugging, I can tell you my notifications are firing off, but they're both firing at the same time, one after the other, when the external adaptor is disconnected.

addUIInterruptionMonitor(withDescription:handler:) not working on iOS 10 or 9

The following tests works fine on iOS 11. It dismisses the alert asking permissions to use the locations services and then zooms in in the map. On iOS 10 or 9, it does none of this and the test still succeeds
func testExample() {
let app = XCUIApplication()
var handled = false
var appeared = false
let token = addUIInterruptionMonitor(withDescription: "Location") { (alert) -> Bool in
appeared = true
let allow = alert.buttons["Allow"]
if allow.exists {
allow.tap()
handled = true
return true
}
return false
}
// Interruption won't happen without some kind of action.
app.tap()
removeUIInterruptionMonitor(token)
XCTAssertTrue(appeared && handled)
}
Does anyone have an idea why and/or a workaround?
Here's a project where you can reproduce the issue: https://github.com/TitouanVanBelle/Map
Update
Xcode 9.3 Beta's Changelogs show the following
XCTest UI interruption monitors now work correctly on devices and simulators running iOS 10. (33278282)
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
let allowBtn = springboard.buttons["Allow"]
if allowBtn.waitForExistence(timeout: 10) {
allowBtn.tap()
}
Update .exists to .waitForExistence(timeout: 10), detail please check comments.
I had this problem and River2202's solution worked for me.
Note that this is not a fix to get the UIInterruptionMonitor to work, but a different way of dismissing the alert. You may as well remove the addUIInterruptionMonitor setup. You'll need to have the springboard.buttons["Allow"].exists test anywhere the permission alert could appear. If possible, force it to appear at an early stage of the testing so you don't need to worry about it again later.
Happily the springboard.buttons["Allow"].exists code still works in iOS 11, so you can have a single code path and not have to do one thing for iOS 10 and another for iOS 11.
Incidentally, I logged the base issue (that addUIInterruptionMonitor is not working pre-iOS 11) as a bug with Apple. It has been closed as a duplicate now, so I guess they acknowledge that it is a bug.
I used the #River2202 solution and it works better than the interruption one.
If you decide to use that, I strongly suggest that you use a waiter function. I created this one in order to wait on any kind of XCUIElement to appear:
Try it!
// function to wait for an ui element to appear on screen, with a default wait time of 20 seconds
// XCTWaiter was introduced after Xcode 8.3, which is handling better the timewait, it's not failing the test. It uses an enum which returns: 'Waiters can be used with or without a delegate to respond to events such as completion, timeout, or invalid expectation fulfilment.'
#discardableResult
func uiElementExists(for element: XCUIElement, timeout: TimeInterval = 20) -> Bool {
let expectation = XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == true"), object: element)
let result = XCTWaiter().wait(for: [expectation], timeout: timeout)
guard result == .completed else {
return false
}
return true
}

Handler of addUIInterruptionMonitor is not called for Alert related to Photos

private func acceptPermissionAlert() {
_ = addUIInterruptionMonitor(withDescription: "") { alert -> Bool in
if alert.buttons["Don’t Allow"].exists { //doesnt get here second time
alert.buttons.element(boundBy: 1).tapWhenExists()
return true
}
return false
}
}
and this doesn't work for:
In the beginning of the app, it works perfect while accepting permission for notifications, but here, it doesn't work. Why is this?
I'vs found that addUIInterruptionMonitor sometimes doesn't handle an alert in time, or until tests have finished. If it isn't working, try using Springboard, which manages the iOS home screen. You can access alerts, buttons, and more from there, and this is particularly useful for tests where you know exactly when an alert will show.
So, something like this:
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
let alertAllowButton = springboard.buttons.element(boundBy: 1)
if alertAllowButton.waitForExistence(timeout: 5) {
alertAllowButton.tap()
}
The buttons.element(boundBy:1) will ensure you tap the button on the right, change 1 to 0 to tap the left, (because sometimes the ' in "Don't Allow" causes a problem).
Add:
app.tap()
at the end of the method.
This is because you need to interact with the app for the handler to fire.
After adding the interruption monitor, you should continue to interact with the app as if it has not appeared.
Also note that you have a 'smart quote' in your button identifier, instead of a regular apostrophe.
let photosAlertHandler = addUIInterruptionMonitor(withDescription: "Photo Permissions") { alert -> Bool in
if alert.buttons["Don't Allow"].exists {
alert.buttons.element(boundBy: 1).tapWhenExists()
return true
}
return false
}
// Do whatever you want to do after dismissing the alert
let someButton = app.buttons["someButton"]
someButton.tap() // The interruption monitor's handler will be invoked if the alert is present
When the next interaction happens after the alert appears, the interruption monitor's handler will be invoked and the alert will be handled.
You should also remove the interruption monitor when you think you're done with it, otherwise it will be invoked for any other alerts that appear.
removeUIInterruptionMonitor(photosAlertHandler)

MPRemoteCommandCenter works in simulator but not on device

Edited to explain why its not a duplicate:
I am trying to use MPMusicPlayerController.systemMusicPlayer() to control music playback from my app, but I want to disable the Command Center previous track button. I also want to override the default Command Center play and next track functions. The code should be simple:
This code is in ViewController.swift - viewDidLoad
let commandCenter = MPRemoteCommandCenter.sharedCommandCenter()
commandCenter.previousTrackCommand.enabled = false
commandCenter.previousTrackCommand.addTargetWithHandler({ (commandEvent: MPRemoteCommandEvent!) -> MPRemoteCommandHandlerStatus in
self.empty()
return MPRemoteCommandHandlerStatus.Success
})
//MPRemoteCommandCenter.sharedCommandCenter().previousTrackCommand.addTarget(self, action: "empty")
commandCenter.nextTrackCommand.enabled = true
commandCenter.nextTrackCommand.addTargetWithHandler { (commandEvent: MPRemoteCommandEvent!) -> MPRemoteCommandHandlerStatus in
self.gameOver()
return MPRemoteCommandHandlerStatus.Success
}
commandCenter.playCommand.enabled = true
commandCenter.playCommand.addTargetWithHandler { (commandEvent: MPRemoteCommandEvent!) -> MPRemoteCommandHandlerStatus in
self.playing()
return MPRemoteCommandHandlerStatus.Success
}
Also, in AppDelegate.swift - application
UIApplication.sharedApplication().beginReceivingRemoteControlEvents()
And in the iOS simulator (both iPad and iPhone), it works correctly, as can be seen in the first screenshot (of the simulator).
However, when deploying the app to my iPad, none of the MPRemoteCommandCenter commands work at all, as can be seen in the second screenshot (of an actual device).
This is different from the "dupliate" question (How Do I Get Audio Controls on Lock Screen/Control Center from AVAudioPlayer in Swift) in the following ways:
I am not using AVAudioSession, I am using MPMusicPlayerController.systemMusicPlayer()
I have already called beginReceivingRemoteControlEvents, so that cant be the issue (unless I have somehow called it incorrectly, in which case, I would love an answer explaining how else it should be called).
Thank you.
,

Resources