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

2

7

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

Holladay answered 24/10, 2015 at 11:57 Comment(5)
I have a same problem. Hope Apple add another function to UIViewControllerPreviewingDelegate.Verily
For time being, you can have a flag that tells you that the vc is peeking or not and set it in previewingContext(previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) and reset to false in previewingContext(previewingContext: UIViewControllerPreviewing, commitViewController viewControllerToCommit: UIViewController).Verily
Thanks I had similar thoughts and even made a protocol for all peek actions I thought would be good to be received on the being peeked view. Unfortunately the previewed view disappears before were called to pop the view so I still cannot distinguish between the peek disappearing and the peek transitioning into the popHolladay
As in viewWillDisappear is called before we properly popHolladay
@Verily that only works if you follow through and pop. If you peek then dont pop, the flag is never correctly resetEnrage
P
15

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.

Phenice answered 18/11, 2015 at 11:0 Comment(6)
Thanks, sounds really promising. I'll have a look and accept the answer once I've got it working! Thank you!Holladay
that works, but if one scrolls for previewActionItems it will have then .Ended even if we are still in peek modeCommodore
I wrote sample code in Swift: ShingoFukuyama / 3DTouchDetectStateOfPeekPopCancelDaedalus
Looks like on iOS 11 things have changed. I immediately get .ended while the gesture is still in progressNason
@ShingoFukuyama: You need no KVO as in your code. You can simply use target action.Craftsman
@Tim: You're right Tim. It appears that this has been completely broken by iOS11. I too get .ended immediately which has broken my code that used to work based off of clemens original answer. Sadly at this point it appears we're not able to know whether our view has been peeked and popped, or just peeked and then left. I guess we were always abusing this gesture recogniser for our own purposes, as no other recognisers are in contention it seems to just end immediately. I'm going to have to remove support for peeking in aspects of my app due to this.Holladay
T
1

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.

Trailblazer answered 20/9, 2018 at 5:41 Comment(1)
Thanks for the detailed reply, sorry it took me so long to reply. I've not been working on this app much lately but just recently got a crash that I've discovered was caused by more fallout from the peek breakages in iOS11. Unfortunately your suggested workflow won't work for my use case as I was wanting to do different tear down behaviour in viewDidDisappear when the peeked view was disappearing after a pop compared to disappearing after a peek. ViewDidDisappear is called on the peeked view BEFORE popping, and when canceling so there's no way to distinguish the two :(Holladay

© 2022 - 2024 — McMap. All rights reserved.