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
}
}
Related
I have two view controllers. MainViewController and SecondViewController (this one is embedded in a Navigation Controller).
MainViewController has a UIButton that will modally present SecondViewController, while SecondViewController has a UIButton that will dismiss itself.
Each of them have the following code:
var statusBarHidden = false {
didSet {
UIView.animate(withDuration: 0.5) { () -> Void in
self.setNeedsStatusBarAppearanceUpdate()
}
}
}
override var prefersStatusBarHidden: Bool {
return statusBarHidden
}
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .slide
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
statusBarHidden = true
}
The slide animation of the status bar works great in the simulator but not on the actual device, what am i doing wrong ?
I'm using xCode 8.2.1 and Swift 3
What i ended up doing was this. I created a variable that links to the view of the status bar and added functions so i can do what i need.
extension UIApplication {
var statusBarView: UIView? {
return value(forKey: "statusBar") as? UIView
}
func changeStatusBar(alpha: CGFloat) {
statusBarView?.alpha = alpha
}
func hideStatusBar() {
UIView.animate(withDuration: 0.3) {
self.statusBarView?.alpha = 0
}
}
func showStatusBar() {
UIView.animate(withDuration: 0.3) {
self.statusBarView?.alpha = 1
}
}
}
A typical use would be:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let alpha = tableView.contentOffset.y / 100
UIApplication.shared.changeStatusBar(alpha: alpha)
}
I have a portrait application with one landscape ViewController.
I've been digging a lot on how to force the orientation to landscape when the app is locked to portrait and I tried a lot of solutions presented here and not only but without any luck so far.
So far I managed to autorotate the VC I need in landscape but since the orientation is not locked, all other VCs will also rotate.
override func viewDidAppear(animated: Bool)
{
let value = UIInterfaceOrientation.LandscapeRight.rawValue
UIDevice.currentDevice().setValue(value, forKey: "orientation")
}
override func shouldAutorotate() -> Bool
{
return true
}
After some more digging I ended up with this after finding a sample project but while it works in that project, it doesn't seem to work for me.
This is in AppDelegate
func application(application: UIApplication, supportedInterfaceOrientationsForWindow window: UIWindow?) -> UIInterfaceOrientationMask {
if self.window?.rootViewController?.presentedViewController is ConclusionReferencesVC {
let conclusionReferencesVC = self.window!.rootViewController!.presentedViewController as! ConclusionReferencesVC
if conclusionReferencesVC.isPresented
{
return UIInterfaceOrientationMask.LandscapeRight;
}
else
{
return UIInterfaceOrientationMask.Portrait;
}
}
else
{
return UIInterfaceOrientationMask.Portrait;
}
}
This is in the VC I want to have in landscape:
var isPresented = true
#IBAction
func dismiss()
{
isPresented = false
self.presentingViewController!.dismissViewControllerAnimated(true, completion: nil);
}
For some reason, the supportedInterfaceOrientationsForWindow method does not validate the initial condition.
I also tried these among others but no luck:
How to lock orientation of one view controller to portrait mode only in Swift
supportedInterfaceOrientationsForWindow in Swift 2.0
Any ideas? It seems I'm missing something but I can't figure it out what.
try this for force to LandscapeRight mode only
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if(self.supportedInterfaceOrientations() == UIInterfaceOrientationMask.LandscapeRight && UIDevice.currentDevice().orientation != UIDeviceOrientation.LandscapeRight)
{
let value = UIInterfaceOrientation.LandscapeRight.rawValue
UIDevice.currentDevice().setValue(value, forKey: "orientation")
}
}
and then use a category like this
import UIKit
extension UINavigationController{
override public func shouldAutorotate() -> Bool
{
return (self.viewControllers.last?.shouldAutorotate())!
}
override public func supportedInterfaceOrientations() ->UIInterfaceOrientationMask
{
return (self.viewControllers.last?.supportedInterfaceOrientations())!;
}
override public func preferredInterfaceOrientationForPresentation()-> UIInterfaceOrientation
{
return (self.viewControllers.last?.preferredInterfaceOrientationForPresentation())!;
}
}
EDITED
If you are not using navigation controller use this
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if(self.supportedInterfaceOrientations() == UIInterfaceOrientationMask.LandscapeRight && UIDevice.currentDevice().orientation != UIDeviceOrientation.LandscapeRight)
{
let value = UIInterfaceOrientation.LandscapeRight.rawValue
UIDevice.currentDevice().setValue(value, forKey: "orientation")
}
}
override func supportedInterfaceOrientations() ->UIInterfaceOrientationMask
{
return .LandscapeRight;
}
I hope this helps you
As an update to Reinier Melian's post, here is the UINavigationController extension in Swift 3:
import UIKit
extension UINavigationController {
override open var shouldAutorotate: Bool {
return (self.viewControllers.last?.shouldAutorotate)!
}
override open var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return (self.viewControllers.last?.supportedInterfaceOrientations)!
}
override open var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
return (self.viewControllers.last?.preferredInterfaceOrientationForPresentation)!
}
}
Unfortunately, this code crashes if you call a UIImagePickerController, as that is contained within a UINavigationController whose last view controller is nil.
probably I´m crazy ;-), but why don´t u use this?
The effect that I want to achieve is:
And the current state of my app is:
This is the set up of my view controller. I put a tool bar underneath the navigation bar. Then, I set the tool bar's delegate to the navigation bar. I've read several posts about this. One solution that was provided was:
navigationController?.navigationBar.shadowImage = UIImage();
navigationController?.navigationBar.setBackgroundImage(UIImage(), forBarMetrics: .Default)
However, this causes the navigation bar to become white and loses the effect. So I got the following code from this post (UISegmentedControl below UINavigationbar in iOS 7):
#IBOutlet weak var toolbar: UIToolbar!
var hairLine: UIView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
doneButton.enabled = false
for parent in self.navigationController!.navigationBar.subviews {
for childView in parent.subviews {
if childView is UIImageView && childView.bounds.size.width == self.navigationController!.navigationBar.frame.size.width {
hairLine = childView
print(hairLine.frame)
}
}
}
}
func removeHairLine(appearing: Bool) {
var hairLineFrame = hairLine.frame
if appearing {
hairLineFrame.origin.y += toolbar.bounds.size.height
} else {
hairLineFrame.origin.y -= toolbar.bounds.size.height
}
hairLine.frame = hairLineFrame
print(hairLine.frame)
}
override func viewWillAppear(animated: Bool) {
removeHairLine(true)
}
override func viewWillDisappear(animated: Bool) {
removeHairLine(true)
}
However, this code removes the hairline before the view is completely loaded but when the view is loaded, it appears again. Any solutions?
I found solution on this site but don't remember where exactly.
Objective-C:
#interface YourViewController () {
UIImageView *navBarHairlineImageView;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
navBarHairlineImageView = [self findHairlineImageViewUnder:self.navigationController.navigationBar];
navBarHairlineImageView.hidden = YES;
}
- (UIImageView *)findHairlineImageViewUnder:(UIView *)view {
if ([view isKindOfClass:UIImageView.class] && view.bounds.size.height <= 1.0) {
return (UIImageView *)view;
}
for (UIView *subview in view.subviews) {
UIImageView *imageView = [self findHairlineImageViewUnder:subview];
if (imageView) {
return imageView;
}
}
return nil;
}
Swift:
class YourViewController: UIViewController {
var navBarLine: UIImageView?
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
navBarLine = findHairlineImageViewUnderView(self.navigationController?.navigationBar)
navBarLine?.hidden = true
}
func findHairlineImageViewUnderView(view: UIView?) -> UIImageView? {
if view.isKindOfClass(UIImageView.classForCoder()) && view.bounds.height <= 1 {
return view as? UIImageView
}
for subview in view.subviews {
if let imgView = findHairlineImageViewUnderView(subview) {
return imgView
}
}
return nil
}
}
I use this lines of code
UINavigationBar.appearance().shadowImage = UIImage()
UINavigationBar.appearance().setBackgroundImage(UIImage(named: "background"), for: .default)
Try this
for parent in self.navigationController!.navigationBar.subviews {
for childView in parent.subviews {
if(childView is UIImageView) {
childView.removeFromSuperview()
}
}
}
I hope this help you.
You could use this
self.navigationController?.navigationBar.subviews[0].subviews.filter({$0 is UIImageView})[0].removeFromSuperview()
I didn't find any good Swift 3 solution so I am adding this one, based on Ivan Bruel answer. His solution is protocol oriented, allows to hide hairline in any view controller with just one line of code and without subclassing.
Add this code to your views model:
protocol HideableHairlineViewController {
func hideHairline()
func showHairline()
}
extension HideableHairlineViewController where Self: UIViewController {
func hideHairline() {
findHairline()?.isHidden = true
}
func showHairline() {
findHairline()?.isHidden = false
}
private func findHairline() -> UIImageView? {
return navigationController?.navigationBar.subviews
.flatMap { $0.subviews }
.flatMap { $0 as? UIImageView }
.filter { $0.bounds.size.width == self.navigationController?.navigationBar.bounds.size.width }
.filter { $0.bounds.size.height <= 2 }
.first
}
}
Then make sure view controller which doesn't need hairline conforms to HideableHairlineViewController protocol and call hideHairline().
Swift 4 version of alexandr answer
Step 1: Create property of type UIImageView?
private var navigationBarHairLine: UIImageView?
Step 2: Create findHairlineImageViewUnderView function
This function filters through the view's subviews to find the view with the height of less than or equal to 1pt.
func findHairlineImageViewUnderView(view: UIView?) -> UIImageView? {
guard let view = view else { return nil }
if view.isKind(of: UIImageView.classForCoder()) && view.bounds.height <= 1 {
return view as? UIImageView
}
for subView in view.subviews {
if let imageView = findHairlineImageViewUnderView(view: subView) {
return imageView
}
}
return nil
}
Step 3: Call the created function in ViewWillAppear and pass in the navigationBar. It will return the hairline view which you then set as hidden.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationBarHairLine = findHairlineImageViewUnderView(view: navigationController?.navigationBar)
navigationBarHairLine?.isHidden = true
}
You can subclass UINavigationBar and set the following in initializer (Swift 5):
shadowImage = UIImage()
setBackgroundImage(UIImage(), for: .default) // needed for iOS 10
E.g.:
class CustomNavigationBar: UINavigationBar {
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupViews()
}
private func setupViews() {
shadowImage = UIImage()
setBackgroundImage(UIImage(), for: .default) // needed for iOS 10
}
}
The Problem
I have a problem with my split view. It works fine on iPhone and iPad simulators, but on the iPhone 6+ I lose the navigation bar after rotating the device. Here's what happens on the 6+ simulator:
I start the app and it presents a + button in the navigation bar. I tap this button.
It loads a view controller over the existing view. A navigation bar, as expected, is visible with a working back button.
I turn the device horizontally. As intended the new controller appears in the Master section, with an empty detail section on the right. Unfortunately the navigation bar dissapears.
When I turn the device vertically the navigation bar does not reappear.
In fact when I turn the device horizontally it seems the navigation controller is removed from the stack (I've observed this from outputting the contents of splitViewContoller.viewControllers).
My Code
The test application is simply the Master Detail template with a few modifications.
I've added a new "Add Item" controller and then created a show segue from the Master view's "+" button. The "Add Item" controller is blank, just a blue background.
The DetailViewController has a timerStarted boolean value that is true when the detail view is being used and false when it isn't. The master view is hidden when the detail is in use and displayed when it isn't.
Here's the relevant code (there's nothing interesting in AppDelegate as it's no longer a split view delegate, and MasterViewController has no interaction as the button works via the storyboard)
DetailViewController
import UIKit
class DetailViewController: UIViewController, UISplitViewControllerDelegate {
#IBOutlet weak var detailDescriptionLabel: UILabel!
var collapseDetailViewController = true
var detailItem: AnyObject? {
didSet {
self.configureView()
}
}
var timerStarted: Bool = false {
didSet {
self.changeTimerStatus()
}
}
func configureView() {
if let detail: AnyObject = self.detailItem {
if let label = self.detailDescriptionLabel {
label.text = detail.description
self.timerStarted = true
}
}
}
func changeTimerStatus() {
if self.timerStarted {
if splitViewController!.collapsed == false {
UIView.animateWithDuration(0.3, animations: {
self.splitViewController?.preferredDisplayMode = UISplitViewControllerDisplayMode.PrimaryHidden
})
}
collapseDetailViewController = false
} else {
if splitViewController!.collapsed {
self.splitViewController?.viewControllers[0].popToRootViewControllerAnimated(true)
} else {
UIView.animateWithDuration(0.3, animations: {
self.splitViewController?.preferredDisplayMode = UISplitViewControllerDisplayMode.AllVisible
})
}
collapseDetailViewController = true
}
}
override func viewDidLoad() {
super.viewDidLoad()
splitViewController?.delegate = self
self.disabledScreen.hidden = false
self.view.bringSubviewToFront(disabledScreen)
self.configureView()
}
override func viewWillAppear(animated: Bool) {
if splitViewController!.collapsed == false && self.timerStarted == false {
splitViewController?.preferredDisplayMode = UISplitViewControllerDisplayMode.AllVisible
}
}
#IBAction func closeButton(sender: AnyObject) {
self.timerStarted = false
}
func primaryViewControllerForExpandingSplitViewController(splitViewController: UISplitViewController) -> UIViewController? {
if timerStarted == true {
splitViewController.preferredDisplayMode = UISplitViewControllerDisplayMode.PrimaryHidden
} else {
splitViewController.preferredDisplayMode = UISplitViewControllerDisplayMode.AllVisible
}
return nil
}
func splitViewController(splitViewController: UISplitViewController, collapseSecondaryViewController secondaryViewController: UIViewController!, ontoPrimaryViewController primaryViewController: UIViewController!) -> Bool {
return collapseDetailViewController
}
}
AddItemViewController
import UIKit
class AddItemViewController: UIViewController, UISplitViewControllerDelegate {
var collapseDetailViewController = false
override func viewDidLoad() {
super.viewDidLoad()
self.splitViewController?.delegate = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func viewDidAppear(animated: Bool) {
self.splitViewController?.delegate = self
self.collapseDetailViewController = false
}
override func viewWillDisappear(animated: Bool) {
self.splitViewController?.delegate = nil
self.collapseDetailViewController = true
}
func primaryViewControllerForExpandingSplitViewController(splitViewController: UISplitViewController) -> UIViewController? {
return self
}
func primaryViewControllerForCollapsingSplitViewController(splitViewController: UISplitViewController) -> UIViewController? {
return nil
}
func splitViewController(splitViewController: UISplitViewController, collapseSecondaryViewController secondaryViewController: UIViewController!, ontoPrimaryViewController primaryViewController: UIViewController!) -> Bool {
return collapseDetailViewController
}
}
I'd be grateful for any suggestions.
I have found the answer. I read a article which I had originally missed because it focuses on changing the detail view rather than the master. As it turns out, the split view works better if I just manage the detail and then the master will take care of itself. Since I never want to change the detail I can simply add the following to my split view delegate:
func splitViewController(splitViewController: UISplitViewController, separateSecondaryViewControllerFromPrimaryViewController primaryViewController: UIViewController!) -> UIViewController? {
return (UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("detailView") as! UIViewController)
}
Once this is done, I no longer lose the navigation bar.
What is the preferred way of segueying between two viewcontrollers based on device orientation in iOS 8/9? I want to build an instrument that is shown in landscape. In portrait i want to show the settings of the App.
In AppDelegate.swift inside the "didFinishLaunchingWithOptions" function I put:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "rotated", name: UIDeviceOrientationDidChangeNotification, object: nil)
And then inside the AppDelegate class, put the following function:
func rotated(){
if(UIDeviceOrientationIsLandscape(UIDevice.currentDevice().orientation){
println("landscape")
}
if(UIDeviceOrientationIsPortrait(UIDevice.currentDevice().orientation)){
println("Portrait")
}
}
Or you can use this:
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
if UIDevice.currentDevice().orientation.isLandscape.boolValue {
println("landscape")
} else {
println("portraight")
}
}
Here is my mechanism I created for iOS 9, but this should also work for iOS 8 as well.
import UIKit
extension CGSize {
static func minMaxSize(size: CGSize) -> CGSize {
return CGSize(width: size.minValue(), height: size.maxValue())
}
static func maxMinSize(size: CGSize) -> CGSize {
return CGSize(width: size.maxValue(), height: size.minValue())
}
func minValue() -> CGFloat {
return min(self.width, self.height)
}
func maxValue() -> CGFloat {
return max(self.width, self.height)
}
}
extension UIScreen {
func isPortrait() -> Bool {
return self.bounds.width < self.bounds.height
}
}
class BaseViewController: UIViewController {
var horizontalConstraints = [NSLayoutConstraint]()
var verticalConstraints = [NSLayoutConstraint]()
private lazy var _isPortrait: Bool = UIScreen.mainScreen().isPortrait()
func isPortrait() -> Bool {
if let parentViewController = self.parentViewController as? BaseViewController {
return parentViewController.isPortrait()
}
return _isPortrait
}
override func loadView() {
self.view = UIView()
}
override func viewDidLoad() {
super.viewDidLoad()
self.addViewController()
self.addLayoutGuides()
self.setupLayoutGuides()
self.addSubViews()
self.setupSubViews()
}
func addViewController() {
/* override point */
}
func addLayoutGuides() {
/* override point */
}
func setupLayoutGuides() {
/* override point */
}
func addSubViews() {
/* override point */
}
func setupSubViews() {
/* override point */
}
private func applyConstraints() {
self.willApplyConstraints()
self.isPortrait() ? self.applyVerticalConstrains() : self.applyHorizontalConstrains()
self.didApllyedConstraints()
}
func willApplyConstraints() {
/* override point */
}
func applyVerticalConstrains() {
/* override point */
}
func applyHorizontalConstrains() {
/* override point */
}
func didApllyedConstraints() {
/* override point */
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
if let superView = self.view.superview {
if superView.isKindOfClass(UIWindow.self) && self.parentViewController == nil {
if CGSize.minMaxSize(size) == CGSize.minMaxSize(UIScreen.mainScreen().bounds.size) {
_isPortrait = size.width < size.height
}
}
}
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
self.applyConstraints()
}
}
At least I assume that the child viewController will never touch inherited private _isPortrait, because it is lazy. Only the top level view can of the rootViewController is allowed to set this property. The super viewWillTransitionToSize function is called after the check is done. So if the value was set on the rootViewController, all child viewController will have the correct value at the time their viewWillTransitionToSize function is called. This has to be done, because you can not rely on the the size of the child viewController views, but at least you can trust your `rootViewController if it is fullscreen. Hope that will help someone somehow.
So my RootViewController may look like this now.
class RootViewController: BaseViewController {
override func addViewController() {
/* add child view controller here so they can receive callbacks */
}
override func addSubViews() {
/* add your subviews here in a way you like so you can play with AutoLayout as you wish */
}
override func setupSubViews() {
/* setup up your views here and always activate vertical constraints here */
/* because iOS will always initialize in portrait even if launched in landscape */
/* deactivate your horizontal constrains and put them inside the array */
/* add vertical constraints to the vertical array; other constraints can be active */
/* without the need to be added somewhere */
}
override func willApplyConstraints() {
/* deactivate all constrains */
NSLayoutConstraint.deactivateConstraints(self.verticalConstraints)
NSLayoutConstraint.deactivateConstraints(self.horizontalConstraints)
}
override func applyVerticalConstrains() {
NSLayoutConstraint.activateConstraints(self.verticalConstraints)
}
override func applyHorizontalConstrains() {
NSLayoutConstraint.activateConstraints(self.horizontalConstraints)
}
override func didApllyedConstraints() {
/* force layouting if needed */
self.view.layoutIfNeeded()
}
}
UPDATE (Xcode 7 - OS X 10.11.1 beta): For iPad testing use iPad 2 or iPad Retina simulator, because the Air 1/2 are broken with AutoLayout. All simulators will produce this message: <CATransformLayer: 0x790af320> - changing property masksToBounds in transform-only layer, will have no effect