3D Touch Peek and Pop

In my last post I covered adding 3D Touch Quick Actions. This time I will look at adding peek and pop previewing of a view controller to my WorldFacts sample project. It is an example of a master-detail split view controller. The master view controller is table view controller of type UYLCountryTableViewController and the detail view controller is of type UYLCountryViewController.

Touching a country cell in the master table view first blurs the surrounding content and then when the user presses harder shows a preview of the country view controller. If the user continues to press the preview pops to the country view:

Peeking a view controller

Interface Builder Support for Peek and Pop

The release notes for the upcoming Xcode 7.1 (in beta at time of writing) include the news that you will be able to add peek and pop to storyboard segues directly in Interface Builder:

Interface Builder supports enabling Peek & Pop for segues. Peek & Pop segues will be omitted when running on OS versions prior to iOS 9.1.

That makes it almost too easy to add peek and pop to a storyboard segue:

Peek and Pop Segue

I could stop here but note the caveat in the release notes. This will only work for iOS 9.1 so until you want to drop support for iOS 9.0.x you will need to add code. That should be no big deal as the code to add support is trivial.

Steps to Support Peek & Pop

Registering

Let’s get started by checking if 3D Touch is available and then registering our view controller as the 3D Touch preview delegate. Add the following to the viewDidLoad method of our table view controller:

if ([self.traitCollection
     respondsToSelector:@selector(forceTouchCapability)] &&
    (self.traitCollection.forceTouchCapability == 
     UIForceTouchCapabilityAvailable))
{
    [self registerForPreviewingWithDelegate:self sourceView:self.view];
}

We also need to declare that our view controller conforms to the previewing delegate protocol:

@interface UYLCountryTableViewController ()
           <UIViewControllerPreviewingDelegate, ...>

Notes:

Peeking

When a user presses the source view we registered our view controller receives a call to the previewingContext:viewControllerForLocation: delegate method:

- (UIViewController *)previewingContext:
    (id<UIViewControllerPreviewing>)previewingContext
    viewControllerForLocation:(CGPoint)location {

The delegate method contains the location of the touch which we can use to get the indexPath of the table view cell that the user touched:

  NSIndexPath *indexPath = [self.tableView 
                            indexPathForRowAtPoint:location];

Then using the indexPath we can retrieve the country object and table view cell:

  Country *country = [self countryForIndexPath:indexPath];
  if (country) {
    CountryCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];

Once we have the table view cell we use the cell frame as the sourceRect for the previewing context. As we will see shortly the system uses this sourceRect to create the visual effect that tells the user they can peek:

    if (cell) {
      previewingContext.sourceRect = cell.frame;

We then create, configure and return our detail view controller for the preview. Note that there is a subtle problem here. Our detail view controller, a UYLCountryViewController, is usually embedded in a navigation controller and uses the title in the navigation bar to show the name of the country. So instead of creating a UYLCountryViewController for our preview we need to create the navigation controller that embeds the view controller. This is easy enough to do straight from our storyboard:

      UINavigationController *navController = [self.storyboard 
      instantiateViewControllerWithIdentifier:@"UYLCountryNavController"];
      [self configureNavigationController:navController
                              withCountry:country];
      return navController;
    }
  }

If any of the above steps failed we return nil to disable the preview:

  return nil;
}

When a user first presses on our registered view the content surrounding the sourceRect blurs to show that peeking is possible.

Blur

If the user presses harder they see the preview of our view controller:

Peek

If you want to adjust the size of the preview set the preferredContentSize property of the view controller.

Popping

When the user presses deeper on the peek view we transition with a pop to the view. We already have an instance of the view controller so in the previewingContext:commitViewController delegate method we present it as normal - in this case a show detail presentation:

- (void)previewingContext:
    (id<UIViewControllerPreviewing>)previewingContext
    commitViewController:(UIViewController *)viewControllerToCommit {

  [self showDetailViewController:viewControllerToCommit sender:self];
}

Preview Actions

When the user swipes upwards on a preview you can, if it makes sense, display quick actions to the user. You create the actions in the view controller that you are previewing by returning an array of preview actions (or groups of actions) from the method previewActionItems. For example, if we lazily instantiate the array of actions, it might look something like this:

- (NSArray<id<UIPreviewActionItem>> *)previewActionItems {
  return self.previewActions;
}

Then to create a UIPreviewAction, we specify the action title, style and a block to handle the action:

- (NSArray<id<UIPreviewActionItem>> *)previewActions {    
  if (_previewActions == nil) {
    UIPreviewAction *printAction = [UIPreviewAction
     actionWithTitle:@"Print"
               style:UIPreviewActionStyleDefault
             handler:^(UIPreviewAction * _Nonnull action,
                    UIViewController * _Nonnull previewViewController) {
      // ... code to handle action here
      }];
    _previewActions = @[printAction];
  }
  return _previewActions;
}

Note though that we cannot simply add the above code to our UYLCountryViewController class. As we saw when setting up the preview we are previewing a navigation controller that embeds our detail view controller. So if we want to add actions we would need to subclass the navigation controller. I will leave that as an exercise for the reader.

Sample Code

The updated sample code is in my Code Examples GitHub repository.

Further Reading

Never miss a post!

iOS Size Classes Cheat Sheet

Subscribe and get my free iOS Size Classes Cheat Sheet

Unsubscribe at any time.
No time to watch WWDC videos?

Sign up to get my iOS posts direct to your inbox and I will send you a free PDF of my iOS Size Classes Cheat Sheet.

Unsubscribe at any time.
Archives Categories
comments powered by Disqus