iOS 13 UISplitView Problems - swift

On iOS 13 Beta 5, I currently have problems with my UISplitView on iPhones.
My app starts with the detailsview off my splitview not with my masterview (look at the picture)
Does anyone know how i can fixed this problem under iOS 13? On iOS 12 everything works like a charm ☹️
Thx in advance Sebastian
Edit:
Sorry for the late answer I was on a short holiday trip without any internet :/
my Class looks like this:
class MyClass : UITableViewController, UISplitViewControllerDelegate, UIPickerViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
if (UIDevice.current.userInterfaceIdiom == .pad){
navigationController?.navigationBar.isTranslucent = false
}
/*SplitView*/
splitViewController?.preferredDisplayMode = .allVisible
splitViewController?.delegate = self
self.definesPresentationContext = true
}
// SplitView
func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {
return true
}
}
I think it's look like the normal procedure for this problem :/

I had the same issue.
After a bit of investigation it seems like viewDidLoad is too late to set it to all visible.
I subclassed the UISplitViewController and changed the setting in awakeFromNib method. Now it is working as expected.

Alxlives's answer helped me. By using the debugger, I notice that in the master view controller, viewDidLoad is not called. So the delegate is never set, thus the collapse method is never called.
I fixed this by setting the delegate in awakeFromNib():
override func awakeFromNib() {
self.splitViewController?.delegate = self
}
Now the splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool is called, and if you return true, the master will be displayed.

Warren Milward's answer helped me to guide me in the right direction, but actually I got it working with viewDidLoad().
I ended up using a subclass for the UISplitViewController and set the required values in viewDidLoad() as well as the delegate calls here.
class CustomSplitViewController: UISplitViewController, UISplitViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
self.preferredDisplayMode = .allVisible
}
func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {
return true
}
}

did you try this one (UISplitViewControllerDelegate):
self.preferredDisplayMode = .allVisible
&
func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {
return true;
}

Try MasterViewController_Instance.view.layoutIfNeeded() inside Splitviewcontroller ViewDidLoad() method. It fixed my problem.
class CustomSplitController: UISplitViewController{
override public func viewDidLoad() {
MASTER_CONTROLLER_INSTANCE.view.layoutIfNeeded()
//If you are using navigation controller in master controller try
MASTER_CONTROLLER_INSTANCE.navigationController?.view.layoutIfNeeded()
}
}

I set the spliteViewController's delegate on appdelegate, and it worked

For those who use storyboard and configure the new controller in a subclass from UIStoryboardSegue, it'll be simpler:
just before
[source presentViewController:destination animated:YES completion:nil];,
just set destination.modalPresentationStyle = UIModalPresentationFullScreen;, because the default is now UIModalPresentationPageSheet.

As many answers here leads to change in the Split View Controller starting sequence, I realized, that on compact size it performs vewDidLoad on detail controller ONLY. But awakeFromNib is called on every controller. So - I've just transfer my code from viewDidLoad to awakeFromNib and everything now is works perfectly!
override func awakeFromNib() {
super.awakeFromNib()
if let splitController = splitViewController {
splitController.delegate = self
}
}

Related

self.tabBarController.selectedIndex not calling viewDidAppear

