肇鑫的技术博客

业精于勤,荒于嬉

Strongly Recommend You to Add Your Own UINavigationControllers even in iOS 14 when Using UISplitViewController

In iOS 14, Apple adapts UISplitViewController from two columns to three columns. Especially, it says that

If you set a child view controller that’s not a navigation controller, the split view controller creates a navigation controller for it.

However, Apple didn't say that it will try to reuse the UINavigationController when you reset the columns.

That means, when you set .secondary view controller to another view controller, the split view controller will push the new view controller to the back stack, which is not what you want.

Issue Sample

UISplitViewController Issue

  1. run the sample
  2. you will see detail 01, press back
  3. you will see master 01, press back
  4. you will see primary, press Master 02
  5. you will see Master 02, press Master 02
  6. you will see Detail 02, press back

You expect to see Master 02.
You see Detail 01.

Work Around

Add a UINavigationViewController to each of detail view controllers.

UISplitViewController Issue work around

I have file this as a bug to Apple. The id is FB8640357.

Apple's Reply

Just got the reply from Apple. 8 days since I have filed this issue. Apple says:

After reviewing your feedback, we have some additional information for you:

This is expected behavior.

The secondary column gets this special treatment if the view controller gets set to a non-navigation controller when it already has a nav controller—i.e., it’s pushed onto the nav stack, as they discovered.

Expected use patterns would be:

• Change the contents of the detail view controller (not the vc itself) and send -showColumn:UISplitViewControllerColumnSecondary (if desired) to confirm that the secondary column is visible, or ...
• For the secondary column only, clear it first by setting its vc to nil, then setting new desired vc, or ...
• Manage the navigation controller in the secondary column directly with -setViewControllers: or push/pop. If you’re using an automatically created UINavigationController instead of one you provided, you can find it with -viewControllerForColumn: and then ask that vc for its navigation controller.

Apple gave 3 recommends. The first was easy to think of, but it needed the two view controllers to be very similar.

I like the second recommend most. func setViewController(_ vc: UIViewController?, for column: UISplitViewController.UISplitViewController.Column) does allow vc to be nil. And it is direct. There is some performance lose though. As it will create another UINavigationViewController each time.

The third recommend has the most performance. But it is indirect. And it smells bad. It should be seen as a workaround.

Final

I recommend to fix it in storyboard as I previously did. Or, using the second recommend by Apple.

UISplitViewController的几种模式

模型

  • UISplitViewController拥有两个视图控制器,在故事版中分别对应主视图控制器和细节视图控制器,在编码中对应为viewControllers属性,主视图控制器在前,细节视图控制器在后。
  • 开发者通过preferredDisplayMode推荐想要的模式,通过displayMode获得当前的模式。
  • 在iPhone中,横屏、和竖屏都是allVisible,此时可以通过UISplitViewControllerDelegatesplitViewController(_:collapseSecondary:onto:)方法来告诉系统默认是显示哪个视图控制器。
  • 在iPad中,横屏是allVisible,竖屏是primaryHidden。此时如果想要默认显示主视图控制器,需要在UISplitViewControllerDelegatesplitViewController(_:willChangeTo:)方法,设定preferredDisplayModeprimaryOverlay

UISplitViewController

  • displayMode
    • 这是一个只读属性。
    • 它代表的是UISplitViewController当前的具体模式。
  • preferredDisplayMode
    • 开发者只能通过displayMode了解到UISplitViewController当前的模式。如果开发者不满意这个模式,想要手动调节,就可以通过修改preferredDisplayMode来实现。
    • 值得注意的是,这个是开发者的推荐,并不是完全的设定,系统仅会在条件允许的情况下,优先使用这个推荐。如果系统认为可视面积过小,就会忽略这个推荐,而使用系统认为最适合的模式。

UISplitViewController.DisplayMode(按照rawValue的数值排列)

  • automatic
    • 自动模式是默认的模式,完全由系统进行判断,开发者没有任何推荐。
    • 注意:这个自动模式,只在preferredDisplayMode中有效,displayMode实际显示的,只会是下面三个模式。
  • primaryHidden
    • UISplitViewController包含两个视图控制器,主视图控制器和细节视图控制器。primaryHidden表示优先显示细节视图控制器,而将主视图控制器隐藏起来。用户需要使用手势或者是后退按钮显示主视图控制器。
    • iPad在竖屏时默认会是这个模式。
  • allVisible
    • 同时显示主视图控制器和细节视图控制器。
    • 需要注意:这个模式虽然叫allVisible,却不是一定都能同时显示。如果不能,就会需要告诉系统,优先显示哪个视图控制器。
    • iPad在横屏时默认会是这个模式。此时会全部显示。
    • iPhone 6s Plus在竖屏和横屏时都是这个模式。竖屏时,因为实际上不能全部显示,就还需要考虑UISplitViewControllerDelegatesplitViewController(_:collapseSecondary:onto:)方法。
  • primaryOverlay
    • primaryHidden类似。但是会优先显示主视图控制器,叠加在细节视图控制器之上。
    • 如果我们要特别提示用户主视图控制器的存在,就可以使用这个选项。

UISplitViewControllerDelegate

splitViewController(_:collapseSecondary:onto:)方法

UISplitViewController.DisplayModeallVisible,且可显示的面积不能同时容纳显示主视图控制器和细节视图控制器时,系统会调用UISplitViewControllerDelegatesplitViewController(_:collapseSecondary:onto:)方法。

这个方法返回true代表显示主视图控制器,返回false代表显示细节视图控制器。

splitViewController(_:willChangeTo:)方法

当你需要在系统选择了primaryHidden时,就更改为primaryOverlay。使用此方法。

iOS自定义AlertController

最初的代码在这里。作者自己的说明

最初的作品已经很好用了。只是细节需要改动一下。

更改

  1. 原代码低于Swift 4.0,Xcode 11 beta 5无法编译。此次,先将Swift改成4.0。发现一切正常,没有需要改动的地方。
  2. 原代码不支持暗模式,可以在故事版中将颜色重新选择为支持暗模式的。特别的,在资源中新建颜色资源AlertBackgroundColor,获得模拟器中UIAlertController.view在亮/暗模式下的背景颜色。并指定为Alert View的背景色。
  3. 原代码ViewController.swift第55行使用了.overCurrentContext,这个在master/detail模式的布局中,如果detail中弹出自定义的警告窗口,之后旋转屏幕或者更改显示模式(比如由亮改暗),警告窗口就会出现问题。此处需要改成.overFullScreen
  4. 原代码CustomAlertView.swift第23行alertViewGrayColor使用了自定义颜色,注释掉掉。在第35行插入let alertViewGrayColor = UIColor.systemGray2
  5. CustomAlertView.swift第36行插入let lineThickness:CGFloat = 0.5,并且将第37-39行的width: 1.0改成width: lineThickness

小结

经过以上的改动,就可以获得一个支持iOS 13 beta的自定义AlertController了。剩下的你只需要根据自己的需求,继续填充就可以了。