Split View controller occasionally showing detail view on iPhone on iOS 13 - swift

We have a universal app with Split view controllers which are embedded within the different tabs. We're observing that on iOS 13 on iPhone, and while switching tabs, the detail view is showed instead of the master view occasionally. We have not been able to single out the pattern and this just happens randomly, but frequently.
I have already referred to UISplitViewController in portrait on iPhone shows detail VC instead of master and
Open UISplitViewController to Master View rather than Detail and we're implementing the delegate for SplitViewController. This delegate also gets called.
class AppSplitViewController: UISplitViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.delegate = self
self.preferredDisplayMode = .allVisible
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
}
*/
}
// MARK: - UISplitViewControllerDelegate Methods
extension AppSplitViewController: UISplitViewControllerDelegate {
func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {
return true
}
}
I am not sure if other people have observed this on iOS 13 as well or not, but I am not sure why iOS would present detail view occasionally even though we have the proper delegate implementation. Please note that we have not been able to reproduce this on iOS 12. Here is view stack
https://imgur.com/eWO3RQM

Set the UISplitViewController delegate again in awakeFromNib. There appears to be some order of operations issue, from iOS 13 to at least 13.3. I had the exact same issue, and implemented this tip as a result of another answer, which seems to be working.

The below newly introduced method could be helpful if you are using iOS 14 or above
func splitViewController(_ svc: UISplitViewController, topColumnForCollapsingToProposedTopColumn proposedTopColumn: UISplitViewController.Column) -> UISplitViewController.Column {
return .primary
}

Related

swift xcode iOS: can I re-use a loaded modal fullscreen view controller?

