RiValT_GroupEditor_ViewController

@MainActor
class RiValT_GroupEditor_ViewController : RiValT_Base_ViewController
extension RiValT_GroupEditor_ViewController: UICollectionViewDragDelegate
extension RiValT_GroupEditor_ViewController: UICollectionViewDropDelegate
extension RiValT_GroupEditor_ViewController: UICollectionViewDelegate
extension RiValT_GroupEditor_ViewController: UIScrollViewDelegate

This is the view controller for the “multi-timer” screen, where we can arrange timers in groups and add new ones.

It allows the user to drag and drop timers, so they can visually rearrange the matrix.

BASIC STRUCTURE

This controller displays a UICollectionView instance, filled with vertical rows, representing “timer groups.” Each row is a group.

Each group can have up to 4 horizontally-arranged timers. Timers in a group, execute sequentially, from left, to right.

When a timer transitions from a left timer, to the one to its right (the left timer ends, and starts the right timer automatically), a “transition sound” may be played.

When the rightmost timer ends, the “alarm sound” is played.

The user can drag timers around, by long-pressing on a timer. The timer can move within a group, or from one group to another.

Groups can have options specified, that apply to all timers in a group. When a timer is moved from one group to another, it adapts to the group setings for the new group.

Timer Selection and Group Selection

One timer must always be selected. This is indicated by a black background, and a colored digital font for the timer value[s].

If a timer is selected, then its group is also selected. Group selection is indicated by a horizontal “gradient” highlight, across the row.

If there is more than one row, or more than one timer in a group, then a number will appear, at the right end of the row selection highlight.

If there is more than one timer in the group, then this number will be a tappable button. Tapping it, will advance the timer selection, wrapping, if at the end.

GLOBAL SETTINGS

In the left of the Navigation Bar, is a “gear” icon. This displays a popover, with checkboxes that affect options for the entire app (not just single groups).

Start Timer Immediately Checkbox

If this checkbox is checked, then hitting the “Play” triangle will immediately start the timer. If it is unchecked, then the timer will start in a “paused” state, and will require an additional step, to start counting down.

“One-Tap” Timer Editing Checkbox

If this checkbox is checked, then simply tapping on a timer, will bring in the Timer Editor Screen for that timer.

If it is unchecked, then tapping on a timer will simply select the timer, and an “Edit” item will appear in the Toolbar, at the bottom of the screen.

That “Edit” item will need to be tapped, to bring in the Timer Editor Screen for whichever timer is selected.

Show Toolbar In Timer Checkbox (Not displayed for Mac -Mac always shows the toolbar, and it can’t be hidden)

In iPhone and iPad, you can have a toolbar optionally displayed across the bottom of the Running Timer Screen. This toolbar is always shown, for Mac Catalyst.

If the toolbar is shown, then the user must tap on items in the toolbar, to control the timer.

If the toolbar is not shown, then swipe and tap gestures are used to control the running timer (discussed in the Running Timer Screen).

Auto-Hide Toolbar Checkbox (Only displayed when “Show Toolbar In Timer” is shown and selected)

If this is selected, then the toolbar will fade out, after a few seconds of user inactivity (the timer keeps going, though). Tapping on the screen, brings the toolbar back.

About This App Button

Tapping on this button will dismiss the popover, and bring in the “About This App” Screen.

GROUP SETTINGS

In the top, right of the Navigation Bar, are two items: A little “screen” icon, representing the current display mode for the group, and a “clock” icon, representing the final alarm state for the group.

These apply to the currently selected group, and may change, to reflect the current group’s setting.

Display Type

Tapping on the Display icon, brings up a popover, allowing the user to select the type of Running Timer Display to be used for the group. It is a simple popover, with a segmented switch, and a preview area, under it, showing the display type.

Sounds

Tapping on the sound icon, will bring up a ppopver, allowing the user to choose a final alarm sound, and, optionally, a transition sound (only shown, when there is more than one timer in the group).

This popover is a bit more complex than the Display Popover, as it has a segmented switch, allowing the user to choose the type of alarm to use, at the end of the countdown, an optional picker, for selecting a sound, and, optionally, a second picker, allowing the user to select a transition sound.

TOOLBAR

There’s a toolbar, displayed at the bottom of the screen, that affects the selected timer.

Delete Button (Trash Can Icon)

Selecting this, will bring up a confirmation alert, asking if you really want to delete the timer. If you confirm, the timer is deleted, and the next one is selected.

Play Button (Triangle)

Selecting this, starts the selected timer (goes directly to the Running Timer Screen).

Edit Button (Optional)

