AVPlayerViewController pops entire stack of view controllers when closed - swift

I am using an AVPlayerViewController to display video content within my application. This player can be reached via a sequence of views. The sequence is embedded in a navigation view controller. The problem I have is that whenever I close the player the entire stack of view controllers is popped from the list of view controllers in the navigation controller which means that I am sent back to my home screen (however I only want to pop the AVPlayerViewController from the list and return to the screen before). I tried to find a way to override the close Button but did not find a way. Moreover I tried to push a notification and handle it in the home screen by reinitializing the entire stack of view controllers - this solution works but does not seem like the appropriate solution. I attached the class that inherits from AVPlayerViewController and the code that implements the viewController. Thankful for any hint.
import UIKit
import AVKit
import AVFoundation
// MARK: - EduMediaVideoViewController
class EduMediaVideoViewController: AVPlayerViewController, EduMediaViewController {
// MARK: Stored Type Properties
weak var eduMediaDelegate: EduMediaElementDelegate?
var videoMedia: VideoMediaElement?
// MARK: Lifecycle Methods
override func viewDidLoad() {
super.viewDidLoad()
setUpVideo()
}
// MARK: Instance Methods
func setContent(content: MediaElement) {
guard let videoMedia = content as? VideoMediaElement else {
return
}
self.videoMedia = videoMedia
}
// MARK: Private Instance Methods
private func setUpVideo() {
let playerItem = videoMedia?.video
let player = AVPlayer(playerItem: playerItem)
self.player = player
player.play()
}
private func updateVideoProgress() {
self.eduMediaDelegate?.updateProgress(id: 1, progress: 1)
}
}
// create an extension of AVPlayerViewController
extension EduMediaVideoViewController {
// override 'viewWillDisappear'
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
guard let category = eduMediaDelegate?.getCategory() else {
return
}
let dict = ["category": category]
NotificationCenter.default.post(name: .kAVDismissNotification, object: nil, userInfo: dict)
}
}
extension Notification.Name {
static let kAVDismissNotification = Notification.Name.init("dismissing")
}
The code that initializes the video controller:
let eduVideoViewController = EduMediaVideoViewController()
eduVideoViewController.setContent(content: mediaElement)
eduVideoViewController.eduMediaDelegate = self
navigationController?.pushViewController(eduVideoViewController, animated: false)

Related

Call Controller of an autoplayed video in ViewController

I have set it up so that when a ViewController is displayed, a video starts automatically and at the end it switches to a different ViewController.
The problem is that if the app is put in the background while viewing it, the video freezes and you have to restart the application.
I thought about setting the classic pause / play controllers to appear when you press the screen so you can continue watching, but I don't know how to do that.
Or do you have another solution to prevent the video from freezing?
import UIKit
import AVKit
import AVFoundation
class View8BaController: UIViewController {
func setupAVPlayer() {
let videoURL = Bundle.main.url(forResource: "8B-A", withExtension: "mp4") // Get video url
let avAssets = AVAsset(url: videoURL!) // Create assets to get duration of video.
let avPlayer = AVPlayer(url: videoURL!) // Create avPlayer instance
let avPlayerLayer = AVPlayerLayer(player: avPlayer) // Create avPlayerLayer instance
avPlayerLayer.frame = self.view.bounds // Set bounds of avPlayerLayer
self.view.layer.addSublayer(avPlayerLayer) // Add avPlayerLayer to view's layer.
avPlayer.play() // Play video
// Add observer for every second to check video completed or not,
// If video play is completed then redirect to desire view controller.
avPlayer.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: 1) , queue: .main) { [weak self] time in
if time == avAssets.duration {
let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "SCENA7") as! SCENA7ViewController
self?.navigationController?.pushViewController(vc, animated: false)
}
}
}
//------------------------------------------------------------------------------
override func viewDidLoad() {
super.viewDidLoad()
}
//------------------------------------------------------------------------------
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.setupAVPlayer() // Call method to setup AVPlayer & AVPlayerLayer to play video
}
}
have you tried telling the video to play in the sceneDelegate?
add to the class, right above func setupAVPlayer(),
var avPlayer: AvPlayer!
then in your sceneDelegate outside any functions
let view8Ba = View8BaController()
to create an instance of the view controller. Then you can access the properties in the following:
func sceneWillEnterForeground(_ scene: UIScene) {
if view8Ba.viewIfLoaded?.window != nil {
view8Ba.avPlayer.play()
}
}
This will tell your video to start playing again when the app comes back from the background.
If you want to add a play/pause when you tap the screen you can add a tap gesture recognizer and another view to the current view controller and set the background to clear (in storyboard drag the new view to the white bar on top of the view controller)
then call
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
pauseScreenView.frame = View8BaController.bounds
}
This tells the new view you added where to be on the viewController.
In the IBAction of the tap gesture recognizer
#IBAction func screenTapped(_ sender: Any) {
View8BaController.addSubview(pauseScreenView)
DispatchQueue.main.asyncAfter(deadline: .now() + 10.0) {
self.pauseScreenView.removeFromSuperview()
}
}
This adds the new view to the top of the viewController and then removes it after 10 seconds.
In that new view you can add a button that will play/pause your video
#IBAction func pauseVideo(_ sender: UIButton) {
if avPlayer.timeControlStatus == .playing {
avPlayer.pause()
pauseButton.setImage(playImage, for: .normal)
}else {
avPlayer.play()
pauseButton.setImage(pauseImage, for: .normal)
}
}

