Dismiss modal if anywhere but UITableViewCell is touched - ios

I have a modal with a UITableView in it. I want to dismiss the modal if anywhere on the screen is touched except for the UITableViewCells inside the table view. However, you can't create an outlet for a UITableView cell to reference it in code. Currently I have this code:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch: UITouch? = touches.first
if touch?.view != WHAT TO PUT HERE? {
dismiss(animated: true, completion: nil)
}
}
I'm not sure how to reference the UITableViewCells. How should I do this?

I found a solution that works. I had to resize the tableView to fit the size of its content using this method:
override func viewDidAppear(_ animated: Bool) {
//Resize the tableView height to the height of its content.
tableView.frame.size.height = soundTableView.contentSize.height
//Set the origin of tableView to the bottom of the screen minus a 30px margin
//(this was my tableView's original position before resizing)
tableView.frame.origin.y = self.view.frame.size.height - soundTableView.frame.size.height - 30
}
Then I have this method which dismisses the modal if anywhere but the tableView is touched:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch: UITouch? = touches.first
if touch?.view != tableView {
dismiss(animated: true, completion: nil)
}
}

Related

SWIFT: touchesEnded not being called when I touch the screen

So I have a tableviewController called SettingsViewController, and it has the following touchesEnded function:
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
print("A")
super.touchesEnded(touches, with: event)
print("B")
if let touch = touches.first {
print("C")
let touchLocation = touch.location(in: view)
// 290 because the width of the view is 414, and the SettingsViewController width gets set to 0.7 * the view width in SlideInTransition. 0.7 * 414 is 289.8
if touchLocation.x > 290 {
dismiss(animated: true)
}
}
}
I made the print statement to see if it was being called, which it is not. This view controller is presented with a 'menu-esque' slide in custom transition. Is it possible that the bounds of the UIView of the new tablevc displayed with a custom transition is the problem somehow? Regardless, I have tried implementing super.touchesEnded, or adding all the override touches functions and tapped all over the screen with each simulation, but the touchesEnded never gets called. Any idea what's wrong and how to fix it? Thanks
I'm going to guess that you're putting the touchesEnded() func in your table view controller ... which is not going to work. The table view itself is consuming the touches.
This might work for you...
Add this UITableView subclass to your project:
class TouchableTableView: UITableView {
var callback: (() -> ())?
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
print("A")
super.touchesEnded(touches, with: event)
print("B")
if let touch = touches.first {
print("C")
let touchLocation = touch.location(in: self)
// not clear where you're trying to check for touch, so this may need to be changed
// 290 because the width of the view is 414, and the SettingsViewController width gets set to 0.7 * the view width in SlideInTransition. 0.7 * 414 is 289.8
if touchLocation.x > 290 {
// this code is in a UITableView subclass,
// so we can't call dismiss() from here
//dismiss(animated: true)
// instead, we tell the Controller to take action
callback?()
}
}
}
}
Then, in Storyboard, select the Table View in your Table View Controller and assign its Custom Class to TouchableTableView:
Now, touchesEnded() will be called in the custom TouchableTableView class (unless you tap on an interactive UI element in the cell, such as a button).
We then use the "callback" closure to tell the Controller about the touch. So, in your UITableViewController, add this in viewDidLoad():
class ExampleTableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
// we're in a UITableViewController,
// so make sure the tableView is our TouchableTableView class
// if it is, setup the closure
if let tv = tableView as? TouchableTableView {
tv.callback = {
// here is where we would, for example, call dismiss()
self.dismiss(animated: true, completion: nil)
}
}
}
}

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?

Tap on ParentViewController closes the ChildViewController

I have a ViewController which has add button which will open SideBarVC on right side of screen.
If I tap on my main ViewController screen again It should close my SideBarVC.
I tried
#IBAction func click_leistung(_ sender: UIButton) {
leistungList = self.storyboard?.instantiateViewController(withIdentifier: "leistungVC") as! leistungVC
leistungList.view.backgroundColor = .clear
leistungList.modalPresentationStyle = .overCurrentContext
self.present(leistungList, animated: true, completion: nil)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.myscrollView.endEditing(false)
leistungList.removeFromParentViewController()
leistungList = nil
}
First of all - you trying to remove fresh VC, but you should store pointer to old one to remove it. Create var to store pointer to this leistungList and then remove it from parent.
var storedController: UIViewController?
func show() {
// ...
storedController = presentedController
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Das keyboard ist weg
self.myscrollView.endEditing(false)
storedController.removeFromParentViewController()
storedController = nil
}
PS: read this article to understand how to manage child controllers https://developer.apple.com/library/content/featuredarticles/ViewControllerPGforiPhoneOS/ImplementingaContainerViewController.html

Is it possible to cancel a touch from inside touchesBegan or touchesMoved?