This is only displayed, if “One-Tap Edit” is off. Selecting it, opens the Timer Editor Screen for the selected timer.

  • The width of the “gutters” around each cell.

    Declaration

    Swift

    @MainActor
    private static let _itemGuttersInDisplayUnits: CGFloat
  • The ID of the segue to edit a timer.

    Declaration

    Swift

    @MainActor
    private static let _timerEditSegueID: String
  • The storyboard ID for instantiating the class.

    Declaration

    Swift

    @MainActor
    private static let _aboutScreenSegueID: String
  • Used to track scrolling, and to prevent horizontal scroll.

    Declaration

    Swift

    @MainActor
    private var _initialContentOffset: CGPoint
  • This is set to true, if we want to override the pref.

    Declaration

    Swift

    @MainActor
    var forceStart: Bool
  • Maintains the last scroll position, for iterating a row.

    Declaration

    Swift

    @MainActor
    private var _lastScrollPos: CGPoint
  • The settings button, in the navbar.

    Declaration

    Swift

    @IBOutlet
    @MainActor
    weak var settingsBarButtonItem: UIBarButtonItem?
  • The main collection view.

    Declaration

    Swift

    @IBOutlet
    @MainActor
    weak var collectionView: UICollectionView?
  • The toolbar at the bottom of the screen.

    Declaration

    Swift

    @IBOutlet
    @MainActor
    weak var toolbar: UIToolbar?
  • The trash button in the toolbar.

    Declaration

    Swift

    @IBOutlet
    @MainActor
    weak var toolbarDeleteButton: UIBarButtonItem?
  • The “Play” (start) button in the toolbar.

    Declaration

    Swift

    @IBOutlet
    @MainActor
    weak var toolbarPlayButton: UIBarButtonItem?
  • The edit button in the toolbar.

    Declaration

    Swift

    @IBOutlet
    @MainActor
    weak var toolbarEditButton: UIBarButtonItem?
  • This is the datasource for the collection view. We manage it dynamically.

    Declaration

    Swift

    @MainActor
    var dataSource: UICollectionViewDiffableDataSource<Int, RiValT_TimerArray_Placeholder>?
  • Used to prevent overeager haptics.

    Declaration

    Swift

    @MainActor
    var lastIndexPath: IndexPath?
  • This allows us to force-close the popover, easily.

    Declaration

    Swift

    @MainActor
    weak var currentPopover: UIPopoverPresentationController?

Base Class Overrides

  • Called when the view has loaded.

    Declaration

    Swift

    @MainActor
    override func viewDidLoad()
  • Called just before the view appears.

    We use the opportunity to switch to the editor, if we have just one timer, and this is the first time through.

    Declaration

    Swift

    @MainActor
    override func viewWillAppear(_ inIsAnimated: Bool)

    Parameters

    inIsAnimated

    True, if the appearance is animated.

  • Called just after the view appeared.

    Declaration

    Swift

    @MainActor
    override func viewDidAppear(_ inIsAnimated: Bool)

    Parameters

    inIsAnimated

    True, if the appearance is animated.

  • Called just before the view disappears.

    Declaration

    Swift

    @MainActor
    override func viewWillDisappear(_ inIsAnimated: Bool)

    Parameters

    inIsAnimated

    True, if the disappearance is animated.

  • Called when the view lays out its view hierarchy.

    Declaration

    Swift

    @MainActor
    override func viewDidLayoutSubviews()
  • Called to allow us to do something when we change layout size (like rotating)

    Declaration

    Swift

    @MainActor
    override func viewWillTransition(to inSize: CGSize, with inCoordinator: any UIViewControllerTransitionCoordinator)

    Parameters

    inSize

    The new size

    inCoordinator

    The coordinator object.

  • Called to allow us to do something before dismissing a popover.

    Declaration

    Swift

    @MainActor
    override func popoverPresentationControllerShouldDismissPopover(_: UIPopoverPresentationController) -> Bool

    Return Value

    True (all the time).

  • Called to allow us to do something before displaying a popover.

    Declaration

    Swift

    @MainActor
    override func prepareForPopoverPresentation(_ inController: UIPopoverPresentationController)

    Parameters

    inController

    The popover controller about to be displayed.

  • Called when we are to segue to another view controller.

    Declaration

    Swift

    @MainActor
    override func prepare(for inSegue: UIStoryboardSegue, sender inData: Any?)

    Parameters

    inSegue

    The segue instance.

    inData

    An opaque parameter with any associated data.

Instance Methods

  • This simply opens the about this app screen.

    Declaration

    Swift

    @MainActor
    func openAboutScreen()
  • We set up the navbar buttons.

    Declaration

    Swift

    @MainActor
    func setUpNavBarItems()
  • If the first time through, and we only have one timer, we go straight to the editor.

    Declaration

    Swift

    @MainActor
    func checkForFastForward()
  • This establishes the display layout for our collection view.

    Each group of timers is a row, with up to 4 timers each.

    At the end of rows with less than 4 timers, or at the end of the collection view, we have “add” items.

    Declaration

    Swift

    @MainActor
    func createLayout()
  • This sets up the data source cells.

    Declaration

    Swift

    @MainActor
    func setupDataSource()
  • This updates the collection view snapshot.

    Declaration

    Swift

    @MainActor
    func updateSnapshot()
  • This updates the items in the toolbar.

    Declaration

    Swift

    @MainActor
    func updateToolbar()

