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?
-
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.
-
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()
-
Called when the toolbar delete timer button is hit.
See moreDeclaration
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.
-
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.
-
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.
-
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.
-
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).