In an effort to explore how the default calendar app works and to experiment with container view controllers, I created the following mock app. See this post for more info about my motivation for this app. This app only explores basic calendar navigation and orientation changes.
I've chosen a design that uses a UINavigationController
in which a container view controller is embedded as the main view in the root view controller. This embed view controller would be the actual calendar for the app. The plan is to use JTAppleCalendar to implement the calendar functionality. Because it seems I only need one calendar controller and I can toggle between different calendar layouts (day view, 3 day view, 5 day view, month view), the main role of the navigation controller is to toggle between these different layouts. I've also added a right bar button and a tool bar that would push new VCs like a standard navigation controller.
With this app, I am most concerned about the left bar button item and the title view's segmented control and how both work in portrait and landscape and how it controls the calendar layout.
Like the iOS Calendar app, the left bar button item can either be a pseudo 'back' button or a label. When the orientation changes to landscape, a segmented control is added to the title view of the nav bar. In landscape, the left bar button item will always be a label as the layout is controlled by the segmented control. I say 'pseudo because the back button and the segmented control do not actually push/pop calendar view controllers. It only toggles the calendar layout.
In portrait, only day view and month view are the layouts available and the segmented control is hidden. In month view, if the user selects a 'day cell' (not implemented), the calendar layout is changed to the day layout. In day layout and in portrait, the left bar button item changes to a back button. Tapping the back button, the layout is toggled back to month view and the left bar button item is then changed to a label.
In landscape, layout control is passed to the segmented control.
For this review, I am looking for feedback about my overall strategy and how it is currently implemented. I'm also concerned that I'm mis-using UINavigationController and that the container view controller, the calendar, could become overly complicated. Also, am I properly using enums and protocols here?
As a side note, for the 'add' view controller, I'm using 'Show Detail (e.g. Replace)' segue that presents the VC modally. In that view, I've added a back button that dismisses the view. Is this standard if I want the 'add' view to be full screen with the nav bar hidden?
ViewController.swift
// Root view controller
import UIKit
enum CalendarType: CaseIterable {
case day, threeDay, fiveDay, month
}
protocol CalendarLayoutDelegate {
func layoutCalendar(layout: CalendarType)
}
class ViewController: UIViewController {
// When set, the class the must implement the CalendarLayoutDelegate protocol
var calendarLayoutDelegate: CalendarLayoutDelegate?
// Reference needed to set CalendarViewController instance to the CalendarLayoutDelegate and,
// to be able to call the protocal method when currentCalendarLayout changes.
var calendarViewController: CalendarViewController?
// Navbar components
@IBOutlet var leftBackButton: UIButton!
var leftTitleLabel = UILabel()
@IBOutlet weak var segmentedControl: UISegmentedControl!
var currentCalendarLayout: CalendarType? {
didSet {
updateLeftBarButtonItem()
calendarViewController?.layoutCalendar(layout: currentCalendarLayout!)
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "CalendarViewSegue" {
if let calendarVC = segue.destination as? CalendarViewController {
calendarViewController = calendarVC
calendarViewController!.dateDelegate = self
calendarLayoutDelegate = calendarViewController
}
}
if segue.identifier == "AddSegue" {
print("Prepare segue AddVC")
}
}
override func viewDidLoad() {
super.viewDidLoad()
leftTitleLabel.textColor = UIColor.black
currentCalendarLayout = .month
updateLeftBarButtonItem()
layoutNavBar()
}
override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
updateLeftBarButtonItem()
layoutNavBar()
}
func layoutNavBar() {
switch UIDevice.current.orientation {
case .landscapeLeft, .landscapeRight:
segmentedControl.isHidden = false
// Set selected segment, either gonna be .day or .month if transitioning from portrait
segmentedControl.selectedSegmentIndex = CalendarType.allCases.firstIndex(of: currentCalendarLayout!)!
case .portrait:
segmentedControl.isHidden = true
// In portrait, only .month and .day are allowed.
if currentCalendarLayout != .month {
currentCalendarLayout = .day
}
default:
break
}
}
@IBAction func changeCalendarLayout(_ sender: Any) {
switch segmentedControl.selectedSegmentIndex {
case 0:
currentCalendarLayout = .day
case 1:
currentCalendarLayout = .threeDay
case 2:
currentCalendarLayout = .fiveDay
case 3:
currentCalendarLayout = .month
default:
break
}
}
@IBAction func backButtonMonthTapped(_ sender: Any) {
currentCalendarLayout = .month
}
func updateLeftBarButtonItem() {
// if portrait and not .month show back button
if UIDevice.current.orientation == .portrait && currentCalendarLayout != .month {
self.navigationItem.leftBarButtonItem = UIBarButtonItem.init(customView: leftBackButton)
} else {
self.navigationItem.leftBarButtonItem = UIBarButtonItem.init(customView: leftTitleLabel)
}
}
}
extension ViewController: DateDelegate {
// TODO: Update the UI with the current date selected.
func setDate(date: Date) {
let formatter = DateFormatter()
formatter.dateFormat = "MMMM yyyy"
let monthYearString = formatter.string(from: date)
leftTitleLabel.text = monthYearString
// If back button, just show month and not the year
leftBackButton.setTitle(monthYearString.components(separatedBy: " ").first, for: .normal)
}
}
CalendarViewController.swift
import UIKit
protocol DateDelegate {
func setDate(date: Date)
}
class CalendarViewController: UIViewController {
// When set, the class the must implement the DateDelegate protocol
var dateDelegate: DateDelegate?
@IBOutlet weak var tempLayoutLabel: UILabel!
var tempText = ""
override func viewDidLoad() {
super.viewDidLoad()
self.dateDelegate?.setDate(date: Date())
tempLayoutLabel.text = tempText
}
}
extension CalendarViewController: CalendarLayoutDelegate {
func layoutCalendar(layout: CalendarType) {
tempText = "\(String(describing: layout)) Calendar"
if tempLayoutLabel != nil {
tempLayoutLabel.text = tempText
}
}
}
AddViewController.swift
import UIKit
class AddViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func dismissAddViewController(_ sender: Any) {
self.dismiss(animated: true, completion: nil)
}
}