Callbacks

  • Called when the toolbar delete timer button is hit.

    See more

    Declaration

    Swift

    @IBAction
    @MainActor
    func toolbarDeleteButtonHit(_: Any)
  • Called when the “Play” button is hit.

    Declaration

    Swift

    @IBAction
    @MainActor
    func toolbarPlayButtonHit(_: UIBarButtonItem! = nil)
  • Called when the Watch wants us to play.

    Declaration

    Swift

    @MainActor
    func remotePlay()
  • Called when the “Edit” button is hit.

    Declaration

    Swift

    @IBAction
    @MainActor
    func toolbarEditButtonHit(_: Any! = nil)
  • Called to segue to the editor screen.

    Declaration

    Swift

    @MainActor
    func goEditYourself(optionalTitle inTitle: String? = nil)
  • The sound settings button was hit.

    Declaration

    Swift

    @IBAction
    @MainActor
    func soundSettingsButtonHit(_ inBarButtonItem: UIBarButtonItem)
  • The dsiplay settings button was hit.

    Declaration

    Swift

    @IBAction
    @MainActor
    func displaySettingsButtonHit(_ inBarButtonItem: UIBarButtonItem)
  • The main settings button was hit.

    Declaration

    Swift

    @IBAction
    @MainActor
    func settingsButtonHit(_ inBarButtonItem: UIBarButtonItem)
  • The background of a group line was hit.

    Declaration

    Swift

    @objc
    @MainActor
    func groupBackgroundTapped(_ inTapGesture: UITapGestureRecognizer)

    Parameters

    inTapGesture

    The tap that caused the call.

  • The number at the end of a group was hit.

    Declaration

    Swift

    @objc
    @MainActor
    func groupBackgroundNumberTapped(_ inTapGesture: UITapGestureRecognizer)

    Parameters

    inTapGesture

    The tap that caused the call.

UICollectionViewDragDelegate Conformance

  • Called when a drag starts.

    This allows us to configure the “drag preview.”

    Declaration

    Swift

    @MainActor
    func collectionView(_ inCollectionView: UICollectionView,
                        dragPreviewParametersForItemAt inIndexPath: IndexPath) -> UIDragPreviewParameters?

    Parameters

    inCollectionView

    The collection view.

    inIndexPath

    The index path of the item being dragged.

    Return Value

    The drag parameters, or nil, if the item can’t be dragged.

  • Called when a drag starts.

    If the item can’t be dragged (the “add” items, or the timer, if there is only one), then an empty array is returned.

    Declaration

    Swift

    @MainActor
    func collectionView(_ inCollectionView: UICollectionView,
                        itemsForBeginning inSession: UIDragSession,
                        at inIndexPath: IndexPath
    ) -> [UIDragItem]

    Parameters

    inCollectionView

    The collection view.

    inSession

    The session for the drag.

    inIndexPath

    The index path of the item being dragged.

    Return Value

    The wrapper item for the drag.

UICollectionViewDropDelegate Conformance

  • Called to vet the current drag state.

    Declaration

    Swift

    @MainActor
    func collectionView(_ inCollectionView: UICollectionView,
                        dropSessionDidUpdate inSession: UIDropSession,
                        withDestinationIndexPath inIndexPath: IndexPath?
    ) -> UICollectionViewDropProposal

    Parameters

    inCollectionView

    The collection view (ignored).

    inSession

    The session for the drag.

    inIndexPath

    The index path of the item being dragged.

    Return Value

    the disposition proposal for the drag.

  • Called when the drag ends, with a plop.

    Declaration

    Swift

    @MainActor
    func collectionView(_ inCollectionView: UICollectionView,
                        performDropWith inCoordinator: UICollectionViewDropCoordinator
    )

    Parameters

    inCollectionView

    The collection view.

    inCoordinator

    The drop coordinator.

UICollectionViewDelegate Conformance

  • Called when an item in the collection view is tapped.

    Declaration

    Swift

    @MainActor
    func collectionView(_ inCollectionView: UICollectionView,
                        didSelectItemAt inIndexPath: IndexPath
    )

    Parameters

    inCollectionView

    The collection view.

    inIndexPath

    The index path of the item being tapped.

  • Called when the collection view has been rebuilt, and we need to check the scroll position.

    We use this to reset the offset.

    Declaration

    Swift

    @MainActor
    func collectionView(_ inCollectionView: UICollectionView, targetContentOffsetForProposedContentOffset inOffset: CGPoint) -> CGPoint

    Parameters

    inCollectionView

    The collection view.

    inOffset

    The current offset.

UIScrollViewDelegate Conformance

  • Called when a scroll begins.

    Declaration

    Swift

    @MainActor
    func scrollViewWillBeginDragging(_ inScrollView: UIScrollView)

    Parameters

    inScrollView

    The collection view (as a scroll view).

  • Called when a scroll actually happens.

    Declaration

    Swift

    @MainActor
    func scrollViewDidScroll(_ inScrollView: UIScrollView)

    Parameters

    inScrollView

    The collection view (as a scroll view).