I've been looking at and trying all the solutions others have posted to this problem, but I'm still not getting it.
My use case is very simple. I have a viewController with a button, when that button is pressed I want to navigate to another tab and reload the data including an api call.
When using the button, I navigate to the tab fine, but viewDidAppear is not being called.
If on another tab, and navigate using the tab bar, viewDidAppear works fine. Also viewWillAppear is working, but I have to add a manual delay to the functions I want to call so it's not ideal.
So what do I need to do to navigate using self.tabBarController.selectedIndex = 0 and get the functionality of viewDidAppear?
Update: The viewWillAppear method I added gets called but I have to add a delay to my functions in order for them to work, and it's a bit clunky, not ideal. Not sure why viewDidAppear will not work :(
Here is a screenshot of the structure:
I appreciate any help on this one!
The "current" ViewController is my tab index 2:
import UIKit
class PostPreviewVC: UIViewController {
//Here I create a post object and post it to the timeline with the below button
#IBAction func postButtonPressed(_ sender: Any) {
//create the post via Firebase api
self.tabBarController?.selectedIndex = 0
}
}
In my destination viewController:
import UIKit
import Firebase
import SDWebImage
import AVFoundation
class HomeVC: UIViewController {
// MARK: - PROPERTIES
var posts = [Post]()
let refreshControl = UIRefreshControl()
//more properties...
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
configureTableView()
reloadTimeline()
UserFirebase.timeline { (posts) in
self.posts = posts
self.tableView.reloadData()
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("viewDidAppear")
_ = self.view
setupUI()
configureTableView()
reloadTimeline()
UserFirebase.timeline { (posts) in
self.posts = posts
self.tableView.reloadData()
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("viewWillAppear")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.reloadTimeline()
self.configureTableView()
}
}
//All the tableview code below here...
}
Added a custom class for my tab bar controller:
import UIKit
class TabBarController: UITabBarController, UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("viewDidAppear in tabBar custom Class called")
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print("viewWillAppear in tabBar custom Class called")
}
}
When you are using UITabBarController, the method viewDidLoad will called only once when any UIViewController is loaded in memory. After that, when you are navigating the same UIViewController, then it will load from memory.
In order to overcome this problem, you must divide your code in viewDidLoad & viewDidAppear. So, in viewDidLoad, you only put that code which you want to intialize once throughout the app such as adding UIView's or other things, while in viewDidAppear / viewWillAppear, you can make API calls or some functions which fetches dynamic data.
Finally, when you are calling self.tabBarController.selectedIndex = 0, it will call viewDidLoad only once and viewDidAppear / viewWillAppear every time when you are navigating that UIViewController.
Hope this helps to understand like how UITabBarController works.
For UITabBarController viewDidLoad only gets called once. and your viewWillAppear and viewDidAppear get called multiple times. you can either check if your viewWillAppear gets called or not. because your view will appear gets called before your viewDidAppear it's just like going through the reverse engineering process.
You can also add viewDidAppear method into your UITabBarController custom class. and call its superclass method into it in that way I think it will solve your problem.
Note: In the case of UITabbarController, Always do your UI update task and API calling a task in either
viewWillAppear or viewDidAppear

Why isn't my PageViewController's DataSource working correctly?

I have created a simple storyboard with a PageView Controller.
I will flip between two other View Controllers.
My PageView Controller is a custom class TutorialPageViewController. I've also created a custom DataSource class.
In the DataSource class I expect the pageViewController methods to be called when I attempt to scroll. However this is not the case. I have break points at both methods and they are never called.
The first view controller, "Page the first" comes up correctly, but attempting to scroll around doesn't call the methods, so I can't use them yet (hence they return nil for now).
If I set the DataSource of my view controller to self and put the methods in there, they are called correctly. But I want to move the methods out to a separate class for better code management. So why doesn't it work?
I've tried
Setting my DataSource class to be a UIScrollViewDelegate as well as a UIPageViewControllerDelegate and setting the view controller's delegate to be the DataSource
The PageView's transition style is Scroll
class TutorialPageViewController : UIPageViewController {
override func viewDidLoad() {
reset()
}
func reset() {
let dataSource = TutorialPageDataSource(storyBoard: storyboard!)
let content = dataSource.firstContentViewController
self.dataSource = dataSource
self.setViewControllers([content], direction: .forward, animated: true, completion: nil)
}
}
class TutorialPageDataSource : NSObject, UIPageViewControllerDataSource {
private var _storyboard: UIStoryboard
var firstContentViewController: UIViewController
var secondContentViewController: UIViewController
init(storyBoard: UIStoryboard) {
_storyboard = storyBoard
firstContentViewController = _storyboard.instantiateViewController(withIdentifier: "FirstContentViewController")
secondContentViewController = _storyboard.instantiateViewController(withIdentifier: "FirstContentViewController")
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
// break point here never reached
return nil
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
// break point here never reached
return nil
}
}
UIPageViewController dataSource (and delegate) are weak.
You create your TutorialPageDataSource instance in the reset method, assign it to the weak dataSource, and then the TutorialPageDataSource instance goes out of scope and gets deallocated because there is no strong reference to it any more. So now the page view controller's dataSource becomes nil.
You need to keep a strong reference to the TutorialPageDataSource instance. Use an instance variable to keep the reference.

