Different behavior between addTarget and addGestureRecognizer - swift

I have a function that creates a button with a selector function as a target. The address of a button gets passed to handleSelectPhoto.
lazy var image1Button = createButton(selector: #selector(handleSelectPhoto))
func createButton(selector: Selector) -> UIButton {
let button = UIButton(type: .system)
button.addTarget(self, action: selector, for: .touchUpInside)
return button
}
#objc func handleSelectPhoto(button: UIButton) {
// Do something with button, this works
}
Now, I am trying to change the class of the above from UIButton to UIImageView like the following,
lazy var image1Button = createButton(selector: #selector(handleSelectPhoto))
func createButton(selector: Selector) -> UIImageView {
let view = UIImageView()
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: selector))
view.isUserInteractionEnabled = true
return view
}
#objc func handleSelectPhoto(button: UIImageView) {
// HERE, button does not get passed
}
With the above changes, in handleSelectPhoto, button instance is not correct. I can not read it as UIImageView type.
If I add a selector function using addGestureRecognizer, does it behave differently than adding a selector function using addTarget, in terms of how selector function is executed with parameters? Maybe I am not understanding how this selector function works...

Adding a target to something like UIGestureRecognizer or UIButton only passes one parameter to the selected function. This parameter depends on the type you are about to add the target on.
In your case the first code snippet works because you are adding a target to an UIButton, so your selected function gets passed this UIButton instance.
In your second scenario you add the target to an UITapGestureRecognizer, so the passed instance will be exactly this gesture recognizer, which cannot be of type UIImageView.
So the difference from the target parameter perspective between UIGestureRecognizer and UIButton is no difference. They both pass their instances to the selected function.
From the UIView subclass perspective there is the difference that UIGestureRecognizer is not a subclass of UIView, but UIButton is. That's why you can just use the passed UIButton instance in your first snippet. In the second snippet you need use the view property of UIGestureRecognizer.
guard let imageView = gestureRecognizer.view as? UIImageView else { return }
Besides your actual question it seems important to clarify how to write #selectors correctly. You're doing it correct already. No change necessary. Some may say you need to add (_:) or : to your selector like so: #selector(handleSelectPhoto(_:)) but this isn't true. In general, you only need to add these special characters when you are selecting a method which has an overload method with a different amount of parameters, but the same base name.

You should make your tell while setting the selection that your function will accept a parameter by adding : at the end of method name.
lazy var image1Button = createButton(selector: #selector(handleSelectPhoto:))
UIKit will automatically understand that the selector methods parameter will be of type UITapGestureRecognizer. Now rewrite the below method like this and you will be good to go.
#objc func handleSelectPhoto(gesture: UITapGestureRecognizer) {
if let buttonImageView = gesture.view as? UIImageView {
//Here you can make changes in imageview what ever you want.
}
}

Related

How can I define the touchUpInside action for KCFloatingActionButton?

I want to define action for my KCFloatingActionButton. UITapGestureRecognizer is defined for the fab button in ViewController, but I want to do this action in manager class or on another page. I got the KCFloatingActionButton view, but I can't give it a click action.When I click on KCFloatingActionButton I don't want to define a new UITapGestureRecognizer with addGestureRecognizer ,I want it to take the action it is defined.
I can do this for UIButton or UIBarButtonItem as follows
button.sendActions(for: .touchUpInside)
barButtonItem.target?.perform(barButtonItem.action, with: nil)
How can I do this action for KCFloatingActionButton ?
You can try something like this:
button.addTarget(self, action: #selector(handleButtonAction), for: .touchUpInside)
#objc func handleButtonAction(){
//Put your logic here
}
The self in addTarget refers where will be the action to be executed, in this case will be in the current ViewController.
The #selector(handleButtonAction) is the function that will be executed when the user touch the button, in this case will be an Objective-C function (that's why is use the "#selector").
The .touchUpInside is the event that has to be triggered to execute the function.
And finally the function itself #objc func handleButtonAction() { } will execute all the actions that you define for the touch up event.
the #objc attribute comes in: when you apply it to a class or method it instructs Swift to make those things available to Objective-C as well as Swift code.

Gesture Recognizer and UIImageView With Custom Property

