I'm trying to stream two videos at the same time with swift on iphone. I already know that the AV Player can only stream one video at a time, but googling it I saw that it's still possible to stream different tracks at the same time. I also saw the picture in picture implementation. The real problem is that is all in objective-c and the code is quite old. I tried to understand it running the code as it is, but there are errors and some of the functions are deprecated.
Does someone know how to do that in swift? Also, I'm streaming video from the internet so merging them before playing is not an option.
Thank you!
Swift Version
The article you referenced is an interesting method of handling multiple video playback in iOS. The article appears to be related to Apple's Stitched Stream Player Sample Project. As an exercise in Swift 2.2, I've rewritten the code on the iOS Guy article.
You can find both the view and the view controller in Swift on my gist. I'm copying below as it is best practices to not use link only answers on SO.
Custom View Player
This custom view player allows one to swap out multiple AVPlayerItem(s).
import UIKit
import AVFoundation
class MNGVideoPlayerView: UIView {
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect) {
// Drawing code
}
*/
override class func layerClass () -> AnyClass {
return AVPlayerLayer.self
}
func player () -> AVPlayer {
return (self.layer as! AVPlayerLayer).player!
}
func setPlayer(player:AVPlayer) {
(self.layer as! AVPlayerLayer).player = player
}
func setVideoFillMode(fillMode:String) {
let playerLayer = (self.layer as! AVPlayerLayer)
playerLayer.videoGravity = fillMode
}
}
Multiple Video Player View Controller
This controller manages the distribution and presentation of the different AVPlayerItem(s). Check my gist repos for additional updates. (ported from original objc source #iOS Guy)
import UIKit
import AVFoundation
class MNGVideoPlayerViewController: UIViewController {
let kTracksKey = "tracks";
let kStatusKey = "status";
let kRateKey = "rate";
let kPlayableKey = "playable";
let kCurrentItemKey = "currentItem";
let kTimedMetadataKey = "currentItem.timedMetadata";
var _URL:NSURL? = nil
var player:AVPlayer? = nil
var playerItem:AVPlayerItem? = nil
var playerView:MNGVideoPlayerView? = nil
var AVPlayerDemoPlaybackViewControllerStatusObservationContext = UnsafeMutablePointer<Void>()
var AVPlayerDemoPlaybackViewControllerCurrentItemObservationContext = UnsafeMutablePointer<Void>()
var AVPlayerDemoPlaybackViewControllerStatusObservation = UnsafeMutablePointer<Void>()
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.
}
// MARK: - Public methods
func setURL(url:NSURL) {
self._URL = url
let asset = AVURLAsset(URL: self._URL!)
let requestedKeys = [kTracksKey,kPlayableKey]
asset.loadValuesAsynchronouslyForKeys(requestedKeys) { () -> Void in
self.prepareToPlayAsset(asset, withKeys: requestedKeys)
}
}
func prepareToPlayAsset(asset:AVURLAsset, withKeys requestedKeys:NSArray) {
var error:NSErrorPointer = nil
for thisKey in requestedKeys {
let keyStatus = asset.statusOfValueForKey(thisKey as! String, error: error)
if keyStatus == .Failed {
return
}
}
if !asset.playable {
return
}
if (self.playerItem != nil) {
self.playerItem?.removeObserver(self, forKeyPath: kStatusKey)
NSNotificationCenter.defaultCenter().removeObserver(self, name: AVPlayerItemDidPlayToEndTimeNotification, object: self.playerItem)
}
self.playerItem = AVPlayerItem(asset: asset)
self.playerItem?.addObserver(self, forKeyPath: kStatusKey, options: [.Initial,.New], context: AVPlayerDemoPlaybackViewControllerStatusObservationContext)
if (self.player == nil) {
self.player = AVPlayer(playerItem: self.playerItem!)
self.player?.addObserver(self, forKeyPath: kCurrentItemKey, options: [.Initial,.New], context: AVPlayerDemoPlaybackViewControllerCurrentItemObservationContext)
}
if self.player?.currentItem! != self.playerItem! {
self.player?.replaceCurrentItemWithPlayerItem(self.playerItem!)
}
}
// MARK: - Key Value Observing
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if context == AVPlayerDemoPlaybackViewControllerStatusObservation {
let status:Int = (change![NSKeyValueChangeNewKey]?.integerValue)!
if status == AVPlayerStatus.ReadyToPlay.rawValue {
self.player?.play()
}
} else if context == AVPlayerDemoPlaybackViewControllerCurrentItemObservationContext {
let newPlayerItem = change![NSKeyValueChangeNewKey] as? AVPlayerItem
if newPlayerItem != nil {
self.playerView?.setPlayer(self.player!)
self.playerView?.setVideoFillMode(AVLayerVideoGravityResizeAspect)
}
} else {
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
}
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
}
It looks like http://iosguy.com/2012/01/11/multiple-video-playback-on-ios/ is the best solution at the moment. Nobody tried to convert it in swift yet.
I play multiple videos in my app at once, I just use multiple instances of AVPlayer.
Related
I am building an app with several view controllers. I need to control music from all of them so I have created a dedicated music class which I use to setup / play / stop / pause.
I have recorded audio questions and answers and I need to be able to play the question and then the answer mp3 files.
So I believe that here a few way to accomplish this delegate and protocols, using the func audioPlayerDidFinishPlaying and using closures. From what I can understand closures are the best option for what I am trying to achieve.
My starting point in the MakeMusic Class is:
class MakeMusicClass : NSObject, AVAudioPlayerDelegate {
static let shared = MakeMusicClass()
var audioPlayer = AVAudioPlayer()
override init() { }
func setup(Selection: String) {
do {
audioPlayer = try AVAudioPlayer(contentsOf: URL.init(fileURLWithPath: Bundle.main.path(forResource: Selection, ofType: "mp3")!))
audioPlayer.prepareToPlay()
audioPlayer.delegate=self
} catch {
print (error)
}
}
func play() {
audioPlayer.play()
}
My calling file is:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
MakeMusicClass.shared.setup(Selection: "Question11")
MakeMusicClass.shared.play()
MakeMusicClass.shared.setup(Selection: "Answer11")
MakeMusicClass.shared.play()
To get this working I understand that I need to add a closure to the called class:
func play() {
var closure = { in
audioPlayer.play()
}
}
And I need to update where I need to call the function something like:
override func viewDidLoad() {
super.viewDidLoad()
MakeMusicClass.shared.setup(Selection: "Question11")
MakeMusicClass.shared.play() {
MakeMusicClass.shared.setup(Selection: "Answer11")
MakeMusicClass.shared.play()
}
I have spent ages trying to get my head around this, but I am struggling. My code here is clearly not working as there is something fundamental that I am missing. I have tried passing void and parameters, but I don't understand what parameters should be passed. The closest I have come is using the audioPlayerDidFinishPlaying within the makemusic class trigger the next audio file, but I don't know that this is ideal.
I think your best bet will be in your MakeMusicClass taking an Array as an initialiser e.g. a question and and answer, and then use AVAudioPlayerDelegate to trigger the next file, which I think is what you are trying to get at (and very close ^________*).
For example:
class AudioLooper: NSObject, AVAudioPlayerDelegate {
var debugView = true
var audioLoopPlayer: AVAudioPlayer!
var audioFileIndex: Int = 0
var audioFileArray: [String] = []
//-------------------------------------------------
//MARK: Audio Player Initialisation & Functionality
//-------------------------------------------------
/// Function To Initialise The Audio Loop Player
///
/// - Parameter audioFiles: [String] - The Array Of Audio Files To Play
func initAudioPlayerWith(audioFiles: [String]) {
audioFileArray = audioFiles
if debugView { print("Audio Files To Play In Sequence == \(audioFileArray)") }
}
/// Function To Play An Array Of Audio Files
///
/// - Parameter index: (Int) - The Current Index Of The Audio Sequence
func playAudioLoopAt(index: Int) {
let currentAudioFile = "\(audioFileArray[audioFileIndex])"
if let audioFilePath = Bundle.main.path(forResource: currentAudioFile, ofType: "mp3"){
do {
let audioFileUrl = NSURL.fileURL(withPath: audioFilePath)
// Set An Instance Of AVAudioSession Which Allows Playback Even When Device Is Muted
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
audioLoopPlayer = try AVAudioPlayer(contentsOf: audioFileUrl)
audioLoopPlayer.prepareToPlay()
audioLoopPlayer.delegate = self
audioLoopPlayer.play()
if debugView { print("Playing \(currentAudioFile) ") }
audioFileIndex += 1
} catch {
print("Error Playing Audio")
}
} else {
print("Error Finding File: \(currentAudioFile)")
}
}
/// Function To Continue The Audio Sequence
#objc func continueAudioLoop(){
playAudioLoopAt(index: audioFileIndex)
}
/// Function To Stop The Audio Looop Player
func stopAudioPlayer() {
if audioLoopPlayer != nil {
audioFileIndex=0
audioLoopPlayer.stop()
audioLoopPlayer = nil
}
}
//-----------------------------
//MARK: AVAudio Player Delegate
//-----------------------------
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
if audioFileIndex < audioFileArray.count {
self.perform(#selector(continueAudioLoop), with: self, afterDelay: 0.5)
}else{
audioFileIndex=0
}
}
}
In my example you simply init like so:
audioLooper = AudioLooper()
audioLooper?.initAudioPlayerWith(audioFiles: ["question", "answer"])
audioLooper?.playAudioLoopAt(index: 0)
Where audioLooper is declared like so : var audioLooper:AudioLooper?
Clearly my example is not a Singleton, but it should give you an idea of how you can adjust your MakeMusicClass to make it fit...
You can also add a Delegate Method like so to inform the ViewController the audio has finished or perform some other task like update the next question etc e.g:
#objc protocol AudioLooperDelegate {
#objc optional func update()
}
Then in your ViewController:
var delegate: AudioLooperDelegate?
And in the AudioLooper Class you can then add the delegate method where needed e.g:
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
if audioFileIndex < audioFileArray.count {
self.perform(#selector(continueAudioLoop), with: self, afterDelay: 0.5)
}else{
audioFileIndex=0
delegate?.updateUI!()
}
}
}
In a swift game using UIKit I am writing, a human player will interact with UIKit UIButtons, GUI elements to take actions.
In the game, the player will play against AI players.
But here's the thing; the human player presses buttons and interacts and the AI player does not.
Given a simple UIViewController;
class SampleViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func buyBtnPressed(_ sender: UIButton) {
print ("pressed")
}
}
So what I'm trying to ascertain is, how does the AI player itself take actions and handling turns within the context of the current view controller?
I believe the best way to do this is that there should be a loop that will wait until all players have completed their respective turns.
But where does this loop go? In the view did load?
If so, won't it eat up memory, or potentially lead (if not careful) to an endless loop?
I'm finding it hard to ascertain how an AI player can take actions within the given context of a UIViewController considering GUI elements are for human interaction.
I don't mean the AI should be animating pressing buttons or interacting with the screen, I mean; I have a UIViewController, it has a view did load; what is the strategy of implementing AI turns and whether or not this should be be achieved in a "game loop" in the View did load or can this be achieved in another way?
My question is; given the context of a UIViewController; how can I code the handling of an AI player taking turns and can this be achieved with a loop or another strategy?
Many thanks
edit: Code is now added
I have written out a turn base manager using Swift playgrounds, and 2 examples one using a UIViewController and another is a loop.
code now follows;
import Foundation
import GameplayKit
class Player {
var name: String
public private(set) var isAI: Bool = false
public private(set) var turnOrder: Int = 0
init(name: String, isAI: Bool?) {
self.name = name
if let hasAI = isAI {
self.isAI = hasAI
}
}
func setTurnOrderIndex(number: Int) {
self.turnOrder = number
}
}
let p1 = Player.init(name: "Bob", isAI: false)
let p2 = Player.init(name: "Alex", isAI: true)
protocol TurnOrderManagerDelegate: NSObjectProtocol {
func turnOrderWasSet()
}
protocol TurnDelegate: class {
func turnIsCompleted()
}
class Turn: NSObject {
weak var player: Player?
weak var delegate: TurnDelegate?
public private(set) var completed: Bool = false {
didSet {
delegate?.turnIsCompleted()
}
}
init(player:Player, delegate: TurnDelegate) {
self.player = player
self.delegate = delegate
}
func setAsComplete() {
self.completed = true
}
}
class TurnOrderManager: NSObject, TurnOrderManagerDelegate, TurnDelegate {
static var instance = TurnOrderManager()
public private(set) var turnOrderIndex: Int = 0
public private(set) var turnOrder: [Turn] = [Turn]() {
didSet {
self.turnOrderWasSet()
}
}
var playerOnTurn: Player? {
let turnObj = self.turnOrder[turnOrderIndex]
return (turnObj.player)
}
var allTurnsCompleted: Bool {
let filtered = turnOrder.filter { (turnObj:Turn) -> Bool in
return (turnObj.completed)
}.count
return (filtered == turnOrder.count)
}
func setTurnOrder(players:[Player]) {
if (self.turnOrder.count == 0) {
for playerObj in players {
let turnObj = Turn.init(player: playerObj, delegate: self)
self.turnOrder.append(turnObj)
}
}
}
func turnOrderWasSet() {
for (index, turnObj) in self.turnOrder.enumerated() {
turnObj.player?.setTurnOrderIndex(number: index)
}
}
func next() {
if (turnOrderIndex < (self.turnOrder.count - 1)) {
turnOrderIndex += 1
}
else {
turnOrderIndex = 0
}
}
internal func turnIsCompleted() {
print (" - turnIsCompleted")
TurnOrderManager.instance.next()
}
}
class GameModel {
var turnOrderManager: TurnOrderManager
init() {
self.turnOrderManager = TurnOrderManager.instance
self.turnOrderManager.setTurnOrder(players:[p1,p2])
}
// other game model stuff [...]
}
class Phase1State : GKState {
var gameModel: GameModel!
init(gameModel:GameModel) {
super.init()
self.gameModel = gameModel
}
override func isValidNextState(_ stateClass: AnyClass) -> Bool
{
return false
}
override func didEnter(from previousState: GKState?) {
}
override func willExit(to nextState: GKState) {
}
// MARK: - Action
func buy() {
let index = self.gameModel.turnOrderManager.turnOrderIndex
let turn = self.gameModel.turnOrderManager.turnOrder[index]
turn.setAsComplete()
}
}
class SomeViewController: UIViewController
{
var gameModel: GameModel?
weak var gamePhase: Phase1State?
var isPhaseComplete: Bool {
return self.gameModel?.turnOrderManager.allTurnsCompleted ?? false
}
override func viewDidLoad() {
super.viewDidLoad()
self.gameModel = GameModel.init()
self.gamePhase = Phase1State.init(gameModel: self.gameModel!)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func buyButtonPressed() {
self.gamePhase?.buy()
self.finishTurn()
}
func finishTurn() {
guard let turnIndex = self.gameModel?.turnOrderManager.turnOrderIndex else {
return
}
guard let turn = self.gameModel?.turnOrderManager.turnOrder[turnIndex] else {
return
}
turn.setAsComplete()
if (self.isPhaseComplete)
{
print ("All turns are completed")
}
else {
//self.gameModel?.turnOrderManager.next()
self.gamePhase?.buy()
guard let playerOnTurn = self.gameModel?.turnOrderManager.playerOnTurn else {
print ("No player is on turn")
return
}
print ("\(playerOnTurn.name) is on turn")
if (playerOnTurn.isAI)
{
self.gamePhase?.buy()
self.finishTurn()
}
}
}
}
// EXAMPLE 1 -- first attempt ...
let vc = SomeViewController()
vc.viewDidLoad()
vc.buyButtonPressed()
// EXAMPLE 2 -- another attempt ....
let gameModel: GameModel = GameModel.init()
let gamePhase = Phase1State.init(gameModel: gameModel)
// player then takes an action
while (gameModel.turnOrderManager.allTurnsCompleted == false)
{
let turnIndex = gameModel.turnOrderManager.turnOrderIndex
let turnObj = gameModel.turnOrderManager.turnOrder[turnIndex]
guard let playerOnTurn = turnObj.player else {
break
}
print ("Player \(playerOnTurn.name) is on turn")
gamePhase.buy()
}
print ("All turns are completed, advance to next phase")
The issue is;
On the finishTurn, it only seems to work if it relies on the first player in the index is a human player. If its not, I have no idea how to make it fire the buy action.
On the second example, I use a loop; but I'm concerned using a loop could end up just looping forever.
My query is therefore clarifyed, how can I ensure my view controller will fire actions for AI players when they don't press buttons and loop through each player and execute their respective turn.
Many thanks
Further edit:
I do not know if I should have the while (gameModel.turnOrderManager.allTurnsCompleted == false) loop inside my viewDidLoad() to act like a game loop.
There is no need to specifically use Sprite Kit for this. SpriteKit would be more to do with how the UI is made rather than how the logic of the game works.
However, I would recommend looking at GameplayKit. It's a framework that contains lots of built in game logic tools. Specifically you want something like the GKDecisionTree. There are a few WWDC videos about it too. GameplayKit can be used with SpriteKit, UIKit, SSceneKit or any other game engine that you decide to use (or not).
Also, the question you're asking is a very general question about game development. Having the computer "decide" to do something is quite a complex subject.
I'd also suggest having a quick watch of this video from AI & Games and other videos from that channel.
It'll give you an idea of how to approach your problem.
Session 609 and 608 from WWDC 2015 and 2016 are prob good :D
Regarding updating the AI.
Your AI should be event driven. You have the concept of "turns" and "players". There is a point in the game at which it becomes a "player's" "turn". (Even at the very beginning of the game it is either Player 1 or Player 2's turn.
At this time there are two possibilities. Either the player is an AI, or the player is a person.
As soon as this happens there should be some sort of trigger (like a function call or something) that tells the player its turn has started.
If that player is the AI then you need to start some sort of calculation (maybe with a built in delay to make it realistic) so that it decides what to do.
Look, I'm not sure on what kind of game you're making, buy you should probably learn SpriteKit, specially SKActions. With that, you can easily control the flow of events from your game.
With that said, how is your AI implementation? Based on your code, I would begin with something like this:
class AI {
enum Decision {
case doSomething
case doAnotherThing
case dontDoAnything
}
public func decide() -> Decision {
// Decide which action the AI will take...
return .doSomething // This return is just a example!
}
public func act(on : Decision) {
// Do whatever the AI needs based on a decision...
}
}
Then, in your ViewController:
class SampleViewController: UIViewController {
var ai = AI()
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func buyBtnPressed(_ sender: UIButton) {
print ("pressed")
ai.act(on: ai.decide())
}
}
I hope that helps!
I have developed a game in Xcode using sprite kit, and scenes. Now I am trying to integrate the functionality to post high scores to twitter and Facebook. I've looked around, and most people say to use SLComposeServiceViewController which is fine, until I try and present it. Because my app really only uses scenes, they never have the member function "presentViewController(....)". Thus, I am unable to ever present it. Anyone know any way around this?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch:UITouch = touches.first!
let touchLocation = touch.location(in: self)
let touchedNode = self.atPoint(touchLocation)
if (touchedNode.name == "tryAgain") {
let nextScene = Scene_LiveGame(size: self.scene!.size)
nextScene.scaleMode = self.scaleMode
self.view?.presentScene(nextScene, transition: SKTransition.fade(withDuration: 0.5))
}
else if (touchedNode.name == "share") {
if SLComposeViewController.isAvailable(forServiceType: SLServiceTypeFacebook) {
let fShare = SLComposeViewController(forServiceType: SLServiceTypeFacebook)
self.presentViewController(fShare!, animated: true, completion: nil)
//^This is where my problem is. Xcode is telling me that self has no member function presentViewController which I totally understand, because its a scene and thus doesn't share those functions. But every resource online has shown me this is the only way to do it
}
}
Your are getting this error because you need to present a UIViewController from another UIViewController. So
self.presentViewController(...)
will not work because self (SKScene) is not a UIViewController. To present from a SKScene you would have to say this
view?.window?.rootViewController?.presentViewController(fShare!, animated: true, completion: nil)
I would recommend that you do not use those APIs anymore. Its better to use a UIActivityViewController for your sharing needs. This way you only need one share button in your app and you can share to all sorts of services (email, Twitter, Facebook, iMessage, WhatsApp etc).
Create a new Swift file and add this code.
enum ShareMenu {
static func open(text: String, image: UIImage?, appStoreURL: String?, from viewController: UIViewController?) {
guard let viewController = viewController, let view = viewController.view else { return }
// Activity items
var activityItems = [Any]()
// Text
activityItems.append(text)
// Image
if let image = image {
activityItems.append(image)
}
/// App url
if let appStoreURL = appStoreURL {
let items = ActivityControllerItems(appStoreURL: appStoreURL)
activityItems.append(items)
}
// Activity controller
let activityController = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
// iPad settings
if UIDevice.current.userInterfaceIdiom == .pad {
activityController.modalPresentationStyle = .popover
activityController.popoverPresentationController?.sourceView = view
activityController.popoverPresentationController?.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 0, height: 0)
activityController.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection.init(rawValue: 0)
}
// Excluded activity types
activityController.excludedActivityTypes = [
.airDrop,
.print,
.assignToContact,
.addToReadingList,
]
// Present
DispatchQueue.main.async {
viewController.present(activityController, animated: true)
}
// Completion handler
activityController.completionWithItemsHandler = { (activity, success, items, error) in
guard success else {
if let error = error {
print(error.localizedDescription)
}
return
}
// do something if needed
}
}
}
// MARK: - Activity Controller Items
/**
ActivityControllerItems
*/
private final class ActivityControllerItems: NSObject {
// MARK: - Properties
/// App name
fileprivate let appName = Bundle.main.infoDictionary?["CFBundleName"] as? String ?? "-"
/// App store web url
fileprivate let appStoreURL: String
// MARK: - Init
/// Init
fileprivate init(appStoreURL: String) {
self.appStoreURL = appStoreURL
super.init()
}
}
// MARK: - UIActivityItemSource
/// UIActivityItemSource
extension ActivityControllerItems: UIActivityItemSource {
/// Getting data items
/// Placeholder item
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
return ""
}
/// Item for actity type
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivityType) -> Any? {
return URL(string: appStoreURL) ?? appName
}
/// Provide info about data items
/// Subject field for services such as email
func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivityType?) -> String {
return appName
}
}
Than when the share button is pressed you can call it like so
ShareMenu.open(
text: "Can you beat my score?",
image: UIImage(...), // set to nil if unused
appStoreURL: "your iTunes app store URL", // set to nil if unused
from: view?.window?.rootViewController
)
Bear in mind that the image and appStoreURL will not show up everywhere, it depends on the sharing service.
You can also use your score value from your scene and add it to the text e.g
ShareMenu.open(
text: "Can you beat my score \(self.score)?",
...
)
Hope this helps
I will not go into SLComposeViewController related code. I will just show you two techniques aside from what crashoverride777 proposed. So the first technique would be using notifications, like this:
GameScene:
import SpriteKit
let kNotificationName = "myNotificationName"
class GameScene: SKScene {
private func postNotification(named name:String){
NotificationCenter.default.post(
Notification(name: Notification.Name(rawValue: name),
object: self,
userInfo: ["key":"value"]))
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.postNotification(named: kNotificationName)
}
}
Here, you post a notification by tapping the screen. A desired view controller class can listen for this notification, like this:
import UIKit
import SpriteKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(
self,
selector: #selector(self.handle(notification:)),
name: NSNotification.Name(rawValue: kNotificationName),
object: nil)
if let view = self.view as! SKView? {
// Load the SKScene from 'GameScene.sks'
if let scene = GameScene(fileNamed: "GameScene") {
// Set the scale mode to scale to fit the window
scene.scaleMode = .aspectFill
// Present the scene
view.presentScene(scene)
}
}
}
func handle(notification:Notification){
print("Notification : \(notification)")
}
}
Here, we add self as an observer for this notification - means that when notification happens, an appropriate handling method will be called (and that is our custom handle(notification:) method. In that method, you should call your code:
if SLComposeViewController.isAvailable(forServiceType: SLServiceTypeFacebook) {
let fShare = SLComposeViewController(forServiceType: SLServiceTypeFacebook)
self.presentViewController(fShare!, animated: true, completion: nil)
}
Actually, I will write another example for delegation, to keep things clean :)
As I said, this can be done using notifications, like in this answer, or you can go with delegation:
First you should declare the MyDelegate protocol which defines one method called myMethod().
protocol MyDelegate:class {
func myMethod()
}
That method is a requirement that every class must implement if it conforms to this protocl.
In our example, you can look at the scene as a worker, and a view controller as a boss. When the scene finishes its task, it notifies its boss (delegates responsibilities to him) about job completion, so that boss can decide what is next. I mean, I could say : "The scene is a boss, and it delegates responsibilities to his employee, the view controller..." But it doesn't really matter who you consider as a boss... The delegation pattern matters.
So, the view controller, should conform to this protocol, and it will implement the myMethod() (that will be called by the scene later):
class GameViewController: UIViewController, MyDelegate {
override func viewDidLoad() {
super.viewDidLoad()
//MARK: Conforming to MyDelegate protocol
if let view = self.view as! SKView? {
// Load the SKScene from 'GameScene.sks'
if let scene = GameScene(fileNamed: "GameScene") {
// Set the scale mode to scale to fit the window
scene.scaleMode = .aspectFill
scene.myDelegate = self
// Present the scene
view.presentScene(scene)
}
}
}
func myMethod(){
print("Do your stuff here")
}
}
And here is a code from the GameScene where you define the myDelegate property that we use to communicate with our view controller:
import SpriteKit
class GameScene: SKScene {
weak var myDelegate:MyDelegate?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.myDelegate?.myMethod()
}
}
To find out when to choose delegation over notifications and vice-versa take a look at this article (or just search SO, there are some nice posts about that).
I'm trying to improve a GitHub project I forked (https://github.com/giacmarangoni/Swift-Radio-Pro/tree/xcode8).
After some fixes and changes everything seems to work good but suddenly I noticed a really strange behavior.
When I open "NowPlayingViewController" for the first time and station starts to stream, everything is working and AVPlayer delegate updates user interface as expected (songLabel, titleLabel and albumArtwork).
After that, without stopping radio streaming, I tried to go back to "StationsViewController" and immediately to reopen "NowPlayingViewController" using "Now playing" button.
At this point delegation is still active, streaming is going on, but when song changes all variables in this view controller are updated but I can't say the same for the user interface. I tried to debug and I noticed that labels are populated but not updated. UI updates in the main thread and setNeedDisplay didn't help.
NowPlayingViewController
AVPlayer setup:
func setUpPlayer(){
radioPlayer = Player.radio
radioPlayer.rate = 1
NotificationCenter.default.addObserver(
self,
selector: #selector(self.playerItemDidReachEnd),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: self.radioPlayer.currentItem
)
}
Here you can find func onMetaData(_ metaData: [AVMetadataItem]?)).
//*****************************************************************
// MARK: - AVPlayerItem Delegate (for metadata)
//*****************************************************************
extension NowPlayingViewController: CustomAVPlayerItemDelegate {
func onMetaData(_ metaData: [AVMetadataItem]?) {
if let metaDatas = metaData{
startNowPlayingAnimation()
let firstMeta: AVMetadataItem = metaDatas.first!
let metaData = firstMeta.value as! String
var stringParts = [String]()
if metaData.range(of: " - ") != nil {
stringParts = metaData.components(separatedBy: " - ")
} else {
stringParts = metaData.components(separatedBy: "-")
}
// Set artist & songvariables
let currentSongName = track.title
track.artist = stringParts[0].decodeAllChars()
track.title = stringParts[0].decodeAllChars()
if stringParts.count > 1 {
track.title = stringParts[1].decodeAllChars()
}
if track.artist == "" && track.title == "" {
track.artist = currentStation.stationDesc
track.title = currentStation.stationName
}
DispatchQueue.main.async {
if currentSongName != self.track.title {
if kDebugLog {
print("METADATA artist: \(self.track.artist) | title: \(self.track.title)")
}
// Update Labels
self.artistLabel.text = self.track.artist
self.songLabel.text = self.track.title
self.updateUserActivityState(self.userActivity!)
// songLabel animation
self.songLabel.animation = "zoomIn"
self.songLabel.duration = 1.5
self.songLabel.damping = 1
self.songLabel.animate()
// Update Stations Screen
self.delegate?.songMetaDataDidUpdate(self.track)
// Query API for album art
self.resetAlbumArtwork()
self.queryAlbumArt()
}
}
}
}
}
This method is observed in "CustomAVPlayerItem" according to timedMetaData key path; It's fired every time AVPlayer metadatas change. This class is a subclass of AVPlayerItem:
import MediaPlayer
import Foundation
protocol CustomAVPlayerItemDelegate {
func onMetaData(_ metaData:[AVMetadataItem]?)
}
//*****************************************************************
// Makes sure that observers are removed before deallocation
//*****************************************************************
class CustomAVPlayerItem: AVPlayerItem {
var delegate : CustomAVPlayerItemDelegate?
init(url URL:URL)
{
if kDebugLog {print("CustomAVPlayerItem.init")}
super.init(asset: AVAsset(url: URL) , automaticallyLoadedAssetKeys:[])
addObserver(self, forKeyPath: "timedMetadata", options: NSKeyValueObservingOptions.new, context: nil)
}
deinit{
if kDebugLog {print("CustomAVPlayerItem.deinit")}
removeObserver(self, forKeyPath: "timedMetadata")
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if let avpItem: AVPlayerItem = object as? AVPlayerItem {
if keyPath == "timedMetadata" {
delegate?.onMetaData(avpItem.timedMetadata)
}
}
}
}
The following is my AVPlayer:
import MediaPlayer
//*****************************************************************
// This is a singleton struct using Swift
//*****************************************************************
struct Player {
static var radio = AVPlayer()
}
This is the segue function I use to open to "NowPlayingViewController". StationsViewController
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "NowPlaying" {
self.title = ""
firstTime = false
let nowPlayingVC = segue.destination as! NowPlayingViewController
nowPlayingVC.delegate = self
if let indexPath = (sender as? IndexPath) {
// User clicked on row, load/reset station
if searchController.isActive {
currentStation = searchedStations[indexPath.row]
} else {
currentStation = stations[indexPath.row]
}
nowPlayingVC.currentStation = currentStation
nowPlayingVC.newStation = true
} else {
// User clicked on a now playing button
if let currentTrack = currentTrack {
// Return to NowPlaying controller without reloading station
nowPlayingVC.track = currentTrack
nowPlayingVC.currentStation = currentStation
nowPlayingVC.newStation = false
} else {
// Issue with track, reload station
nowPlayingVC.currentStation = currentStation
nowPlayingVC.newStation = true
}
}
}
}
Here's what I think you're not understanding and what's actually going on.
Normally, when you "go back" from a pushed view controller, the pushed view controller is popped and destroyed. Your pushed view controller is a NowPlayingViewController. It should be destroyed when you "go back" from it to the StationsViewController. Thus, when you show the NowPlayingViewController again, you would have to create a new, different NowPlayingViewController.
Okay, so far so good, provided you understand all of that. But in your case there is a further complication: you have a leak! Your old NowPlayingViewController is not being destroyed. Thus, when you "go back" to the StationsViewController and show the NowPlayingViewController for a second time, there are now two NowPlayingViewControllers — the new one that you see, and the old one that is leaking.
Okay, so your logging continues to show the old NowPlayingViewController, which is still observing and updating. But your eyes are seeing the new NowPlayingViewController, which is doing nothing. And that explains the phenomena you have described.
If this is right — and, from what you've said, I'm pretty sure it is — then you need to reorganize your architecture either so that you don't get this leak or so that when you show the NowPlayingViewController the second time you show the same NowPlayingViewController rather than creating a different one. (The first approach would be better.)
So, I have mastered the art of playing sounds/music using one view in iOS, but I am now trying to make a more robust app for an musical artist. Thus far, it involves segues from a "menu" ViewController to his Bio, Show Times, and "Listening Room" etc. Thus far, I created a an "AudioManager" class:
import UIKit
import AVFoundation
import MediaPlayer
class AudioManager: NSObject {
let defaltSong = ["Hell&BackCaf/01 Black Sheep", "caf"]
weak var delegate : PlayerDelegate?
var musicPlayer1 = AVAudioPlayer()
var trackNumber = 0
var musicAudioPath = NSBundle.mainBundle().pathForResource("Hell&BackCaf/01 Black Sheep", ofType: "caf")
var musicAudioPathURL = NSURL()
var error:NSError? = nil
var songList = [["Hell&BackCaf/01 Black Sheep", "caf"], ["Hell&BackCaf/02 Hell & Back", "caf"], ["Hell&BackCaf/03 Save Me", "caf"], ["Hell&BackCaf/04 Broken feat. Hillary Dodson", "caf"], ["Hell&BackCaf/05 Do Or Die", "caf"], ["Hell&BackCaf/06 Divided", "caf"]]
func ButtonPlay(song: NSString, type: NSString) {
error = nil
musicAudioPath = NSBundle.mainBundle().pathForResource(song as String, ofType: type as String)
musicAudioPathURL = NSURL(fileURLWithPath: self.musicAudioPath!)!
musicPlayer1 = AVAudioPlayer(contentsOfURL: musicAudioPathURL, error: &error)
musicPlayer1.prepareToPlay()
musicPlayer1.play()
}
func loadFirstSong() {
error = nil
musicAudioPath = NSBundle.mainBundle().pathForResource("Hell&BackCaf/01 Black Sheep", ofType: "caf")
musicAudioPathURL = NSURL(fileURLWithPath: self.musicAudioPath!)!
musicPlayer1 = AVAudioPlayer(contentsOfURL: musicAudioPathURL, error: &error)
if error == nil {
musicPlayer1.prepareToPlay()
} else {
println(error)
}
}
func advanceTrack(){
if trackNumber < songList.count - 1 {
self.trackNumber++
ButtonPlay(songList[trackNumber][0], type: songList[trackNumber][1])
} else {
trackNumber = 0
ButtonPlay(songList[trackNumber][0], type: songList[trackNumber][1])
}
}
func previousTrack(){
if trackNumber > 0 {
trackNumber--
ButtonPlay(songList[trackNumber][0], type: songList[trackNumber][1])
} else {
trackNumber = songList.count - 1
ButtonPlay(songList[trackNumber][0], type: songList[trackNumber][1])
}
}
func audioPlayerDidFinishPlaying(AVAudioPlayer!, successfully: Bool) {
self.delegate?.soundFinished(self)
println("song over")
}
}
I then used it in my MusicRoomViewController:
import UIKit
import AVFoundation
class MusicRoomViewController: UIViewController, AVAudioPlayerDelegate {
let audioManager = AudioManager()
#IBAction func pressedBackButton(sender: UIButton) {
audioManager.previousTrack()
}
#IBAction func pressedPlayButton(sender: UIButton) {
audioManager.musicPlayer1.play()
}
#IBAction func pressedForwardButton(sender: UIButton) {
audioManager.advanceTrack()
}
override func viewDidLoad() {
super.viewDidLoad()
audioManager.loadFirstSong()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func viewWillDisappear(animated: Bool) {
if self.isMovingFromParentViewController()
{
audioManager.musicPlayer1.play()
}
}
}
But now when I navigate to any other 'page' of the app, the music stops. After researching, I know that I am going about this all wrong, but I can't seem to figure out what I need to do to keep the audio playing until the app is closed or the user presses stop (which does not exist yet, I know). Any thoughts??
You're going to have to make your AudioManager class a singleton. What is happening, is the AudioManager instance you're creating is being deallocated once you navigate away from the view you created it in. A singleton object is created once it is first accessed, and will persist in memory until the application's lifecycle ends or it is explicitly deallocated. For more information on Swift singletons, check out this page. It's a useful pattern to learn.
Make the following modifications to AudioManager (taken from the above website):
private let _AudioManagerSharedInstance = AudioManager()
class AudioManager {
static let sharedInstance = AudioManager()
}
Access AudioManager by using AudioManager.sharedInstance, for example you can call AudioManager.sharedInstance.previousTrack().
You could create a singleton, or instantiate the AudioManager in your AppDelegate class and refer to that like so:
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
appDelegate.audioManger...