I have a review and find this article is the fourth time that I am talking about UISplitViewController. That is because UISplitViewController is hard and UISplitViewController changes.
Finally, this time we will only talk the modern part of UISplitViewController, which means those talks are base on iOS 14 and later.
UISplitViewController In General
Above picture is from Apple's document site.
UISplitViewController
The typical app of UISplitViewController is the Mail app in app.
You should be aware that the order of the ViewControllers. There are primary, supplementary and secondary.
While, in Xcode, the orders are different.
Whenever you find an unknown bug, first make sure you have one and only one UINavigationViewController that is in the primary ViewController.
Goal
Our goal is to have an app all in blue. So first I added some data to it.
First, I choose all background to system blue in storyboard. Then added data.
PrimaryTableViewController.swift
//
// PrimaryTableViewController.swift
// UISplitView Sample
//
// Created by zhaoxin on 2021/10/21.
//
import UIKit
class PrimaryTableViewController: UITableViewController {
private var items:[Int] = (1...3).map { $0 }
override func viewDidLoad() {
super.viewDidLoad()
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "mainCell", for: indexPath)
let item = items[indexPath.row]
cell.textLabel?.text = String(item)
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let splitViewController = self.splitViewController {
splitViewController.show(.supplementary)
}
}
}
SupplementaryViewController.swift
//
// SupplementaryViewController.swift
// UISplitView Sample
//
// Created by zhaoxin on 2021/10/21.
//
import UIKit
class SupplementaryViewController: UIViewController {
private var items:[Int] = (1...5).map { $0 }
override func viewDidLoad() {
super.viewDidLoad()
}
}
extension SupplementaryViewController:UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "supportCell", for: indexPath)
let item = items[indexPath.row]
cell.textLabel?.text = String(item)
return cell
}
}
extension SupplementaryViewController:UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let splitViewController = self.splitViewController {
splitViewController.show(.secondary)
}
}
}
SecondViewController.swift
//
// SecondViewController.swift
// UISplitView Sample
//
// Created by zhaoxin on 2021/10/21.
//
import UIKit
class SecondViewController: UIViewController {
private var items:[Int] = (1...10).map { $0 }
override func viewDidLoad() {
super.viewDidLoad()
}
}
extension SecondViewController:UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let item = items[indexPath.row]
cell.textLabel?.text = String(item)
return cell
}
}
The app run like this.
As you can see, the color of the title bar is not blue.
We need to add an image to background of the navigationBar.
UIColor+Image.swift
//
// UIColor+Image.swift
// Any Counter 2
//
// Created by zhaoxin on 2021/10/20.
// Copyright © 2021 ParusSoft.com. All rights reserved.
//
import Foundation
import UIKit
extension UIImage {
static func from(color: UIColor) -> UIImage {
let rect = CGRect(x: 0, y: 0, width: 1, height: 1)
UIGraphicsBeginImageContext(rect.size)
let context = UIGraphicsGetCurrentContext()
context!.setFillColor(color.cgColor)
context!.fill(rect)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return img!
}
}
SecondViewController.swift
//
// SecondViewController.swift
// UISplitView Sample
//
// Created by zhaoxin on 2021/10/21.
//
import UIKit
class SecondViewController: UIViewController {
private var items:[Int] = (1...10).map { $0 }
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.navigationBar.setBackgroundImage(UIImage.from(color: .systemBlue), for: .default)
}
}
extension SecondViewController:UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let item = items[indexPath.row]
cell.textLabel?.text = String(item)
return cell
}
}
The title bar turned to blue when the device was landscape. But in portrait, the color didn't change. Also, there was extra space in portrait.
View Hierarchy
We captured the portrait view controller in Xcode and looked into the view's hierarchy. We found that there were two instead of one UINavigationController used.
UISplitViewController is complex and hard to predict. Because it is combined states of two UINavigationViewControllers.
So each time we encounter a problem, we must consider the current state of the two UINavigationViewControllers.
Solution
First, we wanted all UINavigationController turn blue. So we do it in the UISplitViewController.
SplitViewController.swift
//
// SplitViewController.swift
// UISplitView Sample
//
// Created by zhaoxin on 2021/10/21.
//
import UIKit
class SplitViewController: UISplitViewController {
override func viewDidLoad() {
super.viewDidLoad()
children.forEach {
if let nav = $0 as? UINavigationController {
nav.navigationBar.setBackgroundImage(UIImage.from(color: .systemBlue), for: .default)
}
}
}
}
And remove the previous changes in SecondViewController.swift
.
Now the portrait view controller was blue, however, the gap was still there.
When you set both UINavigationViewControllers, the space doubles in portrait mode.
You must to hide a second UINavigationViewController to remove the space.
However, isHidden of navigationBar didn't work here.
You have to set background image to nil to hide the UINavigationViewController.
SecondViewController.swift
//
// SecondViewController.swift
// UISplitView Sample
//
// Created by zhaoxin on 2021/10/21.
//
import UIKit
class SecondViewController: UIViewController {
private var items:[Int] = (1...10).map { $0 }
override func viewDidLoad() {
super.viewDidLoad()
let frame = view.frame
if frame.height > frame.width {
self.navigationController?.navigationBar.setBackgroundImage(nil, for: .default)
}
}
}
extension SecondViewController:UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let item = items[indexPath.row]
cell.textLabel?.text = String(item)
return cell
}
}
Now the gap was gone. However, when rotating, the title went blank again.
set background image when screen rotate
SecondViewController.swift
//
// SecondViewController.swift
// UISplitView Sample
//
// Created by zhaoxin on 2021/10/21.
//
import UIKit
class SecondViewController: UIViewController {
private var items:[Int] = (1...10).map { $0 }
override func viewDidLoad() {
super.viewDidLoad()
let frame = view.frame
if frame.height > frame.width {
self.navigationController?.navigationBar.setBackgroundImage(nil, for: .default)
}
}
// MARK: - rotate
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
if size.height > size.width {
self.navigationController?.navigationBar.setBackgroundImage(nil, for: .default)
} else {
self.navigationController?.navigationBar.setBackgroundImage(UIImage.from(color: .systemBlue), for: .default)
}
}
}
extension SecondViewController:UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let item = items[indexPath.row]
cell.textLabel?.text = String(item)
return cell
}
}
SupplementaryViewController.swift
//
// SupplementaryViewController.swift
// UISplitView Sample
//
// Created by zhaoxin on 2021/10/21.
//
import UIKit
class SupplementaryViewController: UIViewController {
private var items:[Int] = (1...5).map { $0 }
override func viewDidLoad() {
super.viewDidLoad()
let frame = view.frame
if frame.height > frame.width {
self.navigationController?.navigationBar.setBackgroundImage(nil, for: .default)
}
}
// MARK: - rotate
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
if size.height > size.width {
self.navigationController?.navigationBar.setBackgroundImage(nil, for: .default)
} else {
self.navigationController?.navigationBar.setBackgroundImage(UIImage.from(color: .systemBlue), for: .default)
}
}
}
extension SupplementaryViewController:UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "supportCell", for: indexPath)
let item = items[indexPath.row]
cell.textLabel?.text = String(item)
return cell
}
}
extension SupplementaryViewController:UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let splitViewController = self.splitViewController {
splitViewController.show(.secondary)
}
}
}
TintColor Lost When Landscape
There was a bug on landscape. TintColor set in storyboard was lost.
SplitViewController.swift
//
// SplitViewController.swift
// UISplitView Sample
//
// Created by zhaoxin on 2021/10/21.
//
import UIKit
class SplitViewController: UISplitViewController {
override func viewDidLoad() {
super.viewDidLoad()
children.forEach {
if let nav = $0 as? UINavigationController {
nav.navigationBar.setBackgroundImage(UIImage.from(color: .systemBlue), for: .default)
nav.navigationBar.tintColor = .systemYellow
}
}
}
}
Sample Project
*UISplitView Sample