Skip to content →

Tag: GitHub

Delayed Preemptive Dispatch

DelayedPreemptiveDispatchQueueDemoIf you’re looking to run a task closure concurrently using Grand Central Dispatch after some delay … with the option to delay it again, replace it with another task, or cancel it altogether, the concept of delayed preemptive dispatch presented here may be of interest to you. For a quick peek, skip to Implementation; for use cases, read on.

Problem Scenarios

Consider these problem scenarios, all taken from some of my recent work. How would you implement the described functionality?

  1. You want to allow a user to make three incorrect passcode entries within a minute of each other before locking them out. If more than a minute elapses between entries, the try count resets to zero.
  2. You are using a third party framework that presents a panning view. Its API provides the means for notification as each pan motion stops…both when the pan comes to a rest and when the user interrupts one pan motion to start another. However, you need to know when all panning has stopped.
  3. You are interfacing with a third party library that concurrently retrieves an unknown quantity of online resources on your behalf. It only notifies you as each resource is downloaded. However, you need to take some action once all resources have loaded.

My approach to these problems was a common one: use a system I call  delayed preemptive dispatch (the term “dispatch” coming from Apple’s Grand Central Dispatch (GCD) libdispatch library available on all their operating systems from the Watch to the TV).

The gist of this system is I want to perform a task after a brief delay, but only if I don’t preempt it with another task before the prior task’s timer expires (in effect canceling the prior one); it is at the conclusion of the last submitted delay that the last submitted task is executed.

Here’s how I apply this to the above three problems, identified by the event I wish to be notified of:

  1. Invalid passcode expired. I start a one-minute timer when a passcode has been wrongly entered that, upon expiration, runs a task to reset the entry count. However, I don’t want that closure to run if, in the meantime, the user has re-entered the passcode. Instead, I want to preempt the first closure, replacing it with a second one-minute timer that will itself reset the entry count. Importantly, once the user enters the passcode correctly, I want to cancel the task altogether.
  2. Panning stopped. Here I start a timer upon receipt of a pan-stopped notification that, upon expiration, declares all-stop on panning. However, I don’t want that to occur if a subsequent pan-stop event occurs in the meantime. Rather, I want to preempt the preceding closure with the next and, yes, initiate a new delayed notification closure.
  3. All resources loaded. Finally, in this case I start a brief timer after each resource loads that, upon expiration, signals all resources loaded. However, I don’t want that signal to fire if a subsequent resource loads before the timer expires. Instead, I want to preempt the preceding notification closure and replace it with another one that, if not itself preempted, will be the one to notify all resources have been loaded.

Implementation

At this GitHub repo you will find an implementation of what I call a DelayedPreemptiveDispatchQueue, written in Swift 3, which includes unit tests and the above-illustrated UI demo.

With this class you can delay execution of a task until expiration of a timer, with the option to later reset the timer, preempt the task with another, or cancel the task altogether. It makes use of the Swift 3 GCD DispatchGroup to manage concurrent timers and fire off the delayed task once all the timers have expired.

Here’s a peek at the delay(_:task:) method in that class (see the repo for the full source):

/// Executes `task` no earlier than after the given delay. 
///
/// Subsequent calls to this method will preempt execution of this `task`. The task may also be provided via the class initializer. Cancel execution of a task via `cancel()`.
///
/// - parameter seconds: The number of seconds before this or the initializer-givn task may be executed. The delay will be longer if there is a pending delay that expires after this period.
/// - parameter task: The closure to run after _all_ pending delays have expired. If `nil` (the default), the task to be run is that given at initialization. If no task was given during initialization or at any call to this method, it takes no action.
func delay(_ seconds: TimeInterval, task: (() -> Void)? = nil) {

  // Make sure we have a defined task task
  if task != nil {
    self.task = task
  }
  guard self.task != nil else { return }

  // Newly submitted task preempts prior cancellation
  cancelled = false

  // Add delay timer to the dispatch group
  group.enter()
  self.queue.asyncAfter(deadline: DispatchTime.now() + seconds) {
    self.group.leave()
  }

  // Only assign GCD notify action when first timer added to group.
  // The notify action, which performs the task, runs once all timers have left the group.
  if empty {
    self.empty = false
    group.notify(queue: .main) {
      if !self.cancelled {
        self.task()
      }
      self.empty = true
    }
  }
}

