1
\$\begingroup\$

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?

storyboard month view portrait month view landscape day view portrait day view landscape 5 day view landscape

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)
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.