Detecting UITouches while UIViewController is being shown as 3DTouch preview - ios

Is it possible to detect touches and get the location of a touch from a UIViewController which is being currently used as previewingContext view controller for 3D Touch? (I want to change the image of within the preview controller when the touch moves from left to right)
I've tried both touchesBegan and touchesMoved none of them are fired.
class ThreeDTouchPreviewController: UIViewController {
func getLocationFromTouch(touches: Set<UITouch>) -> CGPoint?{
guard let touch = touches.first else { return nil }
return touch.location(in: self.view)
}
//Not fired
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let location = getLocationFromTouch(touches: touches)
print("LOCATION", location)
}
//Not fired
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let location = getLocationFromTouch(touches: touches)
print("LOCATION", location)
}
}
Even tried adding a UIPanGesture.
Attempting to replicate FaceBook's 3D Touch feature where a user can move finger from left to right to change the current image being displayed.
Video for context: https://streamable.com/ilnln

If you want to achive result same as in FaceBook's 3D Touch feature you need to create your own 3D Touch gesture class
import UIKit.UIGestureRecognizerSubclass
class ForceTouchGestureRecognizer: UIGestureRecognizer {
var forceValue: CGFloat = 0
var isForceTouch: Bool = false
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
handleForceWithTouches(touches: touches)
state = .began
self.isForceTouch = false
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
handleForceWithTouches(touches: touches)
if self.forceValue > 6.0 {
state = .changed
self.isForceTouch = true
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
state = .ended
handleForceWithTouches(touches: touches)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
state = .cancelled
handleForceWithTouches(touches: touches)
}
func handleForceWithTouches(touches: Set<UITouch>) {
if touches.count != 1 {
state = .failed
return
}
guard let touch = touches.first else {
state = .failed
return
}
forceValue = touch.force
}
}
and now you can add this gesture in your ViewController in viewDidLoad method
override func viewDidLoad() {
super.viewDidLoad()
let gesture = ForceTouchGestureRecognizer(target: self, action: #selector(imagePressed(sender:)))
self.view.addGestureRecognizer(gesture)
}
Now you can manage your controller UI in Storyboard. Add cover view above UICollectionView and UIImageView in a center and connect it with IBOutlets in code.
Now you can add handler methods for gesture
func imagePressed(sender: ForceTouchGestureRecognizer) {
let location = sender.location(in: self.view)
guard let indexPath = collectionView?.indexPathForItem(
at: location) else { return }
let image = self.images[indexPath.row]
switch sender.state {
case .changed:
if sender.isForceTouch {
self.coverView?.isHidden = false
self.selectedImageView?.isHidden = false
self.selectedImageView?.image = image
}
case .ended:
print("force: \(sender.forceValue)")
if sender.isForceTouch {
self.coverView?.isHidden = true
self.selectedImageView?.isHidden = true
self.selectedImageView?.image = nil
} else {
//TODO: handle selecting items of UICollectionView here,
//you can refer to this SO question for more info: https://stackoverflow.com/questions/42372609/collectionview-didnt-call-didselectitematindexpath-when-superview-has-gesture
print("Did select row at indexPath: \(indexPath)")
self.collectionView?.selectItem(at: indexPath, animated: true, scrollPosition: .centeredVertically)
}
default: break
}
}
From this point you need to customize your view to make it look in same way as Facebook do.
Also I created small example project on GitHub https://github.com/ChernyshenkoTaras/3DTouchExample to demonstrate it

Related

Get touch type in hitTest() or pointInside()

I'm implementing a passThroughView by creating a transparent View on top and override hitTest().
passThroughView should consume touches from Apple Pencil and if touch type is not from pencil, it pass touches to the view underneath.
The problems are:
parameter "event" in hitTest contains no touch, so I can't check touch type in hitTest
I can get touch from touchesBegan and check touch type, but it get called only after hitTest returned true
I subclass UIWindow and override sendEvent() but this function also called after hitTest (and I don't know why)
class WindowAbleToKnowTouchTypes: UIWindow {
override func sendEvent(_ event: UIEvent) {
if event.type == .touches {
// This get called after Hittest
if event.allTouches!.first!.type == .pencil {
print("This touch is from Apple Pencil")
}
}
super.sendEvent(event)
}
}
Is there anyway to check touchType to decide to pass or consume touches?
I ended up using a different approach, it could be useful for many cases: If I can't get touchType in hitTest(), I can still get the touchType with GestureRecognize:
class CustomGestureRecognizer : ImmediatePanGesture {
var beganTouch : UITouch!
var movedTouch : UITouch!
var endedTouch : UITouch!
override func shouldReceive(_ event: UIEvent) -> Bool {
// You can check for touchType here and decide if this gesture should receice the touch or not
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
// Save touch for later use
if let firstTouch = touches.first {
beganTouch = firstTouch
}
super.touchesBegan(touches, with: event)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
// Save touch for later use
if let touch = touches.first {
movedTouch = touch
}
super.touchesMoved(touches, with: event)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
// Save touch for later use
if let touch = touches.first {
endedTouch = touch
}
super.touchesEnded(touches, with: event)
}
}
In the target function of the gestureRecognizer, you can get the UITouch by:
let beganTouch = customGesture.beganTouch
let touchType = beganTouch.touchType

Make UIScrollView give up UITouch

I have a drawing app with a canvas larger than the size of the phone screen. I want to implement scrolling with two fingers and drawing with one finger. So far I can make the scrolling work just fine but when it comes to drawing, the line begins and then the view where the drawing is loses control of the touch such that only the first part of the line is drawn. I think the scrollview takes control back. Dots can be draw just fine.
This is my subclassed UIScrollView
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touches = event?.touches(for: self) else { return }
if touches.count < 2 {
self.next?.touchesBegan(touches, with: event)
} else {
super.touchesBegan(touches, with: event)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touches = event?.touches(for: self) else { return }
if touches.count < 2 {
self.next?.touchesEnded(touches, with: event)
} else {
super.touchesEnded(touches, with: event)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touches = event?.touches(for: self) else { return }
if touches.count < 2 {
self.next?.touchesMoved(touches, with: event)
} else {
super.touchesMoved(touches, with: event)
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touches = event?.touches(for: self) else { return }
if touches.count < 2 {
self.next?.touchesCancelled(touches, with: event)
} else {
super.touchesCancelled(touches, with: event)
}
}
override func touchesShouldCancel(in view: UIView) -> Bool {
if (type(of: view)) == UIScrollView.self {
return true
}
return false
}
You will need a Long Press Gesture Recognizer connected to your ScrollerView, set with Min Duration 0 seconds, also to recognize only 1 Touch and Cancel touches in view option active.
You can find all these options under the Attributes Inspector on Interface Builder.
Please play a little with the Tolerance settings to fine tune the results.

Forward touch event from UIView, to its TableView inside it

I built a SplitView class that looks like this picture below:
As you can see the SplitView always have two subviews, therefore it has two properties that are leftView and rightView.
The job of the SplitView is to manage its subview size proportion.
If I do a swipe gesture to the left, it will move the separator location and it changes the size of each subviews, so it will look like this:
It works perfectly until I use UITableView as the leftView and rightView.
This is because the UITableView inside it is the one which will process the touch event, not the superview (which is SplitView)
And because the code to intercept and respond to the touch is in SplitView, it doesn't do anything if the UITableView inside it is the one that receives the touch event.
To do this I implement hitTest to make the SplitView the responder, not the UITableView inside it.
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return self
}
Now I can get the touch event in the SplitView even though the user swipe on the UITableView.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
self.startInfo = StartInfo(timestamp: touch.timestamp,
touchLocation: touch.location(in: self),
separatorLocation: separatorView.frame.origin)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first, let startInfo = startInfo {
let location = touch.location(in: self)
if moveHorizontally == nil {
let deltaX = abs(location.x - startInfo.touchLocation.x)
let deltaY = abs(location.y - startInfo.touchLocation.y)
if deltaX > 4.0 || deltaY > 4.0 {
moveHorizontally = deltaX > deltaY
}
}
if let moveHorizontally = moveHorizontally {
if moveHorizontally {
print("the user intends to adjust separator position")
adjustSeparatorViewPosition(usingTouchLocation: location)
} else {
print("the user intends to scroll the table view")
if rightView.frame.contains(location) {
rightView.touchesMoved(touches, with: event) // doesn't work
} else {
leftView.touchesMoved(touches, with: event) // doesn't work
}
}
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
startInfo = nil
moveHorizontally = nil
leftView.touchesEnded(touches, with: event)
guard let touch = touches.first else { return }
if touch.location(in: self).x < self.bounds.width / 2 {
UIView.animate(withDuration: 0.3) {
self.setSeparatorLocationX(to: self.separatorMinX)
}
} else {
UIView.animate(withDuration: 0.3) {
self.setSeparatorLocationX(to: self.separatorMaxX)
}
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
startInfo = nil
moveHorizontally = nil
leftView.touchesCancelled(touches, with: event)
}
As you can see in this code, I added a delay, just like UIScrollView does when swiping to get the user intention, whether the user want to scroll horizontally or vertically.
- If the swipe direction is horizontal, I want to adjust the separator location
- If the swipe direction is vertical, I want to forward the event to the UITableView inside it (for example the leftView)
But, forwarding the touch event using rightView.touchesMoved(touches, with: event) doesn't work.
How to forward the touch event from the SplitView to the UITableView inside it?

UIGestureRecognizer subclass-- touchesMoved automatically changes state to .changed

I am trying to create a UIGestureRecognizer subclass, and in touchesMoved I have no code, yet state is being set to .changed.
Does anyone know why this is happening? I need to be able to decide for myself when we are in the .changed state.
If I remove my touchesBegan function and never move to .began, this doesn't happen.
import UIKit.UIGestureRecognizerSubclass
class MyRecognizer: UIGestureRecognizer {
private var refView: UIView? {
return self.view?.window
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
if touches.count != 1 {
state = .failed
return
}
guard let location = touches.first?.location(in: refView) else {
self.state = .failed
return
}
state = .began
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
// there is no code here!
}
//This is how I'm debugging state
override var state: UIGestureRecognizerState {
get { return super.state }
set {
super.state = newValue
print("state = \(newValue.debugDescription)")
}
}
}
extension UIGestureRecognizerState: CustomDebugStringConvertible {
public var debugDescription: String {
switch self {
case .began: return "began"
case .possible: return "possible"
case .changed: return "changed"
case .ended: return "ended"
case .cancelled: return "cancelled"
case .failed: return "failed"
}
}
}
Your testing approach is flawed. I tried this (this is my test app's complete code):
import UIKit
import UIKit.UIGestureRecognizerSubclass
class MyRecognizer: UIGestureRecognizer {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
print(self.state.rawValue)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
print(self.state.rawValue)
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let g = MyRecognizer(target: self, action: #selector(grFired))
self.view.addGestureRecognizer(g)
// Do any additional setup after loading the view, typically from a nib.
}
#objc func grFired(_ g:UIGestureRecognizer) {
print("here", g.state.rawValue)
}
}
Then I dragged on the background view, to which the gesture recognizer is attached. My gesture recognizer never moved from the possible state.
Also note that your expectations may be incorrect ("I need to be able to decide for myself when we are in the .changed state"). The normal and correct behavior is that, having declared the state to be .began in touchesBegan, your touchesMoved will be called once with state .began and the gesture recognizer will immediately advance to .changed automatically. That is correct; if this is continuous gesture, it cannot be the case that a moved event should come in while we are in .began state without our proceeding to .changed. Again, here's a test:
import UIKit
import UIKit.UIGestureRecognizerSubclass
class MyRecognizer: UIGestureRecognizer {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
if touches.count != 1 {
state = .failed
return
}
print(self.state.rawValue)
self.state = .began
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
print("moved", self.state.rawValue)
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let g = MyRecognizer(target: self, action: #selector(grFired))
self.view.addGestureRecognizer(g)
// Do any additional setup after loading the view, typically from a nib.
}
#objc func grFired(_ g:UIGestureRecognizer) {
print("here", g.state.rawValue)
}
}

How do I get a CGPoint from a touchesBegan event?

In my AppDelegate, I want to detect when a tap event has been made to the status bar. In order to do so, I need to get the CGPoint from the event. How do I get it from this code?
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
super.touchesBegan(touches, withEvent: event)
let location = // how to get a CGPoint ????
let statusBarFrame = UIApplication.sharedApplication().statusBarFrame
if CGRectContainsPoint(statusBarFrame, location){
print("Status bar touched")
}else{
print("Not touched")
}
}
You're using Swift 2; you could retrieve the tap location as follows:
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent)
{
if let touch = touches.first as? UITouch
{
//The view you would like to get the tap location from.
let tapPoint = touch.locationInView(self.view)
let statusBarFrame = UIApplication.sharedApplication().statusBarFrame
if CGRectContainsPoint(statusBarFrame, tapPoint)
{
print("Status bar touched")
}
else
{
print("Not touched")
}
}
}
A UITouch object has a method locationInView: that allows you to find the location of a touch in a particular view. From the docs:
Returns the current location of the receiver in the coordinate system of the given view.
The view object in whose coordinate system you want the touch located. A custom view that is handling the touch may specify self to get the touch location in its own coordinate system. Pass nil to get the touch location in the window’s coordinates.
So try:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
super.touchesBegan(touches, withEvent: event)
guard let touch = touches.first else {
return
}
let location = touch.locationInView(nil)
let statusBarFrame = UIApplication.sharedApplication().statusBarFrame
if CGRectContainsPoint(statusBarFrame, location) {
print("Status bar touched")
} else {
print("Not touched")
}
}
This works regardless of device orientation
Swift 5.1
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
guard let touch = touches.first else {
return
}
let location = touch.location(in: nil)
}
You'd have to have a view that you touched. If you've managed to get your app delegate to receive touch events, you could try getting the locationInView: using the app delegates window.

Resources