The task may be provided via init(label:task:) (not shown), delay(_:task:), or a combination of both. Neither the delay times nor tasks need be the same with each call. Only the last task submitted will be called once all concurrent delay timers have expired. While you cannot cancel the timers themselves, you can cancel execution of the task via cancel() (also not shown). Once all timers have expired, you can reuse the DelayedPreemptiveDispatchQueue with or without specifying a new task.

While this class makes use of the DispatchGroup to manage a group of timers, the functionality given by DelayedPreemptiveDispatchQueue is not inherent to it. For example, you cannot provide a task to a DispatchGroup at initialization, but only via its instance notify() method. Also, all tasks submitted to a dispatch group execute; there is no concept of “preemption” as there is in this class.

For documentation on the Swift 3 implementation of Grand Central Dispatch, reference this current pre-release Swift 3 evolution proposal 88, “Modernize libdispatch for Swift 3 naming conventions.”

Leave a Comment

Page View Controller in a Container View

Late one evening this week I decided to turn Netflix off and see what I could cobble together with regard to a feature a teammate of mine was working on, namely, using UIPageViewController to provide paginated content.

PageViewControllerDemoAs seen in the animation, for us there are some key characteristics of the targeted implementation. These include:

Swift 3 language and UIKit features illustrated but not specifically discussed include:

  • Protocols
  • Switch case value bindings akin to if let value = value as? SomeType
  • String splitting using map(); substrings
  • UITableViewController and UITableViewControllerDataSource
  • Storyboard and programmatic instantiation of view controllers
  • UIView.animate(withDuration:animations:)

If any of these topics interest you and you want a quick fix, take a look at the project on GitHub, written in Swift 3 (updated for Swift 4). Have specific questions? Keep reading.

FAQ

  • How do I provide paginated content? There’s a storyboard piece and a programmatic piece. In summary, on the storyboard you drop in and hook up a Page View Controller object into your scene flow and add a content object of your choosing. Programmatically, you need only implement a couple UIPageViewControllerDataSource methods … and provide the initial view controller. See PageViewController.swift for that implementation. Details below.
  • How do I use a page scroll rather than curl transition? In the Attributes Inspector for the Page View Controller storyboard scene, set “Transition Style” to “Scroll”.
  • How do I add a page indicator? Implement UIPageViewControllerDataSource methods presentationCount(for:) and presentationIndex(for:); see PageViewController.swift. If you don’t want a page indicator, don’t implement these methods.
  • How do I set the color of the page indicator dots? See PageViewController viewDidLoad():
    let pageControl = UIPageControl.appearance(whenContainedInInstancesOf: [PageViewController.self])
    pageControl.pageIndicatorTintColor = .lightGray
    pageControl.currentPageIndicatorTintColor = .black
    
  • How do I embed a paginated subview? The storyboard is all you need. The process I follow:
    1. Drop a Container View onto the containing view. If you’re good with the tag-along view controller, stop here.
    2. Otherwise, delete the tag-along view controller.
    3. Add the desired controller to the storyboard.
    4. Control-drag from the container to the controller.
    5. Select the “Embed” segue.
  • How do I use a segmented control to switch between subviews? My approach is through manipulation of each subview’s hidden property, accessed by @IBOutlet connection. See MainViewController.swift.
  • How do I reuse a view controller in a storyboard? I recommend embedded Container Views (they’re awesome!). In this project, I have one container view with an Embed segue directly to the content table view controller; then I have a second container view with an Embed segue to the Page View Controller that is programmatically connected to the same table view controller.
  • How do I access a container-embedded view controller? Rather simply…when you know how. The trick is to override prepare(for:sender:) in your container view controller. By doing so, you get access to the destination (embedded) view controller via the provided UIStoryboardSegue. See MainViewController.swift.

Details

Okay, let’s dive into some details. Keep in mind the context of this discussion is the specific implementation as given on GitHub.

Storyboard