Opening a side menu on click on a TAB bar in SWIFT

I want to make something like below snapshots.
When I click on profile tab bar instead of opening a new view controller it shows a side menu. Is it something that has been handled on click of tabbar ?
if you want to achieve something like your screenShot then you are using a wrong library, because when you show your right viewController the front viewController go to left by amount of width of your right viewController, but anyways here is the code for what you need to do
first you need to put your viewController as delegate of your TabBarViewController and in func tabBarController(tabBarController: UITabBarController, shouldSelectViewController viewController: UIViewController) -> Bool you need to return false and call the method of SWRevealViewController to show right viewController rightRevealToggleAnimated(true)
class FirstViewController: UIViewController,SWRevealViewControllerDelegate,UITabBarControllerDelegate {
#IBOutlet weak var sliderControl: UISlider!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.revealViewController().delegate = self
self.tabBarController?.delegate = self
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
//self.view.removeGestureRecognizer(self.revealViewController().panGestureRecognizer())
//self.view.addGestureRecognizer(self.revealViewController().panGestureRecognizer())
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tabBarController(tabBarController: UITabBarController, shouldSelectViewController viewController: UIViewController) -> Bool {
//checking for specific viewController
if(viewController is DesiredViewControllerClass) {
self.revealViewController().rightRevealToggleAnimated(true)
}
return false
}
}
I hope this helps you, regards
You can use the tab bar delegate:
extension ViewController: UITabBarDelegate {
func tabBar(tabBar: UITabBar, didSelectItem item: UITabBarItem) {
// Present hamburger menu
}
}

interactivePopGestureRecognizer corrupts navigation stack on root view controller

In my UINavigationController I added custom back buttons with the side effect that it is not possible anymore to swipe left to right to pop the view controller and navigate back.
So I implemented interactivePopGestureRecognizer in my custom UINavigationController class:
class UINavigationControllerExtended: UINavigationController, UIGestureRecognizerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
if self.respondsToSelector(Selector("interactivePopGestureRecognizer")) {
self.interactivePopGestureRecognizer?.delegate = self
}
}
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailByGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return gestureRecognizer.isKindOfClass(UIScreenEdgePanGestureRecognizer)
}
}
This works fine except when I am in my root view controller (RVC) which is a UICollectionViewController, i.e. the most bottom view controller in the navigation stack. When I do the swipe left to right gesture, nothing seems to happen, as expected. But when I then tap a UICollectionViewCell the destination view controller (DVC) does not get pushed over the RVC. Instead I only see the DVC's shadow on the right side of the screen.
The RVC is not responsive anymore, but as I swipe left to right again, the DVC interactively moves right to left into the screen. When I finish the gesture, the DVC moves completely into the screen just to quickly disappear left to right again. The RVC becomes responsive again.
So it seems the DVC gets pushed onto the navigation stack but not visibly into the screen.
Any suggestions where this strange behaviour originates?
Implement UINavigationControllerDelegate for your navigation controller and enable/disable the gesture recognizer there.
// Fix bug when pop gesture is enabled for the root controller
func navigationController(navigationController: UINavigationController, didShowViewController viewController: UIViewController, animated: Bool) {
self.interactivePopGestureRecognizer?.enabled = self.viewControllers.count > 1
}
Keeping the code independent from the pushed view controllers.
My current solution is to disable the interactivePopGestureRecognizer in the root view controller:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.navigationController?.interactivePopGestureRecognizer?.enabled = false
}
In the first child view controller I enable it again. But this seems to be more a workaround because I don't understand the actual problem why the navigation stack got messed up in the first place.
Here is my solution for this. Swift 4.2
Implement UIGestureRecognizerDelegate and UINavigationControllerDelegate protocols. In my case I did this in an UITabBarController that would be also my root view controller.
Then, on viewDidLoad, do:
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.interactivePopGestureRecognizer?.delegate = self
self.navigationController?.delegate = self
}
Then, add the delegate method for UINavigationControllerDelegate and check if it is the root view by counting the number of view on the navigation controller and disable it if it is or enable it if its not.
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
let enable = self.navigationController?.viewControllers.count ?? 0 > 1
self.navigationController?.interactivePopGestureRecognizer?.isEnabled = enable
}
Lastly, add the UIGestureRecognizerDelegate method
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
This should fix the problem without the necessity of manually enabling/disabling the gesture recogniser in every view of your project.
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
self.interactivePopGestureRecognizer?.isEnabled = self.viewControllers.count > 1
}
Updated #rivera solution for Swift 5
Solution from Guilherme Carvalho worked for me, but I had to assign delegate methods in viewWillAppear, in viewDidLoad was too late for my implementation.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.interactivePopGestureRecognizer?.delegate = self
navigationController?.delegate = self
}
Try this code
func navigationController(navigationController: UINavigationController, didShowViewController vc: UIViewController, animated: Bool) {
self.interactivePopGestureRecognizer?.enabled = self.vc.count > 1
}

