Showing/Hiding primary view controller of UISplitViewController shifts detail view - ios

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.

Related

How to properly use didSelectRowAt in splitViewController on compact device in swift?

I'm using a splitViewController to display a master view and a detail view.
When I tap on a row, the detail view updates correctly.
Then, when I'm in portrait view, I collapse the splitview detail view, so that that master list items are shown as follows:
And when I tap on a row, I correctly move to the detail view, as shown:
The problem I'm having is that if I rotate the device in the detail view shown above, while I'm in the detail view, the rotation correctly goes back to the splitView, however, now when I select a row, the delegate method does not update the detail view. It only seems to work if I start out in the splitView and stay in that view, or if I start out in the collapsed view and stay in that. If I rotate, then the delegate method does not seem to work.
I found a prior post, which shows how to use the delegate method to update the detail view using objective C code, using the didSelectRow function. I tried to duplicate this code with the following swift code:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let navigationVC = UINavigationController()
var detailVC = TestsDetailAdvertVC()
if let tests = controller.fetchedObjects, tests.count > 0 {
//if there is, keep track of the test which is selected
selectedTest = tests[indexPath.row]
if let isCollapsed = splitViewController?.isCollapsed {
if isCollapsed {
//solves problem of navigating to the detail view in compact view
// on the iPhone (compact) the split view controller is collapsed
// therefore we need to create the navigation controller and detail controller
detailVC = self.storyboard!.instantiateViewController(withIdentifier: "detailVC") as! TestsDetailAdvertVC
navigationVC.setViewControllers([detailVC], animated: false)
self.splitViewController?.showDetailViewController(detailVC, sender: self)
detailVC.testToEdit = selectedTest
} else {
// if the split view controller shows the detail view already there is no need to create the controllers
// so we just pass the correct test using the delegate
// if the test variable is set, then it calls the showDetail function
delegate?.testToEdit = selectedTest
}
}
}
}
I think that somehow when the one or the other method is used to update the detail view it works, but then when it switches back and forth, it stops working. I wonder if anyone has solved this issue using swift code who could point me to an example.
Note: After some additional searching, I realized that there are a few of delegate methods for the splitViewController, including:
func primaryViewControllerForExpandingSplitViewController:
and
func primaryViewControllerForCollapsingSplitViewController:
and
splitViewController:separateSecondaryViewControllerFromPrimaryViewController:
I've been fiddling around with these methods, but so far haven't been able to get them to work, and I haven't found any posts that show examples of how they are used.
Thanks.
I figured out how to make the detail view update properly, using an answer from a prior post at:
In UISplitViewController, can't make showDetailViewController:sender: push onto detail navigationController
my code to solve the problem is updated using swift code:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
var detail = UINavigationController()
var testVC = TestsDetailAdvertVC()
if let tests = controller.fetchedObjects, tests.count > 0 {
//if there is, keep track of the test which is selected
selectedTest = tests[indexPath.row]
if let isCollapsed = splitViewController?.isCollapsed {
if isCollapsed {
//in collapsed view, the correct detail view controller is not
//yet substantiated, so we need to substantiate it
testVC = self.storyboard?.instantiateViewController(withIdentifier: "detailVC") as! TestsDetailAdvertVC
detail.setViewControllers([testVC], animated: true)
testVC.testToEdit = selectedTest
} else {
//in expanded view, the correct view controller needs
//to be identified, using the appropriate view controller for
//the splitview controller
let vc = self.splitViewController?.viewControllers[1]
//which is a navigation controller
if vc is UINavigationController {
detail = vc as! UINavigationController
//which we then use to identify the correct detail view
testVC = detail.viewControllers.first as! TestsDetailAdvertVC
testVC.testToEdit = selectedTest
}
}
}
}
self.splitViewController?.showDetailViewController(detail, sender: self)
}
The key solution is that on the collapsed splitviewcontroller, the detail view has to be instantiated form the storyboard. However, on the expanded splitviewcontroller, the detail view has to come from the expanded navigation controller. Then when I rotate the correct detail view controller updates correctly.

