Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Optimized Airplay Support #31

Closed
129 changes: 125 additions & 4 deletions DeltaCore/UI/Game/GameViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ public protocol GameViewControllerDelegate: class
func gameViewController(_ gameViewController: GameViewController, handleMenuInputFrom gameController: GameController)

func gameViewControllerDidUpdate(_ gameViewController: GameViewController)

func gameViewControllerDidEnterAirplay(_ gameViewController: GameViewController)
func gameViewControllerDidExitAirplay(_ gameViewController: GameViewController)
}

public extension GameViewControllerDelegate
Expand All @@ -45,6 +48,9 @@ public extension GameViewControllerDelegate
func gameViewController(_ gameViewController: GameViewController, handleMenuInputFrom gameController: GameController) {}

func gameViewControllerDidUpdate(_ gameViewController: GameViewController) {}

func gameViewControllerDidEnterAirplay(_ gameViewController: GameViewController) {}
func gameViewControllerDidExitAirplay(_ gameViewController: GameViewController) {}
}

private var kvoContext = 0
Expand Down Expand Up @@ -96,6 +102,9 @@ open class GameViewController: UIViewController, GameControllerReceiver
private var _previousControllerSkin: ControllerSkinProtocol?
private var _previousControllerSkinTraits: ControllerSkin.Traits?

public var isAirplayEnabled: Bool = false
public var airplayWindow: UIWindow?

/// UIViewController
open override var prefersStatusBarHidden: Bool {
return true
Expand Down Expand Up @@ -123,6 +132,10 @@ open class GameViewController: UIViewController, GameControllerReceiver
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.keyboardWillShow(with:)), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.keyboardWillChangeFrame(with:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.keyboardWillHide(with:)), name: UIResponder.keyboardWillHideNotification, object: nil)

NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.screenDidConnect), name: UIScreen.didConnectNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.screenDidDisconnect), name: UIScreen.didDisconnectNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.screenModeDidChange), name: UIScreen.modeDidChangeNotification, object: nil)
}

deinit
Expand Down Expand Up @@ -162,6 +175,8 @@ open class GameViewController: UIViewController, GameControllerReceiver
let tapGestureRecognizer = UITapGestureRecognizer(target: self.controllerView, action: #selector(ControllerView.becomeFirstResponder))
self.view.addGestureRecognizer(tapGestureRecognizer)

self.handleAirplayScreen()

self.prepareForGame()
}

Expand Down Expand Up @@ -230,6 +245,11 @@ open class GameViewController: UIViewController, GameControllerReceiver
{
super.viewDidLayoutSubviews()

self.calculateGameFrame()
}

@discardableResult public func calculateGameFrame(returnFrame: Bool = false) -> CGRect?
{
let screenAspectRatio = self.emulatorCore?.preferredRenderingSize ?? CGSize(width: 1, height: 1)

let controllerViewFrame: CGRect
Expand Down Expand Up @@ -292,7 +312,12 @@ open class GameViewController: UIViewController, GameControllerReceiver
self.controllerView.frame = controllerViewFrame

/* Game View */
if
if returnFrame
{
return AVMakeRect(aspectRatio: screenAspectRatio, insideRect: availableGameFrame)
}
else if
self.airplayWindow == nil,
let controllerSkin = self.controllerView.controllerSkin,
let traits = self.controllerView.controllerSkinTraits,
let screens = controllerSkin.screens(for: traits),
Expand All @@ -306,7 +331,19 @@ open class GameViewController: UIViewController, GameControllerReceiver
}
else
{
let gameViewFrame = AVMakeRect(aspectRatio: screenAspectRatio, insideRect: availableGameFrame)
var screenAspectRatioToUse = screenAspectRatio
var availableGameFrameToUse = availableGameFrame
if let airplayWindow = self.airplayWindow
{
availableGameFrameToUse = airplayWindow.bounds
if screenAspectRatio.height > screenAspectRatio.width
{
// the only VideoFormat where height > width (for now) is melonDS; correct height for non-touch screen
screenAspectRatioToUse = CGSize(width: screenAspectRatio.width, height: screenAspectRatio.height / 2)
}
}

let gameViewFrame = AVMakeRect(aspectRatio: screenAspectRatioToUse, insideRect: availableGameFrameToUse)
self.gameView.frame = gameViewFrame
}

Expand All @@ -319,12 +356,14 @@ open class GameViewController: UIViewController, GameControllerReceiver
}

self.setNeedsUpdateOfHomeIndicatorAutoHidden()

return nil
}

// MARK: - KVO -
/// KVO
open dynamic override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?)
{
{
guard context == &kvoContext else { return super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) }

// Ensures the value is actually different, or else we might potentially run into an infinite loop if subclasses hide/show controllerView in viewDidLayoutSubviews()
Expand Down Expand Up @@ -395,6 +434,13 @@ extension GameViewController
let outputFrame = screen.outputFrame.applying(.init(scaleX: self.view.bounds.width, y: self.view.bounds.height))
gameView.frame = outputFrame

if let airplayWindow = self.airplayWindow
{
let screenAspectRatio = self.emulatorCore?.preferredRenderingSize ?? CGSize(width: 1, height: 1)
let gameViewFrame = AVMakeRect(aspectRatio: screenAspectRatio, insideRect: airplayWindow.bounds)
self.gameView.frame = gameViewFrame
}

gameViews.append(gameView)
}
}
Expand Down Expand Up @@ -527,7 +573,7 @@ private extension GameViewController
}
}

// MARK: - Notifications -
// MARK: - Notifications -
private extension GameViewController
{
@objc func willResignActive(with notification: Notification)
Expand Down Expand Up @@ -589,3 +635,78 @@ private extension GameViewController
animator.startAnimation()
}
}

//MARK: - AirPlay -
private extension GameViewController
{
@objc func screenDidConnect()
{
self.handleAirplayScreen()
}

@objc func screenDidDisconnect()
{
self.handleAirplayScreen()
}

@objc func screenModeDidChange()
{
self.handleAirplayScreen()
}
}

public extension GameViewController
{
func handleAirplayScreen()
{
// perform teardown first if already AirPlaying and screens are being switched
if self.airplayWindow != nil
{
self.view.insertSubview(self.gameView, belowSubview: self.controllerView)

self.airplayWindow = nil

self.gameView.setNeedsLayout()
self.gameView.layoutIfNeeded()
self.view.setNeedsLayout()
self.view.layoutIfNeeded()

self.delegate?.gameViewControllerDidExitAirplay(self)
}

// now perform setup of AirPlay screen
guard
self.isAirplayEnabled,
UIScreen.screens.count > 1
else { return }

let secondScreen = UIScreen.screens[1]

// find max resolution
var max = CGSize(width: 0, height: 0)
var maxScreenMode: UIScreenMode? = nil
for mode in secondScreen.availableModes
{
if (maxScreenMode == nil || mode.size.height > max.height || mode.size.width > max.width)
{
max = mode.size
maxScreenMode = mode
}
}
secondScreen.currentMode = maxScreenMode

// setup window on second screen
self.airplayWindow = UIWindow(frame: secondScreen.bounds)
self.airplayWindow?.isHidden = false
self.airplayWindow?.layer.contentsGravity = .resizeAspect
self.airplayWindow?.screen = secondScreen

self.airplayWindow?.addSubview(self.gameView)
self.gameView.frame = self.airplayWindow?.frame ?? .zero

self.gameView.setNeedsLayout()
self.airplayWindow?.layoutIfNeeded()

self.delegate?.gameViewControllerDidEnterAirplay(self)
}
}