UISplitViewController's preferredDisplayMode: incorrect behavior - ios

I have a master-detail application (I created it using the Xcode template, and then I modified it a bit), and I'm trying to set the preferredDisplayMode property of UISplitViewController to obtain this behavior:
UISplitViewControllerDisplayMode.PrimaryOverlay: The primary view controller is layered on top of the secondary view controller, leaving the secondary view controller partially visible.
So the master view controller should initially be on top of the detail view controller, and it should be possible to dismiss it. I change this property in application:didFinishLaunchingWithOptions:, that's the full code:
// Inside application:didFinishLaunchingWithOptions:
// Override point for customization after application launch.
let rootViewController = window!.rootViewController as! UINavigationController
// The root view controller is a navigation controller that contains the split view controller
let splitViewController = rootViewController.viewControllers[0] as! UISplitViewController
let navigationController = splitViewController.viewControllers[splitViewController.viewControllers.count-1] as! UINavigationController
navigationController.topViewController.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem()
splitViewController.delegate = self
splitViewController.preferredPrimaryColumnWidthFraction = 0.4
splitViewController.maximumPrimaryColumnWidth = 600
splitViewController.preferredDisplayMode = .PrimaryOverlay
return true
I have two problems: first, that's not the behavior that I obtain. The master view controller is hidden when the application launches, and if I click on the left bar button item to show the master, it rapidly appears and then disappears again. If I click it another time it appears without disappearing.
Second, I get a warning in the console:
2015-06-30 12:06:26.613 Presidents[29557:857547] Unbalanced calls to begin/end appearance transitions for <UINavigationController: 0x7b8be610>.
But I have no transitions in my code.
PS: It's from the book "Beginning Phone Development with Swift" by D. Mark, J. Nutting, K. Topley, F. Olsson, J. LaMarche, chapter 11.

