iOS 3D Touch - Peek and pop actions (Part 2)

This is the second part of the series of 3 posts on how to use 3D force touch on an iOS. Posts are as follows

To give you a glimpse of this demo, let's begin with a little demo presentation.

Peek_and_pop_actions_demo

In this demo we are going to create a list of former US presidents and use 3D touch peek and pop actions to get more information about them.

Let's begin with creating object model for President,


struct JKPresidentInfo {
    let name: String
    let birthDate: String
    let deathDate: String
    let birthPlace: String
}
// Now let's make an array holding objects of type JKPresidentInfo

var listOfPresidents: [JKPresidentInfo] = [JKPresidentInfo(name: "Lincoln", birthDate: "02/1809", deathDate: "04/1865", birthPlace: "Hardin County"), JKPresidentInfo(name: "Roosevelt", birthDate: "01/1882", deathDate: "04/1945", birthPlace: "Hyde Park"), JKPresidentInfo(name: "Clinton", birthDate: "08/1946", deathDate: "N/A", birthPlace: "Hope"), JKPresidentInfo(name: "Obama", birthDate: "08/1961", deathDate: "N/A", birthPlace: "Honolulu")]

A thing to note before we begin,

  1. There is possibility that device may not be 3D touch enabled. In that case you will have to add alternate to 3D touch which includes but not limited to long press recognizer gesture.

lazy var longPress: UILongPressGestureRecognizer = {
    let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress))
    return longPressGesture
}() 
// Add it to self.view in case 3D touch is not enabled.
self.view.addGestureRecognizer(longPress)

This long press gesture can be added to self.view in case device does not have 3D force touch capability or user toggles the 3D touch capabilities through settings. 3D touch capability can be detected in viewWillAppear or by overriding traitCollectionDidChange method. This method gets called every time user enables/disables 3D touch capabilities.


override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    self.check3DTouchEnabledFlag()
}
    
override func traitCollectionDidChange(previousTraitCollection: UITraitCollection?) {
    self.check3DTouchEnabledFlag()
}

func check3DTouchEnabledFlag() {
    if self.traitCollection.forceTouchCapability  == UIForceTouchCapability.Available {
        self.registerForPreviewingWithDelegate(self, sourceView: self.view)
        self.longPress.enabled = false
        print("Force touch does exist")
    } else {
        self.longPress.enabled = true
        print("Force touch does not exists on this device")
    }
}

This list of presidents looks like this,

list_of_presidents

Now there are two actions attached to it.

  • When user lightly touches the president name
    This action is used as a preview. On this screen user can briefly preview the president info and do additional actions without having to go to actual president details page.

    Here we will assume user can like the president. So we will present that options from the bottom of the page and user can select it. This can be implemented with following delegate method. In order to use it you will have to conform to UIViewControllerPreviewingDelegate protocol.


// MARK: - 3D touch delegate method.
func previewingContext(previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? {
    if (self.presentedViewController?.isKindOfClass(JKLongPress3DPreviewViewController) == true) {
        return nil
    }
        
    var updatedLocation = tableView.convertPoint(location, toView: self.view)
    updatedLocation = CGPoint(x: location.x, y: location.y - 64)
    if let indexPath = tableView.indexPathForRowAtPoint(updatedLocation) {
        if let cell = tableView.cellForRowAtIndexPath(indexPath) {
            tableView.selectRowAtIndexPath(indexPath, animated: false, scrollPosition: UITableViewScrollPosition.None)
            previewingContext.sourceRect = CGRectMake(cell.frame.origin.x, cell.frame.origin.y + 64, cell.bounds.width, cell.bounds.height)
            let president = listOfPresidents[indexPath.row]
            let selectedPresidentPreviewViewController = JKLongPress3DPreviewViewController()
                selectedPresidentPreviewViewController.selectedPresident = president
                selectedPresidentPreviewViewController.presidentLiked = self.presidentLikedWithName(president.name)
                selectedPresidentPreviewViewController.presidentFavoritedAction = { president in
                    if (self.presidentLikedWithName(president.name) == true) {
                        self.favoritedList.removeObject(president.name)
                    } else {
                        self.favoritedList.append(president.name)
                    }
                    self.tableView.reloadData()
                }
                return selectedPresidentPreviewViewController
            }
        }
        return nil
    }

Where JKLongPress3DPreviewViewController is the preview controller where user can choose an additional actions.

You may customize this preview controller the way you want, but please be aware that this view appears as a foreground view fading the original view behind it. You can then provide any number of list actions on this page and specify which actions they should trigger. This can be done by overriding previewActionItems method.


override func previewActionItems() -> [UIPreviewActionItem] {
    let actionTitle = presidentLiked == true ? "Unlike": "Like"
    let action1 = UIPreviewAction(title: actionTitle, style: .Default) { (action, controller) in
        if let president = self.selectedPresident {
            print("President \(president.name) was liked by the user")
            if let presidentFavoritedBlock = self.presidentFavoritedAction {
                presidentFavoritedBlock(president)
            }
        }
    }
        
    let action2 = UIPreviewAction(title: "Cancel", style: .Destructive) { (action, controller) in
        print("Cancel Action Selected")
    }
        
    let actionGroup = UIPreviewActionGroup(title: "More Actions", style: .Default, actions: [action1, action2])
        
    let finalArray = NSArray.init(object: actionGroup)
    return finalArray as! [UIPreviewActionItem]
}

To explain the above code briefly, we override the method previewActionItems to provide the list of UIPreviewActionItem. We create two actions. One for liking the president photo and another for cancel operation. These action items allow us to specify which action should take place upon selecting that option.

  • When user heavily touches the president's name from the list

// MARK: - 3D touch delegate method.
func previewingContext(previewingContext: UIViewControllerPreviewing, commitViewController viewControllerToCommit: UIViewController) {
    if let previewVC = viewControllerToCommit as? JKLongPress3DPreviewViewController, selectedPresident = previewVC.selectedPresident {
        if let destinationVC = JK3DTouchDestinationViewController(president: selectedPresident) {
            self.showViewController(destinationVC, sender: self)
        }
    }
}

This action takes user directly to the page which shows detailed president info. This is same as user simply clicking the president name from the list.

In case where 3D touch is disabled/not available you can simply use long press gesture recognizer to detect light press and present appropriate preview controller when user can perform additional actions.

Note: The full project for 3D touch demo is Available on Github