One of possible solutions is to split your application to "rotating" part and "non-rotating" part using windows.
It is not an ideal choice but lately the tools that we get do not give us much options. The problem you are facing when using this procedure is that you can have some chaos when presenting new view controllers. In your case this may not be issue at all but still...
In short what you do is:
- Leave main window as it is but enable your application to rotate into all directions that you need
- Create a view controller that only supports one orientation (whichever you prefer) and show it a new window over your main one but below status bar (Default behavior)
- Create a view controller with transparent background and that can pass touch events through to the window below it. Also show it in a new window over the previous one. Also this window needs to pass touch events through to bottom window.
You can create all of these in code or with storyboard. But there are a few components to manage. These are all I used when validating this approach:
class StandStillViewController: WindowViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
func setupManually() {
self.view = DelegatedTouchEventsView()
self.view.backgroundColor = UIColor.darkGray
let label = UILabel(frame: .zero)
label.text = "A part of this app that stands still"
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
view.addConstraint(.init(item: label, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1.0, constant: 0.0))
view.addConstraint(.init(item: label, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .centerY, multiplier: 1.0, constant: 0.0))
let button = UIButton(frame: .zero)
button.setTitle("Test button", for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(testButton), for: .touchUpInside)
view.addSubview(button)
view.addConstraint(.init(item: button, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1.0, constant: 0.0))
view.addConstraint(.init(item: button, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .centerY, multiplier: 1.0, constant: 50.0))
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask { return .portrait }
override var shouldAutorotate: Bool { false }
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { return .portrait }
@IBAction private func testButton() {
print("Button was pressed")
}
}
This is the controller at the bottom. I expect this one will host your game. You need to preserve
override var supportedInterfaceOrientations: UIInterfaceOrientationMask { return .portrait }
override var shouldAutorotate: Bool { false }
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { return .portrait }
the rest may be changed, removed.
class RotatingViewController: WindowViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
func setupManually() {
self.view = DelegatedTouchEventsView()
self.view.backgroundColor = UIColor.clear // Want to see through it
let label = UILabel(frame: .zero)
label.text = "A Rotating part of this app"
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
view.addConstraint(.init(item: label, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1.0, constant: 0.0))
view.addConstraint(.init(item: label, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1.0, constant: 60.0))
let button = UIButton(frame: .zero)
button.setTitle("Test rotating button", for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(testButton), for: .touchUpInside)
view.addSubview(button)
view.addConstraint(.init(item: button, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1.0, constant: 0.0))
view.addConstraint(.init(item: button, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .centerY, multiplier: 1.0, constant: -50.0))
}
@IBAction private func testButton() {
print("Rotating button was pressed")
}
}
This controller will deal with rotating stuff. You need to preserve
self.view = DelegatedTouchEventsView()
self.view.backgroundColor = UIColor.clear // Want to see through it
which may both be set in storyboard as well. Anything you put onto this controller will rotate with your device. A nice place to put some GUI stuff for instance.
class WindowViewController: UIViewController {
private var window: UIWindow?
func shownInNewWindow(delegatesTouchEvents: Bool, baseWindow: UIWindow? = nil) {
let scene = baseWindow?.windowScene ?? UIApplication.shared.windows.first!.windowScene!
let newWindow: UIWindow
if delegatesTouchEvents {
newWindow = DelegatedTouchEventsWindow(windowScene: scene)
} else {
newWindow = UIWindow(windowScene: scene)
}
newWindow.rootViewController = self
newWindow.windowLevel = .normal
newWindow.makeKeyAndVisible()
self.window = newWindow
}
func dismissFromWindow(completion: (() -> Void)? = nil) {
removeFromWindow()
completion?()
}
func removeFromWindow() {
self.window?.isHidden = true
self.window = nil
}
}
This is what I used as base class for both view controllers above. It is not much but it allows view controllers to be shown in a new window. This code was pasted from one of my older projects and could use some minor improvements. But it does works so...
class DelegatedTouchEventsView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
return view == self ? nil : view
}
}
class DelegatedTouchEventsWindow: UIWindow {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
return view == self ? nil : view
}
}
These are the two subclasses to let touch events go through. A quick explanation on how this works: When event is received your system will first send this event through your view hierarchy asking "who is going to handle this event?". Returning nil
means "not me" and default for UIView
is self
. So in this code we say: "If any of my subviews wants to handle this event (such as a button) then it may handle this event. But if none of them wants to handle them then neither I will."
And UIWindow
is a subclass of UIView
so we need to deal with both of them.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// let standingStillController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "StandStillViewController") as! StandStillViewController
// standingStillController.shownInNewWindow(delegatesTouchEvents: false, baseWindow: self.view.window)
//
// let rotatingViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "RotatingViewController") as! RotatingViewController
// rotatingViewController.shownInNewWindow(delegatesTouchEvents: true, baseWindow: self.view.window)
let standingStillController = StandStillViewController()
standingStillController.setupManually()
standingStillController.shownInNewWindow(delegatesTouchEvents: false, baseWindow: self.view.window)
let rotatingViewController = RotatingViewController()
rotatingViewController.setupManually()
rotatingViewController.shownInNewWindow(delegatesTouchEvents: true, baseWindow: self.view.window)
}
}
This is an example on how to use it all together. As promised, either using Storyboards or manually, both should work.
Seems like a lot of work but you need to set it up once and never look at it again.