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
.
Last updated: Jun 12, 2020: Peek and Pop are deprecated in iOS 13 in favour of contextual menus. See Adding Context Menus in iOS 13.
Peeking and Popping
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:
Interface Builder Support for Peek and Pop
The release notes for Xcode 7.1 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:
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
- Register a view controller and one or more of its views for previewing.
- Use the
previewingContext:viewControllerForLocation:
delegate method to create the preview (peek). - Use the
previewingContext:commitViewController:
delegate method to transition (pop) to the previewed view controller.
Note: These methods are deprecated in iOS 13, you should migrate to context menus. See Adding Context Menus in iOS 13.
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:
-
The
UITraitCollection
class gains an extra property in iOS 9 to show if 3D Touch is available. To avoid crashing on pre-iOS 9 devices we do therespondsToSelector
dance first before checking the property. (Too bad we cannot use Swift API availability checking). -
Specify the view the user will touch to trigger the preview as the
sourceView
argument. In this case we use the table view. You can register more than one view for the same view controller. -
The
registerForPreviewingWithDelegate:
method returns a context object for managing the preview. The systems manages the life cycle of this object so you do not need to unregister when the view controller is deallocated. -
If you want to disable previewing for a view you can call
unregisterForPreviewingWithContext:
with the context returned when you registered.
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.
If the user presses harder they see the preview of our view controller:
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.