Pop to root view when tab is selected

I've been having some trouble with something I thought might be easy. I have a table in my root view controller, when a row is selected I push a new view and from there I go to another tab.
My question is how do I make sure that as soon as the user taps the first tab the navigation controller will pop to root?
Following delegate is called while each tab is selected on tabbar.
-(void) tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController
Put following code inside this delegate method.
if ([viewController isKindOfClass:[UINavigationController class]])
{
[(UINavigationController *)viewController popToRootViewControllerAnimated:NO];
}
its working fine on my app.
For Swift lovers:
import UIKit
class YourTabBarControllerHere: UITabBarController,
UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self;
}
func tabBarController(tabBarController: UITabBarController,
didSelectViewController viewController: UIViewController) {
if let vc = viewController as? UINavigationController {
vc.popViewControllerAnimated(animated: false);
}
}
}
Edit: Swift 3 update, thanks to #Justin Oroz for pointing that out.
In Swift 3.1
Add UITabBarControllerDelegate to your TabBar Class:
class YourClass: UITabBarController, UITabBarControllerDelegate {
After:
override func tabBar(tabBar: UITabBar, didSelectItem item:
UITabBarItem) {
let yourView = self.viewControllers![self.selectedIndex] as! UINavigationController
yourView .popToRootViewControllerAnimated(false)
}
What you are trying to do sounds a little bit odd. Have you read the Human Interface Guidelines on combining UINavigationControllers and UITabBarControllers?
However, what you need to do is detect the selection of the tab by setting a delegate for your UITabBarController and implementing the tabBarController:didSelectViewController: delegate method. In this method you need to pop back to the root view controller using UINavigationController's popToRootViewControllerAnimated: method.
Swift 5.1 Answer:
class YourTabBarName: UITabBarController, UITabBarControllerDelegate
{
override func viewDidLoad()
{
super.viewDidLoad()
self.delegate = self
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController)
{
if let vc = viewController as? UINavigationController
{ vc.popToRootViewController(animated: false) }
}
}
[self.navigationController popToRootViewControllerAnimated:NO];
Swift 4.2
The solution that works for me is to subclass the UITabBarController and add the two delegate functions as follows:
import UIKit
class MyCustomTabBarController: UITabBarController, UITabBarControllerDelegate {
var previousSelectedTabIndex:Int = 0
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
self.previousSelectedTabIndex = tabBarController.selectedIndex
}
override func tabBar(_ tabBar: UITabBar, didSelect item:
UITabBarItem) {
let vc = self.viewControllers![previousSelectedTabIndex] as! UINavigationController
vc.popToRootViewController(animated: false)
}
}
Make sure you set animated to false otherwise you will get
Unbalanced calls to begin/end appearance transitions for the targeted ViewController
Try this.
class TabBarClass: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
let vc = self.viewControllers![selectedIndex] as! UINavigationController
vc.popToRootViewController(animated: false)
}
}
First, you should create subclass of UITabbarController and add Observer:
- (void)viewDidLoad {
[super viewDidLoad];
[self.tabBar addObserver:self forKeyPath:#"selectedItem" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
}
When tabbar is selected, We will process in method:
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
if ([keyPath isEqualToString:#"selectedItem"] && [object isKindOfClass:[UITabBar class]]){
UITabBar *bar = (UITabBar *)object; // The object will be the bar we're observing.
// The change dictionary will contain the previous tabBarItem for the "old" key.
UITabBarItem *wasItem = [change objectForKey:NSKeyValueChangeOldKey];
NSUInteger was = [bar.items indexOfObject:wasItem];
// The same is true for the new tabBarItem but it will be under the "new" key.
UITabBarItem *isItem = [change objectForKey:NSKeyValueChangeNewKey];
NSUInteger is = [bar.items indexOfObject:isItem];
if (is == was) {
UIViewController *vc = self.viewControllers[is];
if ([vc isKindOfClass:[UINavigationController class]]) {
[(UINavigationController *)vc popToRootViewControllerAnimated:YES];
}
}
}
}
The UTabController suggests a different UX for letting a user "pop to root". When switching back to a tab, it keeps the full UINav stack from before. If they tap the bar item a second time (tapping the selected tab), only then does it pop to root. That's all automatic. Some apps, like instagram, allow a third tap to scroll to top.
I'd suggest sticking with the defaults as that's what users will be expecting.
The below had worked for me .This code in swift 3:
1> subclass UITabbarController and implement two below method with one iVAr:
class MyTabBarController: UITabBarController ,UITabBarControllerDelegate {
var previousSelectedTabIndex : Int = -1
}
2> set the tabbar delegate in viewdidLoad
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self // you must do it}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
self.previousSelectedTabIndex = tabBarController.selectedIndex
}
func tabBarController(_ tabBarController: UITabBarController,
shouldSelect viewController: UIViewController) -> Bool {
if self.previousSelectedTabIndex == tabBarController.selectedIndex {
let nav = viewController as! UINavigationController // mine in nav_VC
for vc in nav.childViewControllers {
if vc is YUOR_DESIRED_VIEW_CONTROLLER {
nav.popToViewController(vc, animated: true)
return false// IT WONT LET YOU GO TO delegate METHOD
}
}
}
return true
}
tabBarController.selectedIndex give you the selected tab
In tabBarController_shouldSelect_viewController method you can set your desired view controller with some easy calculation.
if you are not getting the above code play with both above method and you come to know how both working together
Use selected view controller to popToRootViewController. Basically you need to cast this instance.
Swift
((selectedViewController) as! UINavigationController).popToRootViewController(animated: false)
//
// I just added extra line so the scroll bar won't annoy you.
Xcode 11.5, Swift 5:
You don't need to use two delegate methods. Only one is enough:
extension CustomTabBarController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if tabBarController.viewControllers?.firstIndex(of: viewController) == tabBarController.selectedIndex,
let navigationController = viewController as? UINavigationController {
navigationController.popToRootViewController(animated: true)
}
return true
}
}
//create a tabbar controller class set it to your TabbarController in storyboard
import UIKit
class MyTabbarViewController: UITabBarController,UITabBarControllerDelegate{
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController)
{
if let vc = viewController as? UINavigationController
{ vc.popToRootViewController(animated: false) }
}
}