Lessons learned from 3D touch demo

Lessons learned from 3D touch demo

This weekend I spent significant amount of time working on the previously inactive 3D touch demo project. I added few tweaks, made little experiments and removed extraneous code from the repository. Below are some of the findings and gotchas I learned from touching this project after long time.

Lincoln_3D_touch

  • Detecting device for 3D touch [1]

First thing you need even before making an app feature to work with 3D touch is checking if current device (Or trackpad if running an app on the simulator) supports the 3D touch.


if self.traitCollection.forceTouchCapability  == UIForceTouchCapability.Available {
    print("Force touch does exist on this device")
} else {
    print("Force touch does not exists on this device")
}

The code above simply checks the forceTouchCapability enum on UIKit's traitcollection object and indicates if 3D touch is available, unavailable or unknown

  • Registering a view controller for preview [2]

    Once your app made sure that device supports 3D touch, next step is to register view controller under consideration for preview with self acting as a delegate


if self.traitCollection.forceTouchCapability  == UIForceTouchCapability.Available {
    self.registerForPreviewingWithDelegate(self, sourceView: self.view)
}

  • Calculating point offset for scroller views (e.g. TableView and CollectionView)

This one was such a headache. One of the tasks with showing preview controller when cell of either tableView or collectionView is tapped is to get the cell at that position. A simple code for doing it will look like this


  func previewingContext(previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? {
    // Find the location of point relative to tableView
    var updatedLocation = self.view.convertPoint(location, toView: self.tableView)
    // Check if index path exists at that point
    if let indexPath = tableView.indexPathForRowAtPoint(updatedLocation) {
    // Check if cell exists at the point
        if let cell = tableView.cellForRowAtIndexPath(indexPath) {
            // Do anything you want to do with cell
        }
    }
    return nil
}

In short, you have to compute several intermediate values before you can reach to get the valid cell instance

This is fine when tableView cells count is small enough and there is no vertical scrolling. However, things can get complicated when there are several cells and user scrolls the list. The problem is, value of updatedLocation in code example above is calculated relative to the self.view instance assuming tableView is static. However, as you scroll, this offset won't be correct at all

For example, consider this. You have tableView with 100 rows in it. Now, when user force touches 3rd cell, let's assume that the position of touch in the y direction is 100px. Now user scrolls the tableView and reached any arbitrary say 57th cell. Now this cell is positioned exactly to the same point as the third cell. When user force touches the cell, the index path it is going to get is for 3rd cell and not the 57th cell as expected.

It took me little while to figure out the solution, but it was quite simple. The trick here is to not only get the offset of touch in y-direction, but also the contentOffset associated with tableView. (Same principle applies if you are using collectionView instead). So this is how you will compute the actual touch point which will give you right cell at that position


// Find the location of point relative to tableView
var updatedLocation = self.view.convertPoint(location, toView: self.tableView + self.tableView.contentOffset.y)

And this should be it. Now you've found a dynamic touch point as user scrolls as opposed to static one.

Please remember, static one will always give you the same touch point irrespective of the amount of applied scroll

The iOS 3D touch demo is hosted on Github with demonstration and examples. Feel free to reach out to me with any questions you might have


  1. If the force touch is unavailable and if you want an app to work in similar way it would've worked on 3D touch enabled device, you can add a long-press gesture as an alternative to 3D touch. As far as preview controller is concerned, your custom implementation might be different from the one Apple already provides ↩︎

  2. Make sure to register the current view controller for preview with delegate set to it its own reference. I made this mistake in the beginning and spent several minutes trying to figure out why this feature was not working out in spite of running it on the 3D touch enabled device ↩︎