A storyboard has tow ViewControllers-s, one for Portrait orientation (VCPort for short), another for landscape (VCLand). I want the application to switch automatically to correct layout when the device is rotated.
For that purpose I have two segues portToLand from VSPort to VCLand and LandToPort in opposite direction. Then I override willRotateToScreenOrientation as the following.
For VCPort class:
override func willRotateToInterfaceOrientation(toInterfaceOrientation: UIInterfaceOrientation, duration: NSTimeInterval)
{
if toInterfaceOrientation.isLandscape {
switchViewController()
}
}
func switchViewController() {
performSegueWithIdentifier("portToLand", sender: self)
}
For VCLand class I have isPortrait instead of isLandscape and "landToPort" instead of "portToLand".
Everything works OK if the storyboard starts in Portrait orientation. When the storyboard starts while the device is in Landscape (e.g. with IPad Retina) VCPort still gets the control as the first ViewController in chain and willRotateToScreen doesn't come. To make the application switch to Landscape layout in this case I place the following code in viewDidLoad for VCPort class:
override func viewDidLoad() {
super.viewDidLoad()
if UIApplication.sharedApplication().statusBarOrientation.isLandscape {
switchViewController()
}
}
Tracing with debugger show thats performSegueWithIdentifier is called, but VCLand doesn't get the control, the application still shows VCPort layout! In assumption that I do it too early, I tried viewWillAppear instead of viewDidLoad - no difference.
Thanks for any comments.
Just put a short delay before switching views. And moved the code to viewWillAppear, since the orientation may change while app is in pause:
var timer : NSTimer?
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
let deviceOrientation = UIDevice.currentDevice().orientation
var isLandscape : Bool;
if deviceOrientation == UIDeviceOrientation.Unknown {
isLandscape = UIApplication.sharedApplication().statusBarOrientation.isLandscape
} else {
isLandscape = deviceOrientation.isLandscape
}
if isLandscape {
self.view.hidden = true
timer = NSTimer.scheduledTimerWithTimeInterval(0.05, target: self, selector: Selector("switchViewController"), userInfo: nil, repeats: false)
} else {
self.view.hidden = false
}
}
override func viewWillDisappear(animated: Bool) {
if (timer != nil) {
timer!.invalidate()
timer = nil
}
}
Related
I have an app targeted iOS8 and initial view controller is UISplitViewController. I use storyboard, so that it kindly instantiate everything for me.
Because of my design I need SplitViewController to show both master and detail views in portrait mode on iPhone. So I am looking for a way to override trait collection for this UISplitViewController.
I found that I can use
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator!) { ... }
but, unfortunately, there are only methods to override child controllers traits collections:
setOverrideTraitCollection(collection: UITraitCollection!, forChildViewController childViewController: UIViewController!)
and I can't do so for self in my UISplitViewController subclass.
I checked an example app Adaptive Photos from Apple. And in this app author use special TraitOverrideViewController as root and some magic in his viewController setter to make it all works.
It looks horrible for me. Is there are any way around to override traits? Or If there are not, how can I manage to use the same hack with storyboard? In other words, how to inject some viewController as root one only to handle traits for my UISplitViewController with storyboard?
Ok, I wish there was another way around this, but for now I just converted code from the Apple example to Swift and adjusted it to use with Storyboards.
It works, but I still believe it is an awful way to archive this goal.
My TraitOverride.swift:
import UIKit
class TraitOverride: UIViewController {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
var forcedTraitCollection: UITraitCollection? {
didSet {
updateForcedTraitCollection()
}
}
override func viewDidLoad() {
setForcedTraitForSize(view.bounds.size)
}
var viewController: UIViewController? {
willSet {
if let previousVC = viewController {
if newValue !== previousVC {
previousVC.willMoveToParentViewController(nil)
setOverrideTraitCollection(nil, forChildViewController: previousVC)
previousVC.view.removeFromSuperview()
previousVC.removeFromParentViewController()
}
}
}
didSet {
if let vc = viewController {
addChildViewController(vc)
view.addSubview(vc.view)
vc.didMoveToParentViewController(self)
updateForcedTraitCollection()
}
}
}
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator!) {
setForcedTraitForSize(size)
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
}
func setForcedTraitForSize (size: CGSize) {
let device = traitCollection.userInterfaceIdiom
var portrait: Bool {
if device == .Phone {
return size.width > 320
} else {
return size.width > 768
}
}
switch (device, portrait) {
case (.Phone, true):
forcedTraitCollection = UITraitCollection(horizontalSizeClass: .Regular)
case (.Pad, false):
forcedTraitCollection = UITraitCollection(horizontalSizeClass: .Compact)
default:
forcedTraitCollection = nil
}
}
func updateForcedTraitCollection() {
if let vc = viewController {
setOverrideTraitCollection(self.forcedTraitCollection, forChildViewController: vc)
}
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
performSegueWithIdentifier("toSplitVC", sender: self)
}
override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {
if segue.identifier == "toSplitVC" {
let destinationVC = segue.destinationViewController as UIViewController
viewController = destinationVC
}
}
override func shouldAutomaticallyForwardAppearanceMethods() -> Bool {
return true
}
override func shouldAutomaticallyForwardRotationMethods() -> Bool {
return true
}
}
To make it work you need to add a new UIViewController on the storyboard and made it the initial. Add show segue from it to your real controller like this:
You need to name the segue "toSplitVC":
and set initial controller to be TraitOverride:
Now it should work for you too. Let me know if you find a better way or any flaws in this one.
I understand that you wanted a SWIFT translation here... And you've probably solved that.
Below is something I've spent a considerable time trying to resolve - getting my SplitView to work on an iPhone 6+ - this is a Cocoa solution.
My Application is TabBar based and the SplitView has Navigation Controllers. In the end my issue was that setOverrideTraitCollection was not being sent to the correct target.
#interface myUITabBarController ()
#property (nonatomic, retain) UITraitCollection *overrideTraitCollection;
#end
#implementation myUITabBarController
- (void)viewDidLoad
{
[super viewDidLoad];
[self performTraitCollectionOverrideForSize:self.view.bounds.size];
}
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
NSLog(#"myUITabBarController %#", NSStringFromSelector(_cmd));
[self performTraitCollectionOverrideForSize:size];
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
}
- (void)performTraitCollectionOverrideForSize:(CGSize)size
{
NSLog(#"myUITabBarController %#", NSStringFromSelector(_cmd));
_overrideTraitCollection = nil;
if (size.width > 320.0)
{
_overrideTraitCollection = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
}
[self setOverrideTraitCollection:_overrideTraitCollection forChildViewController:self];
for (UIViewController * view in self.childViewControllers)
{
[self setOverrideTraitCollection:_overrideTraitCollection forChildViewController:view];
NSLog(#"myUITabBarController %# AFTER viewTrait=%#", NSStringFromSelector(_cmd), [view traitCollection]);
}
}
#end
UPDATE:
Apple do not recommend doing this:
Use the traitCollection property directly. Do not override it. Do not
provide a custom implementation.
I'm not overriding this property anymore! Now I'm calling overrideTraitCollectionForChildViewController: in the parent viewControler class.
Old answer:
I know it's more than a year since question was asked, but i think my answer will help someone like me who do not achieved success with the accepted answer.
Whell the solution is really simple, you can just override traitCollection: method. Here is an example from my app:
- (UITraitCollection *)traitCollection {
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) {
return super.traitCollection;
} else {
switch (self.modalPresentationStyle) {
case UIModalPresentationFormSheet:
case UIModalPresentationPopover:
return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
default:
return super.traitCollection;
}
}
}
the idea is to force Compact size class on iPad if controller is presented as popover or form sheet.
Hope it helps.
The extra top level VC works well for a simple app but it won't propagate down to modally presented VC's as they don't have a parentVC. So you need to insert it again in different places.
A better approach I found was just to subclass UINavigationController and then just use your subclass in the storyboard and elsewhere where you would normally use UINavigationController. It saves the additional VC clutter in storyboards and also saves extra clutter in code.
This example will make all iPhones use regular horizontal size class for landscape.
#implementation MyNavigationController
- (UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController
{
UIDevice *device = [UIDevice currentDevice];
if (device.userInterfaceIdiom == UIUserInterfaceIdiomPhone && CGRectGetWidth(childViewController.view.bounds) > CGRectGetHeight(childViewController.view.bounds)) {
return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
}
return nil;
}
#end
Yes, it must use custom container View Controller to override the function viewWillTransitionToSize. You use the storyboard to set the container View Controller as initial.
Also, you can refer this good artical which use the program to implement it. According to it, your judgement portait could have some limitations:
var portrait: Bool {
if device == .Phone {
return size.width > 320
} else {
return size.width > 768
}
}
other than
if **size.width > size.height**{
self.setOverrideTraitCollection(UITraitCollection(horizontalSizeClass: UIUserInterfaceSizeClass.Regular), forChildViewController: viewController)
}
else{
self.setOverrideTraitCollection(nil, forChildViewController: viewController)
}
"
Props To #Ilyca
Swift 3
import UIKit
class TraitOverride: UIViewController {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
}
var forcedTraitCollection: UITraitCollection? {
didSet {
updateForcedTraitCollection()
}
}
override func viewDidLoad() {
setForcedTraitForSize(size: view.bounds.size)
}
var viewController: UIViewController? {
willSet {
if let previousVC = viewController {
if newValue !== previousVC {
previousVC.willMove(toParentViewController: nil)
setOverrideTraitCollection(nil, forChildViewController: previousVC)
previousVC.view.removeFromSuperview()
previousVC.removeFromParentViewController()
}
}
}
didSet {
if let vc = viewController {
addChildViewController(vc)
view.addSubview(vc.view)
vc.didMove(toParentViewController: self)
updateForcedTraitCollection()
}
}
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
setForcedTraitForSize(size: size)
super.viewWillTransition(to: size, with: coordinator)
}
func setForcedTraitForSize (size: CGSize) {
let device = traitCollection.userInterfaceIdiom
var portrait: Bool {
if device == .phone {
return size.width > 320
} else {
return size.width > 768
}
}
switch (device, portrait) {
case (.phone, true):
forcedTraitCollection = UITraitCollection(horizontalSizeClass: .regular)
case (.pad, false):
forcedTraitCollection = UITraitCollection(horizontalSizeClass: .compact)
default:
forcedTraitCollection = nil
}
}
func updateForcedTraitCollection() {
if let vc = viewController {
setOverrideTraitCollection(self.forcedTraitCollection, forChildViewController: vc)
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
performSegue(withIdentifier: "toSplitVC", sender: self)
}
override var shouldAutomaticallyForwardAppearanceMethods: Bool {
return true
}
override func shouldAutomaticallyForwardRotationMethods() -> Bool {
return true
}
}
I have a controller with a AVPlayer in collection view cell. When orientation changes to Landscape, the player should get FullScreen.
For this I am presenting an AVPlayerController with same instance of player in Collection View Cell. The video works fine when it is rotated in playing mode.
However, when video is paused and I change orientation to Landscape, the frame at current moment changes i.e video moves forward.
I have tested, even when the orientation is kept same, when player is passed, the duration skips few seconds.
Here is the code:
In ViewController where cell is present.
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
guard let videoCell = contentGalleryController.curatorVideoCell else {return}
if UIDevice.current.orientation == .landscapeLeft || UIDevice.current.orientation == .landscapeRight {
let player = videoCell.getVideoPlayer
playerViewController = (storyboard!.instantiateViewController(withIdentifier: "FullScreenPlayerController") as! FullScreenPlayerController)
playerViewController?.player = player
playerViewController?.didPlayedToEnd = videoCell.isVideoFinishedPlaying ?? false
playerViewController?.isMuteTurnedOn = player.isMuted
let wasVideoPlaying: Bool = player.isPlaying
present(playerViewController!, animated: false){
if wasVideoPlaying {
player.play()
}
}
}
else {
videoCell._setMuteIcon()
videoCell._setPlayPauseIcon()
playerViewController?.dismiss(animated: false, completion: nil)
}
}
In FullscreenPlayer View Controller
override func viewDidLoad() {
super.viewDidLoad()
setPlayPauseIcon()
setMuteIcon()
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(customControlViewTapped))
customView.addGestureRecognizer(tapGesture)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if !view.subviews.contains(customView) {
customView.frame = view.bounds
view.addSubview(customView)
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
NotificationCenter.default.addObserver(self, selector: #selector(playerDidPlayToEnd), name: Notification.Name.AVPlayerItemDidPlayToEndTime, object: player?.currentItem)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
NotificationCenter.default.removeObserver(self)
}
I am doing nothing else in controller.
Screenshots:
Portrait
LANDSCAPE
When orientation changes, video moves forward even on pause state.
Thanks for help in advance.
You should try capturing the currentTime and then use the seek(to time: CMTime) method on the player to start at that exact time. I am not sure exactly why this is happening to you, but I think this would get you the result you are looking for.
func SetDeviceFontSizes()
{
...
imgView.frame.size.width = 375
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
SetDeviceFontSizes()
}
override func viewWillLayoutSubviews() {
SetDeviceFontSizes()
}
override func viewDidLoad() {
super.viewDidLoad()
SetDeviceFontSizes()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
SetDeviceFontSizes()
}
I want to set sizes for labels and images (for old devices these sizes will be small).
But when I change orientation of device, the sizes return to default values in spite of function SetDeviceFontSizes().
How can I set frame sizes after change orientation?
var m_CurrentOrientation = UIDeviceOrientation()
add this line in your viewdidload()
UIDevice.currentDevice().beginGeneratingDeviceOrientationNotifications()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(self.deviceOrientationDidChange), name: .DidChangeNotification, object: nil)
perform your task in if condition
func OrientationDidChange(notification: NSNotification) {
var Orientation = UIDevice.currentDevice().orientation
if Orientation == .LandscapeLeft || Orientation == .LandscapeRight {
}
else if Orientation == .Portrait {
}
}
i'm not sure this will solve your problem, but you should call super in viewWillLayoutSubviews and viewWillTransition
You can use UITraitCollection. Refer to this link
How to translate this objective c traitCollection to swift?
Do not call anything in viewWillLayoutSubviews() function.
Hope it helps you.
I'm porting my iPad app to iOS8 and Swift.
In portrait, i use a root UIViewController and when the de device is rotated to landscape, I segue to another UIViewController. I've come up with 2 solutions, one based on UIDevice notifications and the other on willRotateToInterfaceRotation. I alway try to stay away from the Observer pattern, it's just a habit of me.
The Observer works fine, but the override in both UIViewController of
func willRotateToInterfaceOrientation(toInterfaceOrientation: UIInterfaceOrientation, duration: NSTimeInterval)
looks cleaner to my eyes ;)
But now in iOS8 that function is deprecated and I should use
func viewWillTransitionToSize(_ size: CGSize,
withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator)
But I don't have any idea how I can work with that to get the same result.
This is the rootViewController:
override func willRotateToInterfaceOrientation(
toInterfaceOrientation: UIInterfaceOrientation,
duration: NSTimeInterval) {
if (toInterfaceOrientation == UIInterfaceOrientation.LandscapeLeft ||
toInterfaceOrientation == UIInterfaceOrientation.LandscapeRight) {
self.performSegueWithIdentifier("toLandscape", sender: self)
}
}
and the UIViewControllerLanscape:
override func willRotateToInterfaceOrientation(
toInterfaceOrientation: UIInterfaceOrientation,
duration: NSTimeInterval) {
if (toInterfaceOrientation == UIInterfaceOrientation.Portrait ||
toInterfaceOrientation == UIInterfaceOrientation.PortraitUpsideDown) {
self.presentingViewController?.dismissViewControllerAnimated(true,
completion: nil)
}
}
I don't like to use deprecated functions, so I'm in doubt what to do...go for the observer or what??
This is the code I use when (only) the root UIViewController is an observer of the UIDeviceOrientationDidChangeNotification:
override func awakeFromNib() {
let dev = UIDevice.currentDevice()
dev.beginGeneratingDeviceOrientationNotifications()
let nc = NSNotificationCenter.defaultCenter()
nc.addObserver(self, selector: "orientationChanged:", name: UIDeviceOrientationDidChangeNotification, object: dev)
}
func orientationChanged(note: NSNotification) -> () {
if let uidevice: UIDevice = note.object? as? UIDevice {
switch uidevice.orientation {
case .LandscapeLeft, .LandscapeRight:
self.performSegueWithIdentifier("toLandscape", sender: self)
default:
self.dismissViewControllerAnimated(true, completion: nil)
}
}
}
I really like your ideas on how to make a solution for these deprecated functions. It's a total mystery to me....or maybe the Observer is the better solution now. Please share your ideas.
Regards,
jr00n
I have been creating a game in sprite kit using swift and have encountered a problem. I have two view controllers, each with one scene and one transitions to the other modally. This all works perfectly first time round, but then when i return to the first view controller and then go to the second again, i have using double the memory. This gives me the impression that nothing is being deallocated, but the objects are rather reallocated every time I transition to the scene. I ran the app in instruments and got the same result. In the below image i moved from one scene to the next, and then back to the first one again and yet it appears to reallocate the first scene and yet not clear any memory. As the dealloc method is unused now, i don't see how i can fix this. I will post the code to the first view controller below so you can have a look at it. Thanks a lot.
import UIKit
import SpriteKit
class SelectionViewController: UIViewController {
var selectionScene:SelectionScene?
var currentRocketName = ""
#IBOutlet var playButton: UIButton
override func viewDidLoad() {
super.viewDidLoad()
if let selectionScene = SelectionScene.unarchiveFromFile("SelectionScene") as? SelectionScene {
// Configure the view.
let skView = self.view as SKView
skView.showsFPS = true
skView.showsNodeCount = true
skView.multipleTouchEnabled = false
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
selectionScene.scaleMode = .ResizeFill
selectionScene.viewController = self
skView.presentScene(selectionScene)
NSNotificationCenter.defaultCenter().addObserver(selectionScene, selector: "spinnerChanged", name: "spinnerValueChanged", object: nil)
NSNotificationCenter.defaultCenter().addObserver(selectionScene, selector: "productBought", name: "ProductBought", object: nil);
NSNotificationCenter.defaultCenter().addObserver(selectionScene, selector: "manageErrorInPurchase", name: "ErrorOccured", object: nil)
}
}
override func shouldAutorotate() -> Bool {
return true
}
override func viewDidAppear(animated: Bool) {
}
#IBAction func playButtonPressed(sender: UIButton) {
self.performSegueWithIdentifier("moveToGame", sender: nil)
}
override func prefersStatusBarHidden() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> Int {
if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
return Int(UIInterfaceOrientationMask.AllButUpsideDown.toRaw())
} else {
return Int(UIInterfaceOrientationMask.All.toRaw())
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
override func viewDidUnload() {
NSNotificationCenter.defaultCenter().removeObserver(selectionScene)
}
override func viewDidDisappear(animated: Bool) {
NSNotificationCenter.defaultCenter().removeObserver(selectionScene)
}
override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {
if segue.identifier == "moveToGame" {
let destController = segue.destinationViewController as GameViewController
destController.rocketTexture = SKTexture(imageNamed: self.currentRocketName)
}
}
}
Both selectionScene and currentRocketName are passed to the viewController as soon as they are loaded into the view
I'm not familiar with Swift yet, so I'll give you examples in Objective-C.
Create an IBOutlet for skView. When you are going to present another ViewController, remove skView from it's superview and nil it out:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// Need to deallocate GameScene (if game is not paused)
[self.skView removeFromSuperview];
self.skView = nil;
....
}
Don't forget to add skView back to the ViewController's view, when ViewController is getting loaded:
if (!self.skView.window) {
[self.view addSubview:self.skView];
}
To easily check if SKScene was deallocated or not, add this method to it:
- (void)dealloc {
NSLog(#"GAME SCENE DEALLOCATED");
}
I had a similar issue.
Turns out I had created a strong reference by having an SKScene instance as a delegate in another class. After declaring each property of SKScene type or UIView type as weak my issue was resolved:
weak var skScene:SKScene!