Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Detect when an iOS 3D Touch peek has finished (without a pop)

I just started adding basic 3D Touch functionality to my app, and the first attempt at adding it has gone well, seems fairly straightforward.

I was wondering however whether there was a way to detect that a peek had finished, and not gone into the pop.

The UIViewControllerPreviewingDelegate methods are good for telling you that a peek or pop is requested but I don't see a way to be told that the peek has ended and NOT gone into a pop.

Does the Peeked ViewController have a way of knowing it's peeked at the moment and going away as I guess this would be sufficient. Basically I have a segue that normally creates some things as it goes into the view, which if I peek into it would need to be undone if the user chooses to just end the peek without popping in. At the moment I can't seem to see a good way of detecting this case to be able to perform the required clean up.

Cheers

like image 324
jimbobuk Avatar asked Oct 24 '15 11:10

jimbobuk


2 Answers

When you register for previewing using registerForPreviewingWithDelegate(), this returns a context that conforms to the UIViewControllerPreviewing protocol. That protocol contains a reference to the gesture recognizer that is used in peeking/popping, called previewingGestureRecognizerForFailureRelationship. It is intended to use when other gesture recognizers might be recognized simultaneously, but you can also add your own object as a target to observe changes.

Now, when you are peeking, the state of this gesture recognizer will be .Changed. When you release without popping, the state will change to .Ended. When you do pop, the state will change to .Cancelled (I actually expected this to be the other way, but at least we can tell the difference). Importantly, this state changes before the viewDidDisappear of your peeked view controller is called, so you can adjust your flag in time.

like image 196
Toine Heuvelmans Avatar answered Nov 14 '22 08:11

Toine Heuvelmans


I had the same issue in my app where I needed to know when a view controller started and stopped being peeked at and came up with the following.

In order to monitor the lifecycle of a peek, you can track the lifecycle of the view controller being peeked at, starting with the view controller being created in previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController, and ending with its viewDidDisappear().

I created a callback handler in the view controller being peeked at, PeekingViewController,

var viewDidDisappearHandler: (()->())? = nil

and placed it in PeekingViewController's viewDidDisappear as such:

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    viewDidDisappearHandler?()
}

Back in OriginalViewcontroller, where we're peeking at PeekingViewController from, keep a weak reference to the instance of the view controller being peeked at like so:

weak var peekingViewController: PeekingViewController?

func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? {
    self.peekingViewController = PeekingViewController()
    return peekingViewController
}

Then, you can observe changes to the weak reference to the view controller being peeked at by filling in didSet in the peekingViewController instance like so:

weak private var peekingViewController: PeekingViewController? {
    didSet {
        peekingViewController?.viewDidDisappearHandler = { [weak self] in
            self?.peekingViewController = nil
        }
        if peekingViewController == nil { // Peek ended
            handlePeekEnded()
        } else { // Peek began
            handlePeekBegan()
        }
    }
}

Note: This logic will be triggered if a peak is performed and cancelled, but also if a peak is performed, the segue is completed, and then the newly presented PeekingViewController is popped.

If you need logic regarding the peek being cancelled to only be triggered with an incomplete peek, and not a full peak and then a dismissal, you can achieve this by:

including a new boolean in OriginalViewController to track if a view controller was completely pushed (OriginalViewController's viewDidDisappear will fire on a complete push, but not on a peek), and checking that boolean in peekingViewController's didSet, to determine if any action should be taken, and setting peekingViewController to nil in OriginalViewController's viewWillAppear.

like image 30
Dafurman Avatar answered Nov 14 '22 10:11

Dafurman