SwipeNavigationController framework from GitHub how to implement

so my question today is how can I add a view similar to Snapchat where you use gestures to swipe to other views: left, top, bottom, right. I'm trying to use this framework but I'm not sure how to really implement it as they haven't provided a sample project. The layout of my app is I have a signup/login view controller, from there I want it to go to another view (blue) and that's the view that I want to have the different gestures mentioned above. `import UIKit
import SwipeNavigationController
class BlueViewController: UIViewController {
let orangeVC = OrangeView()
let pinkVC = PinkView()
let greenVC = GreenView()
let purpleVC = PurpleView()
override func viewDidLoad() {
let swipeNavigationController = SwipeNavigationController(centerViewController: self)
swipeNavigationController.topViewController = self.pinkVC
swipeNavigationController.bottomViewController = self.purpleVC
swipeNavigationController.leftViewController = self.greenVC
swipeNavigationController.rightViewController = self.orangeVC
view.backgroundColor = UIColor.facebookBlueColor
}
}'
I'm also not using storyboards for this project.
The SwipeNavigationController is a UIViewController and can be pushed onto a stack or presented the same as any other UIViewController. Wherever you are creating and presenting BlueViewController, you should instead create the SwipeNavigationController as the top level object that contains BlueViewController and all of the direction view controllers. BlueViewController and all of the other directions should not know anything about the SwipeViewController. BlueViewController and all of the other directions should not know anything about each other. The SwipeNavigationController is the top level view controller, all of the view controllers associated with a direction are child view controllers of it. I'm assuming that you have a navigation controller somewhere in your flow that pushes the SwipeNavigationController. In that case, you would have something like this in whatever method you want to trigger the push. I've called it nextTapped, but I'm sure it'll be something different in your code:
func nextTapped() {
let swipeNavigationController = SwipeNavigationController(centerViewController: BlueViewController())
swipeNavigationController.topViewController = PinkViewController()
swipeNavigationController.bottomViewController = PurpleViewController()
swipeNavigationController.leftViewController = GreenViewController()
swipeNavigationController.rightViewController = OrangeViewController()
navigationController?.pushViewController(swipeNavigationController, animated: true)
}
And then remove everything from viewDidLoad in BlueViewController except for the line that sets the background color. This creates the SwipeNavigationController with all of the directional view controllers, keeping the BlueViewController as the center and then pushes it onto your view controller stack. If you don't have a UINavigationController in the view controller that is displayed before the SwipeNavigationController, you can present it modally by replacing the last line with this:
present(swipeNavigationController, animated: true, completion: nil)

Passing the Boolean Flag between the views in swift

I have a single view controller containing the some text labels and multiple other properties. I want to use that view controller for both editing the view and viewing the contents of view. Now what I want to do is pass the Flag so that it indicates whether the request is for editing the fields or viewing the fields.
I have done this but did not work. Lets say my view controller containing the view is third View Controller and I am accessing this view controller from first and second view controller.
//In third View COntroller
var isEdit: Bool! = false
func viewDidLoad(){
self.loadData()
}
override func loadData{
if isEdit == false{
//print this is edit mode
}
else if isEdit == true{
//This is view mode
}
else{
//print error navigtion
}
}
and I am accessing to this view controller from first view controller on button click action as
let mapViewFirst = self.storyboard?.instantiateViewControllerWithIdentifier("ThirdViewController") as? ThirdViewController
mapViewFirst.isEdit == true
self.navigationController?.pushViewController(mapViewFirst!, animated: true)
and from secondViewController as
let mapViewSecond = self.storyboard?.instantiateViewControllerWithIdentifier("ThirdViewController") as? ThirdViewController
mapViewSecond.isEdit == false
self.navigationController?.pushViewController(mapViewSecond!, animated: true)
it always runs on isEdit == false
i.e the view controller is always on Edit mode it never moves to else condition. Can anyone find the better solution to my issue.
Assuming the difference between isEdit and iseditMode is a typo in your question and not in your actual code, and also that the difference between mapViewFirst and mapViewControllerObj and mapViewSecond and mapViewControllerObj are also just typos in your question and not your actual code.
Then it is not working because viewDidLoad() is called when the view controller is loaded into memory, which is occurring when instantiateViewControllerWithIdentifier is called.
To get the functionality you want move loadData() from viewDidLoad to viewWillAppear which will get called when the view controller is pushed to the stack. (and make sure you override viewWillAppear as you are supposed to, not like you have omitted from viewDidLoad())

Swift: how to detect if UISplitViewController is currently showing 1 or 2 controllers?

How can I detect if the UISplitViewController is currently just showing 1 view controller or it's in dual-pane with 2 views controllers shown side-by-side?
The split view controller reflects the actual display mode in the displayMode property:
AllVisible: The primary and secondary UIViewControllers are displayed side-by-side.
PrimaryHidden: The primary UISplitViewController is hidden.
PrimaryOverlay: The primary UISplitViewController overlays the secondary, which is partially visible.
When the isCollapsed property is true, the value of displayMode property is ignored. A collapsed split view interface contains only one view controller so the display mode is superfluous.
Resume: To find out the detailed situation on screen use isCollapsed property and (if isCollapsed = false) displayMode property.
Here is a simple case:
You are on the MasterViewController and you select a cell. Now, depending if the UISplitViewController is collapsed or not you want to either perform a segue (circled in red)
to the DetailViewController (collapsed) or update the DetailViewController (not collapsed).
In your "didSelectRowAtIndexPath" method on your MasterViewController get a reference to the UISplitViewController and choose what to do like this:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
//Reference to Split View
guard let splitView = self.splitViewController else {
return
}
//Check the collapsed property
if splitView.collapsed {
self.performSegueWithIdentifier("segueToDetail", sender: self)
}else {
//Get reference to your details navigation controller
guard let detailViewNavigationController = self.splitViewController?.viewControllers[1] as? UINavigationController else {
return
}
//Get a reference to your custom detail view controller
guard let detailController = detailViewNavigationController.viewControllers[0] as? MyCustomViewController else {
return
}
//Call your custom function to update the detail view controller
detailController.updateSomething()
}
}
If you don't want to use the "collapsed" property of the UISplitViewController you can check the number of view controllers property like this.
if splitView.viewControllers.count == 1 {
self.performSegueWithIdentifier("segueToDetail", sender: self)
}else splitView.viewControllers.count == 2 {
guard let detailViewNavigationController = self.splitViewController?.viewControllers[1] as? UINavigationController else {
return
}
guard let detailController = detailViewNavigationController.viewControllers[0] as? MyCustomViewController else {
return
}
detailController.updateSomething()
}
Another option is to set up delegation from your master view controller to your detail view controller. This will work well if you don't want to have to reach up the view controller chain like this example does. Here is a tutorial on this method. Note the "Hooking Up The Master With the Detail" section.
Just a note: I tested switching on the UISplitViewControllers "displayMode" property. This property did not give me enough info to figure out what to do. The reason is that the property is set to .AllVisible when you are in the horizontal compact mode and the horizontal expanded mode.
Last, before I go. I like the way I do it because lots of times you know you are going to need a UISplitViewController so you create a project from the template. You will notice the template comes with the segue set up. This template is great for phones but doesn't cut it for iPads and iPhone6+'s. If you drag and drop a UISplitViewController onto a story board after project creation you will notice the detail view is neither imbedded in a UINavigationController nor is there a segue from the master to the detail. Just more to set up I guess!
There is a property of UISplitViewController named 'collapsed'.

UISplitViewController's preferredDisplayMode: incorrect behavior

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

Resources