My app uses images which can have various statuses so I am using custom properties as tags. This works ok, but my tap gesture recognizer can't seem to access these properties. When the image is tapped, I need the action to depend on the state of these properties. Is there a way the gesture recognizer can read these custom properties from the tapped subclassed UIImageView or should I take a different approach? Thanks!
public class advancedTagUIImageView: UIImageView {
var photoViewedStatus: Bool?
var photoLikedStatus: Bool?
}
viewDidLoad() {
let imageView = advancedTagUIImageView(frame:CGRect(origin: CGPoint(x:50, y:50), size: CGSize(width:100,height:100)))
imageView.image = UIImage(named: dog.png)
imageView.photoViewedStatus = false
imageView.photoLikeStatus = false
imageView.tag = 7
imageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(soundTapped)))
view.addSubview(imageView)
}
#objc func soundTapped(gesture: UIGestureRecognizer) {
let photoTag = gesture.view!.tag // this works great
let isPhotoLiked = gesture.view!.photoLikeStatus // this doesn't work
// do whatever
}
Swift is strongly typed. The type of the gesture.view property is UIView which doesn't have the properties defined in your advancedTagUIImageView class. This is because you could theoretically also attach your UITapGestureRecognizer to any other type of view. In which case the program would crash on your soundTapped method, because you're just assuming that gesture.view is an advancedTagUIImageView which might not always be the case.
For the compiler to let you access these properties you need first check if gesture.view is really your sublcass like this:
if let photoView = (gesture.view? as? advancedTagUIImageView) {
// you can access your tags here
let isPhotoLiked = photoView.photoLikeStatus
} else {
// you might want to handle the case that the gesture was invoked from another view. If you're certain this should not happen, maybe just throw an assertion error to get notified in case it still does.
}
PS: According to the Swift API Design Guidelines type names should be capitalized, so in your case it should be AdvancedTagUIImageView. Not following these guidelines might not crash your program, but doing so might make your life a lot easier should you ever need to write code together with other people.

passing multiple closures as parameters to a function in swift

So I am trying to pass two closures to a function which creates a subview. The main part of function that takes closures as arguments and calls them is as follows:
///goButton and cancelButton are class level variables
var goButton = UIButton(type: .system)
var cancelButton = UIButton(type: .system)
func addSubViewWithAction(_ titleString:String, _ button1Text:String, _ button2Text:String, closureYes:#escaping ()->(), closureNo:#escaping ()->()) {
goButton.actionHandle(controlEvents: UIControlEvents.touchUpInside,
ForAction:closureYes)
cancelButton.actionHandle(controlEvents: UIControlEvents.touchUpInside,
ForAction:closureNo)
}
here is how I am trying to call it.
addSubViewWithAction("Hide Penguin here?","Yes","Cancel", closureYes: switchPlayers, closureNo: deletePenquin)
The problem is that it calls the deletePenguin function for both the buttons and never calls the switchPlayers function.
here is how I am adding buttons to main view through subview
//v here is a UIView object
//Add all buttons and text to subView
v.addSubview(titleField)
v.addSubview(goButton)
v.addSubview(cancelButton)
v.layer.cornerRadius = 8
//Add subView to main view
window.addSubview(v)
The problem is that actionHandle somehow works statically, so it will overwrite any previous assignment with the most recent one.
You could do the following (no complete code solution here, only pseudo-code):
Subclass UIButton
Add an instance variable that holds the closure to be executed
Add an instance (helper) func that acts as the target for your event and inside executes the closure above
Create a func that takes the closure to be excecuted as a parameter. Inside,
Assign your instance variable with the provided closure
Call addTarget(_:action:for:) with your helper func as the target
If you want to support different UIControlEvents, you'll have to improve those steps a little, maybe by using a dictionary that maps the event to the closure or so.

How do I pass view as an argument (swift)