What is the best way of updating a variable in a view controller from scene delegate?

I am using Spotify SDK. I want to change labels in some view controllers when a user changes his/her player state. Here is my scene delegate:
var playerViewController = MatchViewController()
func playerStateDidChange(_ playerState: SPTAppRemotePlayerState) {
playerViewController.stateChanged(playerState)
}
A view controller:
func stateChanged(_ playerState: SPTAppRemotePlayerState) {
// aLabel.text = playerState.track.name
}
The problem is labels or other outlets are nil when the state is changed because the view controllers are not loaded at that time. How can I fix that? (I tried isViewLoaded)
If you have a more than a few places to update according to a change that occurs at one place use observers. Here's how,
Post notification in SceneDelegate like this:
func playerStateDidChange(_ playerState: SPTAppRemotePlayerState) {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "stateChanged"), object: nil, userInfo: ["playerState": playerState])
}
Observe in ViewControllers like this:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(stateChanged), name: NSNotification.Name("stateChanged"), object: nil)
}
#objc func stateChanged(_ notification: Notification) {
if let playerState = notification.userInfo?["playerState"] as? SPTAppRemotePlayerState {
print(playerState)
}
}
}

How to continuing playing music when user move through the app. [Not in background mode]

I'm making a musicPlayer app in Swift, which is based on Tab Bar Controller and now is creating the NowPlayingViewController which present the playing scene as below screenshot showed. This NowPlayingViewController will be opened when I click the table row from another tableViewController, and the song was played.
My question is that when I dismiss this View Controller to go back the previous view or move to other view of Tab Bar Controller, the music playing is stoped. After some googles, someone suggests to use singleton to make a shared session for music playing.
/// here is my project's storyboard
/// I change my code to add one Audio Manager.swift as the singleton class. However I'm not sure if I'm doing the right thing, need help. Also don't find method about getting playingSong variable from NowPlayingVewController...
import AVKit
class AudioManager {
// use Singleton pattern keep the music continuing when user move through the App.
static let sharedInstance = AudioManager()
var audioPlayer: AVAudioPlayer!
var playingSong: SongData?
private init() {
// config for audio background playing
do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.mixWithOthers])
print("Playback OK")
try AVAudioSession.sharedInstance().setActive(true)
print("Session is Active")
} catch {
print(error)
}
}
func startMusic() {
do {
// how to retrieve the playingSong variable from NowPlayingViewController?
// playingSong =
try audioPlayer = AVAudioPlayer(contentsOf: playingSong!.url)
audioPlayer.prepareToPlay()
audioPlayer.play()
} catch {
print("could not load file")
}
}
func pauseMusic() {
audioPlayer.pause()
}
}
/// NowPlayViewController.swift, in case it is not fully aligned with above picture because it is just an example image to show the playing scene.
import UIKit
import AVKit
class NowPlayingViewController: UIViewController {
var playingSong: SongData?
var audioPlayer: AVAudioPlayer!
var isPlaying = true
var timer:Timer!
#IBOutlet weak var songTitle: UILabel!
#IBOutlet weak var songAlbum: UILabel!
#IBOutlet weak var songArtwork: UIImageView!
#IBOutlet weak var playOrPauseButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
songTitle.text = playingSong?.songName
songAlbum.text = playingSong?.albumName
songArtwork.image = playingSong?.albumArtwork
// start play music in AudioManager sharedInstance
AudioManager.sharedInstance.startMusic()
}
#IBAction func NaviBack(_ sender: UIButton) {
print("press button")
// not sure about this, use present modally of segue,
// so need to use dismiss to return to the previous scene/view.
self.dismiss(animated: true, completion: nil)
}
#IBAction func PlayOrPauseMusic(_ sender: UIButton) {
if isPlaying {
print("isPlaying")
AudioManager.sharedInstance.pauseMusic()
isPlaying = false
playOrPauseButton.setTitle("Pause", for: .normal)
} else {
print("isPaused")
AudioManager.sharedInstance.startMusic()
isPlaying = true
playOrPauseButton.setTitle("Play", for: .normal)
}
}
}
/// Sending data to NowPlayingViewController from tableViewController using Segue present modally.
// define segue to the transition to NowPlaying View
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "playingSong" {
if let indexPath = self.tableView.indexPathForSelectedRow {
let controller = segue.destination as! NowPlayingViewController
if resultSearchController.isActive {
controller.playingSong = filteredTableData[indexPath.row]
} else {
controller.playingSong = tableData[indexPath.row]
}
}
}
}
Oops, I found my AudioManger is actually worked by getting the playingSong: SongData from tableViewController when user click the cell row in this view. I declare this playingSong as static var in order to access/use it from AudioManager.
Now music is playing when user navigation through the Tab Bar Controller, update my code as below. Next step could be add a button on the left/right top of navi view to re-present the playing scene:)
Err, I found that it still has issue, the play and resume for the current song is working, but if I select another song in the tableViewController, then it still plays the previous song in NowPlayingView. The reason could be the init() only once, so I need to find a way to re-assign value to sharedInstance when select another cell row.
import AVKit
class AudioManager {
// use Singleton pattern keep the music continuing when user move through the App.
static let sharedInstance = AudioManager()
var audioPlayer: AVAudioPlayer!
var playingSong: SongData?
private init() {
// config for audio background playing
do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.mixWithOthers])
print("Playback OK")
try AVAudioSession.sharedInstance().setActive(true)
print("Session is Active")
} catch {
print(error)
}
do {
playingSong = SongsTableViewController.playingSong
try audioPlayer = AVAudioPlayer(contentsOf: playingSong!.url)
audioPlayer.prepareToPlay()
} catch {
print("could not load file")
}
}
func playMusic() {
audioPlayer.play()
}
func pauseMusic() {
audioPlayer.pause()
}
}