I have a storyboard with two view controllers. First one, VC_1, has one button that opens 2nd one - VC_2.
VC_2 also has a button that opens VC_1.
Both controllers have almost identical code:
class VC_1: UIViewController
{
override func viewDidLoad()
{
super.viewDidLoad()
print(“VC_1 loaded")
}
override func viewDidAppear(_ animated: Bool){ print(“VC_1 appeared") }
override func viewDidDisappear(_ animated: Bool){ print(“VC_1 disappeared") }
#IBAction func btnShowVC_2(_ sender: UIButton)
{
let storyboard = UIStoryboard(name: "Main", bundle: nil)
secondVC = storyboard.instantiateViewController(identifier: “VC_2”)
secondVC.modalPresentationStyle = .fullScreen
show(secondVC, sender: self)
}
}
The difference is only in "VC_2" instead of "VC_1" in the 2nd controller code.
I have seen this View Controller creation code in Apple documentation and many other examples around the Internet.
When I press the button on the VC_1, I see in the debug window, that VC_2 is loaded and appeared, and VC_1 is disappeared. And same, of course, happens when I press the button on VC_2 - it disappears, and VC_1 is loaded again.
My questions are:
what happens with View Controller object after "viewDidDisappear" has been called? Does it really disappear from memory, or "disappear" only means "you cannot see it on the screen?". I do not see "viewDidUnload" in the documentation...
I suppose that "viewDidLoad" means that new View Controller object was created in memory. Is there any way to load the View Controller object only once, and then hide and show it without causing "viewDidLoad" to be called? I tried to do it with global variable "secondVC" but got "Application tried to present modally an active controller" error.
viewDidDisappear: called after the view is removed from the windows’
view hierarchy. No, View controller object just left the view property. By the way the amount of memory used by view controllers is negligible. So dont think about too much. If you want to catch when Your View controller object release from the memory put
deinit { print("vc deallocated") }
viewDidUnload, it has been deprecated since the iOS
6, which used to do some final cleaning.
Partly true. Keep in mind ViewDidload called one time for the life cycle of view controller. There is a method called before viewdidload but this is not related with your question.
In addition to "There is a method before viewdidload" -> loadView( ) is a method managed by the viewController. The viewController calls it when its current view is nil. loadView( ) basically takes a view (that you create) and sets it to the viewController’s view (superview).

Accessing UINavigationController from rootVC Subview (subview loaded from Nib)

The main ViewController is embedded in a UINavigationController subclass, and the VC has a subview that is loaded from a nib. The subview is called MenuView, and contains UIButtons that will link to other VCs.
To keep my main ViewController less unruly, I have put all these buttons into a subview that loads from a nib that animates the menu opening and closing.
However, I would like to present other view controllers from these, sometimes "Modally", sometimes "Show". What I have done seems to work, but I just want to know if this is alright, or if I have caused some unwanted effects that I'm unaware of (like a strong reference cycle that would cause a memory leak, or something). Or is there a better way to do this?
Some code:
In MenuView.swift
class MenuView: UIView {
var navigationController = CustomNavigationController()
func combinedInit(){
NSBundle.mainBundle().loadNibNamed("MenuViewXib", owner: self, options: nil)
addSubview(mainView)
mainView.frame = self.bounds
}
#IBAction func optionsAction(sender: AnyObject) {
self.navigationController.performSegueWithIdentifier("presentOptions", sender: self)
}
In ViewController.swift
menuView.navigationController = self.navigationController as! CustomNavigationController
Short answer: No, it is not alright to access a view controller from within some view in the hierarchy, because that would break all the MVC rules written.
UIView objects are meant to display UI components in the screen and are responsible for drawing and laying out their child views correctly. That's all there is. Nothing more, nothing less.
You should handle those kind of interactions between views and controllers always in the controller in which the view in question actually belong. If you need to send messages from a view to its view controller, you can make use of either the delegate approach or NSNotificationCenter class.
If I were in your shoes, I would use a delegate when view needs some information from its view controller. It is more understandable than using notification center as it makes it much easier to keep track of what's going on between. If the view controller needs some information from a view (in other words, the other way around), I'd go with the notification center.
protocol MenuViewDelegate: class {
func menuViewDidClick(menuView: MenuView)
}
class MenuView: UIView {
var weak delegate: MenuViewDelegate?
#IBAction func optionsAction(sender: AnyObject) {
delegate?.menuViewDidClick(self)
}
}
Let's look at what's going on at the view controller side:
class MenuViewController: UIViewController, MenuViewDelegate {
override func viewDidLoad() {
...
self.menuView.delegate = self
}
func menuViewDidClick(menuView: MenuView) {
navigationController?.performSegueWithIdentifier("presentOptions", sender: self)
}
}
For more information about communication patterns in iOS, you might want to take a look at this great article in order to comprehend how they work.

How to detect view controller dismissed or not

In iOS app, there may be several view controllers. They may perform segues from one to another. the question is how to detect each view controller about whether it is dismissed or not when implementing segue. Thanks.
You have access to:
override func viewWillDisappear(animated: Bool) {
}
override func viewDidDisappear(animated: Bool) {
}
// Called when the view controller will be removed from memory.
deinit {
}
Which can help you managed things based on that state of a view controller.
I'm not sure if you can detect whether or not it was dismissed, but you can set a variable "viewControllerDismissed = true" in performSegueWithIdentifier that will be detected in the VC behind the one being dismissed.

unwind segue result different with master and detail views

i'm fairly new to swift and having some difficulty with unwind segues in a master detail application.
the unwind seems to work fine dismissing my popovers on the iPhone, however when i try the same thing on the iPad, the popover remains.
If i add a dismissViewControllerAnimated to the presenting viewController's unwind handling action, then the iPad version works fine and dismisses the popover, however the iPhone version dismisses the popover and then dismisses the view that presented the popover. i.e. dismisses two views.
I have worked out that the problem is that popover's are automatically dismissed with an unwind when presented modally such as on an iPhone. However when presented as true popovers they don't dismiss with an unwind segue. Could somebody help me figure out how to manage both cases so that only the popover will be dismissed. Thank you very much in advance.
Okay. after a long time working on this i managed to come up with a solution. i used a popoverpresentationcontroller and declared the presenting controller as the delegate. by then adding an additional function forcing the iPhone to use the popover in lieu of the modal presentation, the presentation and dismissal is consistent for the iPhone and iPad. the code is below. I just used a generic UIViewController in the if let vc statement so that I could use this with a popover that's imbedded in a navigation controller also.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let identifier = segue.identifier {
switch identifier {
case "My Segue Identifier" :
if let vc = segue.destinationViewController as? UIViewController {
if let ppc = vc.popoverPresentationController {
ppc.delegate = self
}
}
default: break
}
}
}
additionally you need to add the following function to prevent the modal presentation on the iPhone:
func adaptivePresentationStyleForPresentationController (controller:UIPresentationController)-> UIModalPresentationStyle {
return UIModalPresentationStyle.None
}

UISplitViewController in portrait on iPhone always show master and detail in iOS 8

UISplitViewController in portrait on iPhone always show master and detail in iOS 8
I try to subclass UISplitViewController and config it to show master and detail at the same time. but no any effect.
class APPSplitViewController: UISplitViewController, UISplitViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
preferredDisplayMode = UISplitViewControllerDisplayMode.AllVisible
maximumPrimaryColumnWidth = 32.0
minimumPrimaryColumnWidth = 32.0
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
How to?
UISplitViewController use show side-by-side only in horizontally regular environment (new TraitCollection size)
The split view controller determines the arrangement of its child view controllers based on the available space. In a horizontally regular environment, the split view controller presents its view controllers side-by-side whenever possible. In a horizontally compact environment, the split view controller acts more like a navigation controller, displaying the primary view controller initially and pushing or popping the secondary view controller as needed. You can also ask the split view controller to prefer a specific arrangement by assigning a value to the preferredDisplayMode property.
Solution.
You have to change TraitCollection of SplitViewController. How do this:
Create a ViewController and add your SplitViewController as a child.
Override traitCollection with size .Regular for your child (UISplitViewController)
ViewController Wrapper
class TraitOverrideViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
configureSplitVC()
}
private func configureSplitVC() {
let splitVC = self.childViewControllers[0] as UISplitViewController
setOverrideTraitCollection(UITraitCollection(horizontalSizeClass: .Regular), forChildViewController: splitVC)
}
}
In iOS 8 UISplitViewController uses Adaptive User Interfaces and TraitCollections to show its content.
It shows different style depending on the size of and type of the view. You can change by technic I explain above.
You can get more info in WWDC video about Building Adaptive Apps with UIKit