I got this working in my iPad app. In the Master View Controller:
override func viewDidLoad() {
super.viewDidLoad()
splitViewController?.delegate = self
let rect: CGRect = UIScreen.mainScreen().bounds
if rect.height > rect.width {
// am in portrait: trick to force the master view to open
self.splitViewController?.preferredDisplayMode = .PrimaryOverlay
}
Then later:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.splitViewController?.preferredDisplayMode = .Automatic
Now trying to find out how to do it with an iPhone app...
EDIT: ah, see this previous answer

Related

Swift and Xcode. All UIViewControllers become black when added to TabBarController

I'm trying to create UITabBarController programmatically, adding multiple NavigationControllers to it. When UITabBarController contains one NavigationController - everything works as expected (see image)
But when i add multiple NavigationControllers to UITabBarController each screen becomes black (see another image )
The same black screen is shown when switching between tabs 1, 2, 3, 4 and 5.
Here's the code how UITabBarController is created
class TabBarViewController : UITabBarController{
override func viewDidLoad() {
super.viewDidLoad()
let controllers = [HistoryViewController.self, StatsViewController.self, DashboardViewController.self, ExpenseManagerViewController.self, ProfileViewController.self]
var navControllers: [UINavigationController] = []
controllers.forEach{ ctrl in
navControllers.append(getController(from: ctrl))
}
tabBar.tintColor = Color.green
viewControllers = navControllers
}
private func getController<TType: UIViewController>(from type: TType.Type) -> UINavigationController{
let ctrl = TType()
let navCtrl = UINavigationController(rootViewController: ctrl)
let ctrlName = String.init(describing: type.self).replacingOccurrences(of: "ViewController", with: String.empty)
navCtrl.tabBarItem.title = ctrlName
navCtrl.tabBarItem.image = UIImage(named: ctrlName)
navCtrl.navigationBar.topItem?.title = ctrlName
return navCtrl
}
}
Those UIViewControllers are created using "add Cocoa Touch Class" option and have assigned *.xib files with some minimum design (see one more image)
Any help regarding why all screens become black when multiple (2 and more) NavigationControllers added to TabBarController would be highly appreciated.
Thanks
Clearly you forget how to init the UIViewControllers with xib file:
private func getController<TType: UIViewController>(from type: TType.Type) -> UINavigationController{
let ctrl = TType(nibName: String.init(describing: type.self), bundle: nil)
let navCtrl = UINavigationController(rootViewController: ctrl)
First if you come from any screen then dont insert navigationbar in between that viewcontroller and tabbarcontroller and when you jump to tabbarcontroller set as rootview controller and any tab you want to open than place navigation controller in between. means dont open tabbar controller with navigationbar heirarchy but when u want to open controller with tabs then place navigation controller in between.

How do I keep UI elements above a UIViewController and its presented ViewController?

I want to display some UI elements, like a search bar, on top of my app's first VC, and also on top of a second VC that it presents.
My solution for this was to create a ContainerViewController, which calls addChildViewController(firstViewController), and view.addSubview(firstViewController.view). And then view.addSubview(searchBarView), and similar for each of the UI elements.
At some point later, FirstViewController may call present(secondViewController), and ideally that slides up onto screen with my search bar and other elements still appearing on top of both view controllers.
Instead, secondViewController is presented on top of ContainerViewController, thus hiding the search bar.
I also want, when a user taps on the search bar, for ContainerViewController to present SearchVC, on top of everything. For that, it's straightforward - containerVC.present(searchVC).
How can I get this hierarchy to work properly?
If I understand correctly, your question is how to present a view controller on top (and within the bounds) of a child view controller which may have a different frame than the bounds of the parent view. That is possible by setting modalPresentationStyle property of the view controller you want to present to .overCurrentContext and setting definesPresentationContext of your child view controller to true.
Here's a quick example showing how it would work in practice:
override func viewDidLoad() {
super.viewDidLoad()
let childViewController = UIViewController()
childViewController.view.backgroundColor = .yellow
childViewController.view.translatesAutoresizingMaskIntoConstraints = true
childViewController.view.frame = view.bounds.insetBy(dx: 60, dy: 60)
view.addSubview(childViewController.view)
addChildViewController(childViewController)
childViewController.didMove(toParentViewController: self)
// Wait a bit...
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) {
let viewControllerToPresent = UIViewController()
viewControllerToPresent.modalPresentationStyle = .overCurrentContext // sets to show itself over current context
viewControllerToPresent.view.backgroundColor = .red
childViewController.definesPresentationContext = true // defines itself as current context
childViewController.present(viewControllerToPresent, animated: true, completion: nil)
}
}

Changing rootViewController for Nav causes UISplitViewController to show detail on Compact portrait orientation

I'm running into an issue where after changing the rootViewController on my UINavigationController and changing it back to my original UINavigationController, a UISplitViewController begins to show both it's master and detail view in a phone device on compact/portrait orientation (so not only on plus size phones, but also others).
Basic overview of architecture:
A TabBarController houses several tabs. One of these tabs is a UISplitViewController. I currently override the following to ensure that the MasterViewController is shown on compact orientations:
func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {
// this prevents phone from going straight to detail on showing the split view controller
return true
}
This works fine and displays the master on portrait as expected. At any point pressing a button on another tab can create a new UINavigationController instance and display it, in which I'm doing the below to change the rootViewController to the newly created UINavigationController to display:
let appDelegate = UIApplication.shared.delegate
appDelegate?.window??.rootViewController = newNavVC
On dismiss, I'm just swapping the UINavigationController back to the original one through the same code above. However, once I do this one time (create nav/display/dismiss), and I switch my tab back to the one with the UISplitViewController, it changes itself to show a side-by-side master detail view. I didn't know this was possible in portrait mode for compact sizing. I tried changing to any of the 4 preferred display modes in the UISplitViewController, but that didn't fix it.
Below is what it looks like (iPhone 6 simulator), am I missing delegates or misunderstanding collapsing?
Before:
After:
You can replace the the logic that assigned the rootViewController with the code snippet found at this link:
Leaking views when changing rootViewController inside transitionWithView
Basically you just create an extension for the UIWindow class that will set the root view controller correctly.
extension UIWindow {
/// Fix for https://stackoverflow.com/a/27153956/849645
func set(rootViewController newRootViewController: UIViewController, withTransition transition: CATransition? = nil) {
let previousViewController = rootViewController
if let transition = transition {
// Add the transition
layer.add(transition, forKey: kCATransition)
}
rootViewController = newRootViewController
// Update status bar appearance using the new view controllers appearance - animate if needed
if UIView.areAnimationsEnabled {
UIView.animate(withDuration: CATransaction.animationDuration()) {
newRootViewController.setNeedsStatusBarAppearanceUpdate()
}
} else {
newRootViewController.setNeedsStatusBarAppearanceUpdate()
}
/// The presenting view controllers view doesn't get removed from the window as its currently transistioning and presenting a view controller
if let transitionViewClass = NSClassFromString("UITransitionView") {
for subview in subviews where subview.isKind(of: transitionViewClass) {
subview.removeFromSuperview()
}
}
if let previousViewController = previousViewController {
// Allow the view controller to be deallocated
previousViewController.dismiss(animated: false) {
// Remove the root view in case its still showing
previousViewController.view.removeFromSuperview()
}
}
}

Navigation and Tab View controllers don't load correctly, then jump into place

When I try to present a TabViewController, I get odd behavior from both my TabBar and NavigationBar as seen in the images below. It stays as shown in the "before" image until I touch the screen or push a button. At the point it jumps to the "after" image.
Before:
After:
Code used to present the TabViewController:
let delegate = UIApplication.shared.delegate as! AppDelegate
delegate.tabViewController = TabViewController()
self.present(delegate.tabViewController!, animated: true, completion: nil)
Initialization of the TabViewController:
override func viewDidLoad() {
super.viewDidLoad()
let groupTable = GroupTableViewController()
let nav = UINavigationController(rootViewController: groupTable)
nav.title = "Groups"
nav.tabBarItem.image = UIImage(named: "groups")
let vc2 = MeViewController()
vc2.title = "Me"
vc2.tabBarItem.image = UIImage(named: "user")
// let vc3 = SettingsViewController
// vc3.title = "Settings"
// vc3.tabBarItem.image = UIImage(named: "settings")
self.viewControllers = [nav, vc2]
self.selectedIndex = 0
}
Console log, but I don't think the error is relevant:
objc[63765]: Class PLBuildVersion is implemented in both /Applications/Xcode-beta.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/PrivateFrameworks/AssetsLibraryServices.framework/AssetsLibraryServices (0x11916f998) and /Applications/Xcode-beta.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/PrivateFrameworks/PhotoLibraryServices.framework/PhotoLibraryServices (0x118069d38).
One of the two will be used. Which one is undefined.
This is a new bug I've been experiencing seemingly after updating to Xcode 8.1/MacOS Sierra.
My XCode version is Version 8.1 beta (8T47). Could this be a bug in the beta?
I'm unsure what is causing this as I didn't make a code change when this started happening.
Thanks for the help.
The viewDidLoad of the tab view controller is really too late to be configuring the tab view controller with its two child view controllers. Either do this in the "Code used to present the TabViewController", or, if you really want to do it from within the tab view controller itself, do it from the tab view controller's initializer. Then all will be well.

Showing/Hiding primary view controller of UISplitViewController shifts detail view

I have a UISplitViewController for Master/Detail functionality on an iPad. The master view controller shows a list of items, and when the user selects one the detail information is shown in the detail view controller. I have the detail view controller set to navigate to another view controller to display a graph. When this happens, I hide the primary view controller with the following lines in my prepareForSeque.
if let svc = self.splitViewController {
svc.preferredDisplayMode = .PrimaryHidden
}
This works great. When navigating back to the detail view from the graph view I would like to again show the primary view from the split view controller. I put this in viewWillAppear.
guard let svc = self.splitViewController else { return }
if svc.preferredDisplayMode != .Automatic {
svc.preferredDisplayMode = .Automatic
}
Again this works exactly as I would expect. The problem is the detail view changes size during this process and it is not laid out properly when returning from the graph view.
Here is a screen shot before navigating to the graph view, and before hiding the primary view of the UISplitViewController.
And this is after returning from the graph view.
My attempt to fix the issue was to force the detail view to layout itself. I tried the following:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
guard let svc = self.splitViewController else { return }
let detailNavController = svc.viewControllers[svc.viewControllers.count-1] as! UINavigationController
detailNavController.view.setNeedsLayout()
}
It sort of works but causes an ugly jump in the interface as the detail view appears and then a second later is relaid out. Is there a better way to get the view laid out properly before it is displayed?
I got it working, and figured I would share in case anybody else runs into the same issue. As Tim stated in the comments, it is a matter of running layout early enough in the chain to get things laid out before the view is presented on screen.
I did the following in the Graph view controller scene:
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
guard let svc = self.splitViewController else { return }
svc.preferredDisplayMode = .Automatic
}
This set the split view back to showing both the primary and secondary view controllers very early in the process.
The place where I was going wrong was the parent of my detail view controller is a UINavigationController. I assumed that since this was being sized wrong, I needed to get it to lay itself out again after setting the split view controller to show the primary and secondary views. This was a wrong assumption. What ended up working was going up one more level to the UISplitviewController and having that perform a layout on itself.
In my detail view controller I did the following:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
guard let svc = self.splitViewController else { return }
svc.view.setNeedsLayout()
svc.view.layoutIfNeeded()
}
This was early enough in the chain that all the layout gets completed before the view is shown, but late enough that the split view controller has the appropriate sizing information for showing both the primary and secondary views.
Hopefully this helps someone else, and thanks for the comments.

Resources