I want to subclass UITapGestureRecognizer as TapRecognizer so that navigation between pages within my app are handled in a standardised way:
Dragging a UITapGestureRecognizer onto any navigational elements in the storyboard, setting their class as TapRecognizer, and referencing them within the View Controller as an IBOutlet (#IBOutlet var heroTapRecognizer: TapRecognizer!)
Initialising them like so:
self.heroTapRecognizer = TapRecognizer.init(pageId: 1, pageType: PageType.CategoryPage)
Then in TapRecognizer.swift:
class TapRecognizer: UITapGestureRecognizer {
var pageId:Int!
var pageType:PageType!
convenience init(pageId: Int, pageType: PageType) {
self.init()
self.pageId = pageId
self.pageType = pageType
self.addTarget(self, action: #selector(TapRecognizer.handleTap(_:)))
}
func handleTap(sender: UITapGestureRecognizer) {
if sender.state == .Ended {
print("Handle navigation based on pageId + pageType")
}
}
}
But the above isn't working. I'm new to Swift and have only previously used UITapGestureRecognizer's programmatically.
Note: User interaction is enabled on the UIView that the recogniser is associated with.
Dragging a UITapGestureRecognizer onto any navigational elements in the storyboard, setting their class as TapRecognizer, and referencing them within the View Controller as an IBOutlet
Okay, but then your init will never be called. If you want something special to happen, implement awakeFromNib.
Alternatively, implement init(coder:). For some reason this is not documented, but it is the initializer that is actually called.
Related
In a collection view, I create a gesture recognizer at class init time. In the viewDidLoad method, I then add the gesture recognizer to the collection view.
class ViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongGesture(gesture:)))
#objc func handleLongGesture(gesture: UILongPressGestureRecognizer) {
// some code
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.addGestureRecognizer(longPressGesture)
}
}
With this, the gesture recognizer does not work.
The fix is easy: it suffices to move the line with let longPressGesture to the viewDidLoad method and everything works as expected. However, I find it a bit surprising that the first version would not work.
Can anyone explain why the first version is not working? Is it because, when the gesture recognizer is created, the collection view is not yet ready to have gestures? So, what must a gesture recognizer know about its target in order to be created?
Good question. That's because you are trying to use self when not fully initialized.
Now, how to make that work with the way you wanted? Perhaps declare it lazily, like so:
private lazy var longPressGesture: UILongPressGestureRecognizer! = {
let gesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongGesture(gesture:)))
return gesture
}()
Edit: Quoting giorashc's answer from this question:
Due to swift's 2-phase initialization you need to initialize the
parent class before you can use self in the inheriting class.
In your implementation self is yet to be initialized by the parent
class so as you said you should move it to the init method of your
view controller and create the button after calling the parent's
initialization method
2 Phase Initialization SO Question & Answer.
I have a custom UIControl that has three subviews. Each of those subviews, I add a target:
button.addTarget(self, action: #selector(buttonTapped(clickedBtn:)), for: .touchUpInside)
Within that function buttonTapped, it does some special animations to do some transitions (It mimics the segmented control).
Now, within the ViewController that this custom UIControl exists in must know when it's touched. I created an #IBAction function that interacts with the touch events for the custom UIControl.
The problem is, that isn't possible (as far as I know). If I add a target touch event to the subviews, the parent touch events won't get called. To have the parent view called the #IBAction function, I must set all the subview's setUserInteractiveEnabledtotrue`. When I do that, the subview's touch event functions won't get called.
I need both touch event functions to be called. How can I do this? Or what's the best way to get around this?
Use delegates, add a protocol in your UIControl that needs to be implemented in your ViewController.
This way you can detect if a button is clicked in your UIControl and invoke a specific function in your VC.
For Example:
//YourUIControl.Swift
protocol YourUIControlDelegate {
func didTapFirstButton()
}
class YourUiControl : UIView { //I'm assuming you create your UIControl from UIView
var delegate : YourUIControlDelegate?
//other codes here
.
.
.
#IBAction func tapFirstButton(_ sender: AnyObject) {
if let d = self.delegate {
d.didTapFirstButton()
}
}
}
//YourViewController.Swift
extension YourViewController : UIControlDelegate {
func didTapFirstButton() {
//handle first button tap here
}
}
I see that there are a ton of these questions, and I think I'm following the accepted Swift 3 methodology, but I'm still getting nothing. I can see that the UITapGestureRecognizer has been attached. Here's my code:
let tileClick = UITapGestureRecognizer(target: self, action: #selector(GameManagement.initiateTileClick(_:)))
newView.addGestureRecognizer(tileClick)
newView.isUserInteractionEnabled = true
func initiateTileClick(_ sender: UITapGestureRecognizer) {
print("initiate tile click")
}
A few things to note:
1) The view that I'm attaching the gesture recognizer to has a two views and a label within it that each cover the entire frame of the view, however, I tried attaching the recognizer to the label, which is the topmost child item and it still doesn't work.
2) Both the function that adds the recognizer and the function that is called on the tap are contained in an NSObject file. I have a variety of interconnected functions that I want to be able to call from multiple view controllers and would prefer to keep this in the separate NSObject file. The process worked when I had everything in a UIViewController file and stopped working when I moved the functions to the NSObject file.
3) I've tried changing GameManagement.initiateTileClick to self.initiateTileClick or just initiateTileClick and none of those worked.
If you are putting your views inside NSObject subclass then these views will lose their behaviors for UIResponder which manages the UI interactions as I am not able to see how you are adding these views to interface.
As you said, it was working inside ViewController because it manages view hierarchy and responder chain.
The solution would be to write extensions to separate code or better abstractions.
extension YourViewController {
newView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(GameManagement.initiateTileClick(_:))))
newView.isUserInteractionEnabled = true
func initiateTileClick(_ sender: UITapGestureRecognizer) {
print("initiate tile click")
}
}
Giving you an idea how the tap recogniser works.
Firstly add Tap gesture recogniser to your view controller. You have to put the object here as shown in the image.
Then control+drag the tap gesture object to your view and select delegate.
Then control+drag the recogniser to your swift file and action will be like this.
#IBAction func tapGesture(_ sender: UITapGestureRecognizer) {
}
Now you must have seen when you give some input to a text field, the keyboard appears. But if you press outside the text field, that is anywhere in the view, the keyboard hides. This is because of the tap gesture recogniser.
Consider you have a text field such that if you click in that text field, keyboard is appeared. But when you tap outside the textfield, the keyboard must hide.
Add this delegate
UITextFieldDelegate
Implement this:
#IBOutlet var phoneText: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
exampleText.delegate = self
}
#IBAction func tapGesture(_ sender: UITapGestureRecognizer) {
exampleText.endEditing(true)
}
Obviously,this function is instance method.
func initiateTileClick(_ sender: UITapGestureRecognizer) {
print("initiate tile click")
}
-
UITapGestureRecognizer(target: self, action:#selector(GameManagement.initiateTileClick(_:)))
but thisGameManagement.initiateTileClick(_:) looks like a class is calling a class method!The target should be the caller of method.self can't call GameManagement.initiateTileClick(_:).
I've got a tableView inside of a pageViewController and when swiping on a cell to bring up the option to delete the cell the gesture is only recognized under certain circumstances, say you swiped very quickly and aggressively.
I imagine this is happening because it's not sure whether the swiping gesture is meant for the pageView or the tableView. Is there a way to specifically determine where the swipe gesture is happening to enable a nice smooth display of the delete button?
Theory:
Both UIPageViewController and UITableView are implemented using UIScrollView, where UIPageViewController embeds UIScrollView and UITableView is a subclass of UIScrollView
UITableView also uses a couple of UIPanGestureRecognizers to bring in all the magic. One of these is UISwipeActionPanGestureRecognizer which handles the swipe to delete actions.
This issue is caused because UIPageViewControllers UIPanGestureRecognizer wins in the conflict with the UITableViews UISwipeActionPanGestureRecognizers.
So we have to some how tell UIPageViewController to ignore gestures if UITableViews UISwipeActionPanGestureRecognizer are in action.
Luckily there is something already provided by UIGestureRecognizer.
UIGestureRecognizer's require(toFail otherGestureRecognizer: UIGestureRecognizer) creates a relation between the two gesture recognizers that will prevent the gesture's actions being called until the other gesture recognizer fails.
So all we had to do is fail UIPageViewControllers embedded UIScrollviews panGestureRecognizer when UITableViews UISwipeActionPanGestureRecognizer are triggered.
There are two ways you could achieve this.
Solution 1: Add a new gesture recognizer to table view and Mimic UISwipeActionPanGestureRecognizer. And make UIPageViewController panGesture require to fail this new gestureRecognizer
Solution 2 (A bit dirty): Make a string comparision to the UITableView's UISwipeActionPanGestureRecognizer and make UIPageViewController panGesture require to fail this new gestureRecognizer
Code:
Solution 1
A helpful utility to get UIPageViewControllers embedded UIScrollView
extension UIPageViewController {
var scrollView: UIScrollView? {
return view.subviews.first { $0 is UIScrollView } as? UIScrollView
}
}
Add the below code to UIViewController holding the UITableView and call it from viewDidLoad()
func handleSwipeDelete() {
if let pageController = parent?.parent as? UIPageViewController {
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: nil)
gestureRecognizer.delaysTouchesBegan = true
gestureRecognizer.cancelsTouchesInView = false
gestureRecognizer.delegate = self
tableView.addGestureRecognizer(gestureRecognizer)
pageController.scrollView?.canCancelContentTouches = false
pageController.scrollView?.panGestureRecognizer.require(toFail: gestureRecognizer)
}
}
And finally delegate methods
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard let panGesture = gestureRecognizer as? UIPanGestureRecognizer else {
return false
}
let translation = panGesture.translation(in: tableView)
// In my case I have only trailing actions, so I used below condition.
return translation.x < 0
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return otherGestureRecognizer.view == tableView
}
Solution 2 (A bit dirty)
A helpful utility to get UIPageViewControllers embedded UIScrollView
extension UIPageViewController {
var scrollView: UIScrollView? {
return view.subviews.first { $0 is UIScrollView } as? UIScrollView
}
}
Add the below code to UIViewController holding the UITableView and call it from viewDidLoad()
func handleSwipeDelete() {
guard let pageController = parent as? UIPageViewController else {
return
}
pageController.scrollView?.canCancelContentTouches = false
tableView.gestureRecognizers?.forEach { recognizer in
let name = String(describing: type(of: recognizer))
guard name == "_UISwipeActionPanGestureRecognizer" else {
return
}
pageController.scrollView?
.panGestureRecognizer
.require(toFail: recognizer)
}
}
I had the same problem. I found a solution that works well.
Put this in your UIPageViewController's viewDidLoad func.
if let myView = view?.subviews.first as? UIScrollView {
myView.canCancelContentTouches = false
}
PageViewControllers have an auto-generated subview that handles the gestures. We can prevent these subviews from cancelling content touches. The tableview will be able to capture swipes for the delete button, while still interpreting swipes that fail the tableview's gesture requirements as page swipes. The delete button will show in cases where you hold and swipe or swipe "aggressively."
You can set delaysContentTouches to false on the tableView itself as well. This solution worked for my collection view's UISlider elements.
See Swift 4.0 code below:
yourTableView.delaysContentTouches = false
I have found a working solution by reassigning UIScrollView's panGestureRecognizer's delegate to my class and ignoring the original delegate when a right-to-left pan is detected. I used method swizzling for that.
Please check my sample project: https://github.com/kambala-decapitator/SwipeToDeleteInsidePageVC
In case you have a tableView in the first or the last viewController inside of your UIPageViewController maybe think of disabling the bouncing at the end of your UIPageViewController first and then implement this solution.
In case you don't know how to disable the bounce of the UIPageViewController, check out my answer here.
I have a tap gesture on a UIImageView within a class that extends UITableViewCell. This code should work, I don't see why it doesn't. The only thing I am iffy on is what the "target" should be - should it be the profileImage, or the overall ViewController that things are in?
#IBOutlet weak var profileImage: UIImageView!
var vc: TweetsViewController? = nil
override func awakeFromNib() {
super.awakeFromNib()
let tapGester = UITapGestureRecognizer(target: vc, action: Selector("handleTapGester:"))
tapGester.delegate = self
profileImage.addGestureRecognizer(tapGester)
}
func handleTapGester(tapGesture: UITapGestureRecognizer) {
print("*******hi*******")
vc?.performSegueWithIdentifier("showProfile", sender: nil)
}
And for the record, as this may seem like a relevant error, I initialize vc when the table cell loads.
The target should be the object that will handle the tap gesture and the handleTapGester function should be inside the object class you specified as the target, not inside the UITableViewCell subclass.
You also need to enable user interaction on the UIImageView by saying:
imageView.userInteractionEnabled = true
Why not just add a tap gesture recogniser to the view, then when called query indexPathForRowAtPoint to find out which cell is being tapped?
If you know the cell you can then determine if the UIImage is being tapped and make your call to performSegueWithIdentifier from there.
If it's tapped on a cell that you're not interested in let it fall through and be handled by the table by calling cancelsTouchesInView on the recogniser.