Assuming you know the basics of Interface Builder storyboarding, here are some things to be aware of:

  • There are two container views on the project storyboard; although, because they’re stacked, you only see one. The first, named “Paged Container View”, has an embed segue to the Page View Controller scene. The second, “Unpaged Container View”, has an embed segue to Table View Controller.
  • Table View Controller has a Storyboard ID of “TableViewController”, which PageViewController makes use of when serving up paged view controllers.
  • There is no direct connection between Page View Controller and Table View Controller on the storyboard. As just alluded to, that is done programmatically in PageViewController.
  • The Table View Controller has one basic prototype cell matched with a class defined in TableViewController.swift named BasicCell.

Page View Controller

The back end of the Page View Controller storyboard scene is a UIPageViewController-subclassed PageViewController that conforms to the UIPageViewControllerDataSource protocol, much the way UITableViewController now does inherently for its data source and delegate.

At a minimum, a UIPageViewController needs only two things to get paging working:

  1. The plugged-in viewControllerAfter and viewControllerBefore data source methods.
  2. The initial view controller (or view controllers if you’re presenting two pages at at time).

Our page content is presented by a TableViewController. Rather than pre-populating a collection of controllers for all pages, I serve them up on-demand one at a time.

PageViewController maintains no state of its own as concerns which page we’re currently viewing. We leave that to each TableViewController via its section property. In viewDidLoad() we initialize the first Table View Controller’s section to zero, and the page view controller’s data source methods take it from there.

When UIPageViewController asks its data source for the previous or next page, we query the current view controller’s section value to do some bounds checking and initialize the new controller. If within bounds, we return the new controller; otherwise, we return nil.

Table View Controller

The reusability of this controller in two contexts hinges entirely on the optional section property:

  • When non-nil, as set only by the PageViewController, we can know the data is paginated and should only serve up one section, which one being determined by this section value.
  • When nil (i.e., left in its initial state), we know the controller is being used by the directly embedded container view, and needs to serve up data for all sections.

Functionally, this TableViewController is entirely a UITableViewDataSource, providing the bare minimum number of sections, rows per section, section title (not necessary, but useful), and cells. It calls upon an abstracted BasicTableDataModel for those values. This model is assigned by the PageViewController, which it receives from the MainViewController.

Anytime these methods reference the section, they use the sweet nil-coalescing operator to give priority to the optional section property, and failing that, the parameterized section or indexPath value.

Page Indicator

Actually managed within the PageViewController, the page indicator is made possible by implementing two additional UIPageViewControllerDataSource methods: presentationCount(for:) and presentationIndex(for:).

They’re straightforward enough. The only oddity is presentationIndex(for:) is called after viewControllers is initialized to an empty array, but before it is populated. Thus the reason for the nil and count > 0 checks.

Customization of the indicator, that is, its color, is another peculiarity. UIPageControl utilizes an “appearance proxy”, which implements the UIAppearance protocol.

For this project, it comes down to the three lines of code from viewDidLoad(), as given earlier and repeated here:

let pageControl = UIPageControl.appearance(whenContainedInInstancesOf: [PageViewController.self])
pageControl.pageIndicatorTintColor = .lightGray
pageControl.currentPageIndicatorTintColor = .black

If you were only to have one page indicator across your app or want them all to have the same colors, you can replace the first line with:

let pageControl = UIPageControl.appearance()

And, if follows, you can do that anywhere, such as in the AppDelegate.

Basic Table Data Model Protocol

An update to my original implementation addresses my glossing over how model data might work its way into the table view in the real world.

In this update, I add a BasicTableDataModel protocol and two conforming types (LatinTableDataModel and NumericTableDataModel) that present distinct data for use in the paged and non-paged views.

The MainViewController is now the keeper of the models, passing them down to the container view controllers via the segue method prepare(for:sender:). The PageViewController then passes its assigned model down to the individual TableViewControllers it instantiates.

Wrap Up

Adding a page view controller to your project is as simple as:

  1. Dropping a Page View Controller into your storyboard
  2. Connecting to a UIPageViewController-derived class that implements its data source protocol
  3. Employing an approach for serving up paged view controllers that fits with your use case, which really just comes down to providing a view controller with the page-specific model data it needs to display
  4. Optionally, configuring the page indicator by implementing a couple more data source methods and customizing it to a color suitable for your view.

By way of bonus material, you also got to see how to:

  • Reuse and tailor model data for a single view controller in multiple contexts
  • Use a segmented control to switch view contexts within a single controller.

Good stuff. Now, back to Netflix…

10 Comments