I'm trying to move my fully functioning swipe gesture code into a view model to clean up the view controller but the code uses a lot of self and view references, so I suppose I need to pass along the view or UIView.self as an argument when calling the function. Can't get it to work though. Tried:
vm.swipeCode(myView: self.view)
func swipeCode(myView: UIView) {...
But it crashes. After some research I also tried variations of inout and & but to no avail. Here's the full swipe code (it references back to the view controller but I will move those as well when things start working :) )
var myVC = RecipesViewController()
func swipeCode(myView: UIView) {
//SWIPE RIGHT
let swipingRight = UISwipeGestureRecognizer()
swipingRight.addTarget(self, action: #selector(myVC.swipeRight))
swipingRight.direction = .right
swipingRight.delegate = self as? UIGestureRecognizerDelegate
swipingRight.cancelsTouchesInView = false
myView.isUserInteractionEnabled = true
myView.addGestureRecognizer(swipingRight)
//// ALLOW SWIPE LEFT ////
let swipingLeft = UISwipeGestureRecognizer()
swipingLeft.addTarget(self, action: #selector(myVC.swipeLeft))
swipingLeft.direction = .left
swipingLeft.delegate = self as? UIGestureRecognizerDelegate
swipingLeft.cancelsTouchesInView = false
myView.isUserInteractionEnabled = true
myView.addGestureRecognizer(swipingLeft)
}
The crash is probably because of these two lines:
swipingRight.addTarget(self, action: #selector(myVC.swipeRight))
swipingLeft.addTarget(self, action: #selector(myVC.swipeLeft))
You passed myVC.swipeLeft as the action, but self as the target, so the gesture recogniser will try to find a swipeLeft method in self, which doesn't exist.
You should always ensure that the action is a member of the target:
swipingRight.addTarget(myVC, action: #selector(myVC.swipeRight))
swipingLeft.addTarget(myVC, action: #selector(myVC.swipeLeft))
You should simply make a UIView or UIViewController inherited class instead if your methods use a lot of self then simply make your controllers or views of that class.
class CustomViewController:UIViewController {
func swipeCode(myView: UIView) {
// Your code here
}
// Any other methods your code might need
}
class RecipesViewController:CustomViewController {
// Whatever else your view controller is made from
}
Or if your code swiping works in every view controller you could also extends the UIViewController to add those functionnalities

"classname has no member functionname" when adding UIButton target

Super newb in Swift and iOS development here.
I am following this tutorial about implementing a custom control in a single view iOS app. It's a Swift 2 tutorial, but so far I'm doing OK transposing everything to 3 as I go (I use XCode 8 Beta).
I have a custom class, RatingControl, connected to a View in the storyboard.
In the class's constructor, I create a button:
let button = UIButton(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
button.backgroundColor = UIColor.red()
Then, I try to assign an action to the button. The tutorial says I should do it like so:
button.addTarget(self, action: #selector(RatingControl.ratingButtonTapped(_:)),
for: .touchDown)
and then create, in the same RatingControl class, the method:
func ratingButtonTapped(button: UIButton) {
print("Button pressed 👍")
}
But when I compile, it complains:
type "RatingControl" has no member "ratingButtonTapped"
I've made 100% sure the function is there, in the class, and properly named. Full source
Is there something obvious I'm missing?
What I tried:
Added #objc to the class definition as per this answer (but that seems weird for a Swift-only thing, no?)
Made ratingButtonTapped() explicitly public (but that doesn't look like it should be necessary)
Fiddled around with strings instead of selectors, button.addTarget(self, action: "RatingControl.ratingButtonTapped", for: .touchDown) and many more, but that just crashes it later.
In Swift 3, method reference for: func ratingButtonTapped(button: UIButton) becomes ratingButtonTapped(button:).
So, using #selector(RatingControl.ratingButtonTapped(button:)) also work.
And if you want to keep #selector(RatingControl.ratingButtonTapped(_:)), then you need to declare the ratingButtonTapped method as:
func ratingButtonTapped(_ button: UIButton) { //<- `_`
print("Button pressed 👍")
}
And if you have only one ratingButtonTapped method in the class, you can address the selector as #selector(RatingControl.ratingButtonTapped) or simply (from inside RatingControl class) #selector(ratingButtonTapped).
This happened because Swift 3 has changed the way it handles the first parameter name. In Swift 3, all parameter names must be used when calling a function unless an explicit _ was declared as the parameter name.
What you used as your selector was fine if you had declared your function as:
func ratingButtonTapped(_ button: AnyObject) {
print("Button pressed 👍")
}
You could also have used this as your selector:
#selector(RatingControl.ratingButtonTapped(button:))
Added #objc to the class definition as per this answer (but that seems
weird for a Swift-only thing, no?)
Your code may be in Swift, but you are interacting with the Objective-C runtime when you are coding for Cocoa Touch (iOS framework). The selector is a function that needs to be visible to the Objective-C runtime. You get this for free most of the time because you are implementing this in a class that ultimately inherits from NSObject (like UIViewController). If you have a Swift only class that doesn't inherit from NSObject, then you can add #objc to make the class and methods visible to the Objective-C runtime.
If you want to call an action that is in your View Controller from a Different Class you can try this.
Use ViewController() for your target. Use ViewController.functionName for your selector. Do not use a helper method for the view controller variable like "vc", otherwise you will not be able to access objects within the ViewController.
Here is an example target:
self.addTarget(ViewController(), action:#selector(ViewController.Test(_:)), for: UIControlEvents.touchDragInside)
In your View Controller, here is an example Action
#IBAction func Test(_ sender: Any?) {
print("Goodtime was here")
}
In the target you must add () but not in the action's selector. You do not have to call #IBAction, it can just be func. Some people use #objc or public any of those prefixes on the action should work.
Review, if the action is in a different Class or ViewController, you must put the the Class reference in both the target and the action's selector. Otherwise, it will try to always call the action within the same file regardless if it is correct in the Selector. Likewise, if the action is in the same file use, self for the target and inside the action's selector.
Cheers
When adding button targets in swift 3 first you need to be in class.
class SomeClass {
// ...
let button = UIButton()
// configure button to taste...
button.addTarget(self,
action: #selector(doSomething),
for: .touchUpInside)
// ...
#objc public func doSomething() {
print("hello, world!")
}
// ...
}