In my main view in a UIViewController I have a mapView and a another view (Let's say view A) that is above mapView. Both of them have frames equal to self.view.bounds. The view A is a rectangle that is resizable similar to those used to crop images. My goal here is to let the user specify an area on the map. So, I want the user to be able to zoom in an out of the map as well as change the rectangle width and height proportions since only letting the view A to be an unrealizable square would limit it too much.
I got this project from GitHub https://github.com/justwudi/WDImagePicker from which I am using the resizable rectangle functionality. In the second picture of the Github link, there's a rectangle with 8 dots and a shaded area outside. I want to be able to let the touch pass to the map which is behind the view A if the user touches on the shaded area. Only if the user clicks on the area inside the dots or on the dots (so that he wants to resize the rectangle) I want the view A to recognize the touch. So, I modified the code on touch on the view A and have this:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
if cropBorderView.frame.contains(touch.location(in: self)){
print("touch contains - touchesbegan")
//self.isUserInteractionEnabled = true
}
else{
print("Touch does not contain - touchesbegan")
self.touchesCancelled(touches, with: event)
//return
}
let touchPoint = touch.location(in: cropBorderView)
anchor = self.calculateAnchorBorder(touchPoint)
fillMultiplyer()
resizingEnabled = true
startPoint = touch.location(in: self.superview)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
print("inside touches moved")
if let touch = touches.first {
if cropBorderView.frame.contains(touch.location(in: self)){
print("touch contains - touchesmoved")
//self.isUserInteractionEnabled = true
}
else{
print("Touch does not contain - touchesmoved ")
self.touchesCancelled(touches, with: event)
//return
}
if resizingEnabled! {
self.resizeWithTouchPoint(touch.location(in: self.superview))
}
}
}
It is indeed recognizing the touch when I click inside and outside as I wanted, but it is not stopping the touch when I click outside. This means calling self.touchesCancelled(touches, with: event) is not working. Calling return gives a crash and does not work as well. Are there any solutions to this problem?
Thank you for your time and consideration.
touchesCancelled(_:with:) just a notification for UITouch, it will not work this way.
As far as I understand, you implemented touch handlers in your overlay UIView, if so, you can try to replace the call to self.touchesCancelled(touches, with: event) with cancelTracking(with:) function from UIControl class implementation:
else {
print("Touch does not contain - touchesmoved ")
self.cancelTracking(with event)
}
Update solution, based on hitTest:
I've checked possible solutions and it seems that you can use hitTest: to avoid unnecessary touch recognitions. The following example is Swift Playground, you can tap and drag touches and see what happens in the console:
import UIKit
import PlaygroundSupport
class JSGView : UIView {
var centerView = UIView()
override func didMoveToSuperview() {
frame = CGRect(x: 0, y: 0, width: 320, height: 480)
backgroundColor = UIColor.clear
centerView.frame = CGRect(x: 110, y: 190, width: 100, height: 100)
centerView.backgroundColor = UIColor.blue
addSubview(centerView)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
dump(event)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
if (hitTest(touch.location(in: self), with: event) != nil) {
print("Touch passed hit test and seems valid")
super.touchesCancelled(touches, with: event)
return
}
}
print("Touch isn't passed hit test and will be ignored")
super.touchesMoved(touches, with: event)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if centerView.bounds.contains(centerView.convert(point, from: self)) {
return centerView
}
return nil
}
}
class JSGViewController : UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.frame = CGRect(x: 0, y: 0, width: 320, height: 480)
let customView = JSGView()
view.addSubview(customView)
}
}
let controller = JSGViewController()
PlaygroundPage.current.liveView = controller.view

UIButton touch location inaccurate when UIView Animating/Rotating

Hello all you awesome peeps! I have been having an odd issue where if UIView is animated, in my case rotated, during the animation the subviews touch locations are inaccurate.
For example, if I keep clicking button1, sometimes button2 will be pressed or nothing will be pressed. I've searched on STO and other blogs, trying their methods, but nothing seems to work. My goal is to have buttons clickable when the view is rotating.
I thought it was a UIButton bug or issue, but even with imageViews, and touch delegates or added Gestures, I get the same thing. I feel like something simple, like the presentation.layer or something that needs to be added to the UIView animate function, but I'm kinda at a loss. Hope you can shed some insight! Thank you all in advance.
func updateRotateView ()
{
UIView.animateWithDuration(2,
delay: 0.0,
options: [UIViewAnimationOptions.CurveLinear, UIViewAnimationOptions.AllowUserInteraction],
animations: {
self.rotationView.transform = CGAffineTransformRotate(self.rotationView.transform, 3.1415926)
//Test A
//self.button1.frame = self.rotationView.convertRect(self.button1.frame, fromView:self.rotationView)
//self.button1.layer.frame = self.rotationView.layer.convertRect(self.button1.layer.frame, fromLayer: self.rotationView.layer)
}
completion: {finished in })
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?)
{
//Test B
let firstTouch = touches.first
let firstHitPoint = firstTouch.locationInView(self.rotationView)
if (self.button1.frame.contains(firstHitPoint)) {
print("Detected firstHit on Button1")
}
if (self.button1.layer.frame.contains(firstHitPoint)) {
print("Detected firstHit on Button1.layer")
}
if ((self.button1.layer.presentationLayer()?.hitTest(firstHitPoint!)) != nil) {
print("Detected button1.layer hit test")
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
//Test C
let lastTouch = touches.first!
let lastHitPoint = lastTouch.locationInView(self.rotationView)
if (self.button1.frame.contains(lastHitPoint)) {
print("Detected lastHit on Button1")
}
if (self.button1.layer.frame.contains(lastHitPoint)) {
print("Detected lastHit on Button1.layer")
}
if ((self.button1.layer.presentationLayer()?.hitTest(lastHitPoint!)) != nil) {
print("Detected button1.layer hit test")
}
}
extension UIButton {
//Test D
public override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView?
{
print("hitTest called")
}
public override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool
{
print("pointInside called")
}
}
Test A -> rotationView.presentationLayer value changes, but the buttons do not, and setting it to an updated frame, is not working as the button1 frame or layer.frame size values do not change.
Test B/C -> UIButton does not respond to touch delegates, but even with imageViews does not hit accurately. Also did try Gestures with same result.
Test D -> Hitpoint does get called, but most likely on the wrong button as well. Point inside only gets called few times and if I am able to press the correct button, but again very inaccurate.
Because you are animating a view. You should know that there is a presentation layer and a model layer when animating.
Once self.rotationView.transform = CGAffineTransformRotate(self.rotationView.transform, 3.1415926)
is called in the animation block, the transform of rotationView has already been set, and the animation you see is just the presentation layer(It is an id type but it should be CALayer).

Resources