Removing Data on Maps with different view controllers

I am really struggling on an issue that I think is rather interesting and quite difficult. My application lets the user create annotation locations within a Mapview. They also have the option to edit and delete these locations in another modal view controller.
The issue I am facing is that when the user presses delete, which removes the location from firebase, the annotation is still displayed upon the map. I cannot reload my annotation data within the view did appear as this does not suit my application. I cant have my annotations being reloaded every time I bring up the Mapview.
I need to figure out a way to implement an annotation reload when the delete button is pressed. However, as this happens within my delete view controller (which does not contain the mapView) I cannot use the reload function. Is there a way to connect view controllers so that I can apply the reload function when delete is pressed?
Updated Code **
This is my map view controller:
class ViewController: UIViewController, SideBarDelegate, MGLMapViewDelegate, DeleteVCDelegate {
let EditSaveSpotController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "EditVC") as! EditSaveSpotViewController
override func viewDidLoad() {
super.viewDidLoad()
EditSaveSpotController.delegate = self
}
func wholeRefresh() {
let uid = FIRAuth.auth()!.currentUser!.uid
let userLocationsRef = FIRDatabase.database().reference(withPath: "users/\(uid)/personalLocations")
userLocationsRef.observe(.value, with: { snapshot in
for item in snapshot.children {
guard let snapshot = item as? FIRDataSnapshot else { continue }
let newSkatepark = Skatepark(snapshot: snapshot)
self.skateparks.append(newSkatepark)
self.addAnnotation(park: newSkatepark)
}
})
if let annotations = mapView.annotations {
mapView.removeAnnotations(annotations)
}
for item in skateparks {
self.addAnnotation(park: item)
}
}
This is my delete view controller:
import UIKit
import Firebase
protocol DeleteVCDelegate {
func wholeRefresh()
}
class EditSaveSpotViewController: UIViewController {
var delegate: DeleteVCDelegate?
#IBAction func deleteSkateSpot(_ sender: Any) {
ref = FIRDatabase.database().reference(withPath: "users").child(Api.User.CURRENT_USER!.uid).child("personalLocations/\(parkId!)")
ref.observe(.value, with: { (snapshot) in
self.ref.setValue(nil)
self.dismiss(animated: true, completion: nil)
self.delegate?.wholeRefresh()
// self.delegate?.mainRefresh()
print("CheckWorking")
})
}
}
This is very high level and I did not have a chance to verify but it should be enough to get you going:
Modal Delete View
protocol DeleteVCDelegate {
func mainRefresh()
}
class DeleteVC: UIViewController {
var delegate: DeleteVCDelegate?
//your delete code
#IBAction func deleteSkateSpot(_ sender: Any) {
ref = FIRDatabase.database().reference(withPath: "users").child(Api.User.CURRENT_USER!.uid).child("personalLocations/\(parkId!)")
ref.observe(.value, with: { (snapshot) in
self.ref.setValue(nil)
//call to delegate
self.delegate?.mainRefresh()
})
}
}
MapView Class (implement DeleteVCDelegate)
class mapVC: MKMapViewDelegate, DeleteVCDelegate{
//when you present your DeleteVC set its delegate to the map view
let vc=(self.storyboard?.instantiateViewController(withIdentifier: "deleteVC"))! as! DeleteVC
//set the delegate
vc.delegate=self
//present deleteVC
self.present(vc, animated: true, completion:nil)
//implement delegate method of DeleteVC
func mainRefresh(){
//dismiss modal
self.dismiss(animated: true) {
//update view
self.loadLocations()
self.annotationRefresh()
}
}
}

Load ViewController when Push Notification is received

when a user receives a push notification and taps the notification, he/she will be brought into my app, where I want a certain view controller to appear. Therefore I use the notification center.
My question is, where do I need to perform the loading of the view controller so it will be shown and pushed on the navigation stack when the user enters the app?
func processReceivedRemoteNotification(userInfo:[NSObject:AnyObject]) {
let notification = userInfo as! Dictionary<String, AnyObject>
let json = JSON(notification)
// Get information from payload
let dispatchType:String = json["dispatch"]["dispatchType"].stringValue
switch dispatchType {
case "alert":
self.notificationCenter.postNotificationName("ALERT_RECEIVED", object: nil, userInfo: userInfo as [NSObject:AnyObject])
break
default:
break
}
}
View Controller to be loaded
class AlertViewController: UIViewController {
let notificationCenter: NSNotificationCenter = NSNotificationCenter.defaultCenter()
override func viewWillAppear(animated: Bool) {
self.notificationCenter.addObserver(self, selector: "alertMessageReceived:", name: "ALERT_RECEIVED", object: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func alertMessageReceived(notification: NSNotification) {
let userInfo = notification.userInfo as! Dictionary<String, AnyObject>
print(userInfo)
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc1: AlertViewController = storyboard.instantiateViewControllerWithIdentifier("example1") as! AlertViewController
self.navigationController?.pushViewController(vc1, animated: true)
}
I don't know your app architecture, but from the given context I can see that you have a navigationController. You should not add as observer AlertViewController in this case. Instead move this code to another view controller, which is already pushed to navigationController. Another option is to subclass UINavigationController and observe "ALERT_RECEIVED" notification in it.