I've got a custom subclass of UIStackView that's got some UIImageViews inside it. I want the view to accept a pan gesture to track a slow swipe across its surface. When I configure a UIPanGestureRecognizer in Interface Builder it works, but not in code. Here's an abbreviated version of the class:
#IBDesignable
class CustomView: UIStackView {
private let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(CustomView.singleFingerSwipe))
override init(frame: CGRect) {
super.init(frame: frame)
initializeProperties()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initializeProperties()
}
#IBAction
private func singleFingerSwipe(_ sender: UIPanGestureRecognizer) {
print("Panning...")
}
private func initializeProperties() {
addGestureRecognizer(panRecognizer)
for _ in 1...3 {
let imageView = UIImageView(image: UIImage(named: "MyImage"))
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.isUserInteractionEnabled = true
addArrangedSubview(imageView)
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true
imageView.heightAnchor.constraint(equalTo: self.heightAnchor).isActive = true
}
}
}
As I swipe around inside the star view, no output prints. But if I drop a Pan Gesture Recognizer onto the view in IB and hook it up to the same selector, it works fine. What am I missing?
make your panRecognizer lazy var so that its initialization is delayed till its first use and till the time the frame of stack view got set. like
private lazy var panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(CustomView.singleFingerSwipe))
or
move the declaration of panRecognizer to initializeProperties() method right before addGesture call. like
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(CustomView.singleFingerSwipe))
addGestureRecognizer(panRecognizer)
EDIT
The reason is the target is setting null and hence it unable to fire the method, when you are initializing with "private let ..". Since the init gets called later and object isn't initialized for CustomView. Here is the console log when declared with let(constant):
Printing description of self.panRecognizer:
; target= <(action=singleFingerSwipe:, target=<(null) 0x0>)>>
But when its lazy var or declared within the method after init is called, the instance for CustomeView is generated till the time of use of panRecognizer. the console print is:
Printing description of self.panRecognizer.storage.some:
; target= <(action=singleFingerSwipe:, target=)>>
here you can see the target is set.
P.S. you can check the same with debugger
Related
My issues with new collection view list cells is that I'm not able to add action handlers to a custom accessory view.
I've been trying to do the following:
protocol TappableStar: class {
func onStarTapped(_ cell: UICollectionViewCell)
}
class TranslationListCell: UICollectionViewListCell {
let starButton: UIButton = {
let starButton = UIButton()
let starImage = UIImage(systemName: "star")!.withRenderingMode(.alwaysTemplate)
starButton.setImage(starImage, for: .normal)
starButton.setContentHuggingPriority(.defaultHigh, for: .horizontal)
starButton.addTarget(self, action: #selector(starButtonPressed(_:)), for: .touchUpInside)
starButton.tintColor = .systemOrange
return starButton
}()
var translation: TranslationModel?
weak var link: TappableStar?
override init(frame: CGRect) {
super.init(frame: frame)
accessories = [.customView(configuration: .init(customView: starButton, placement: .trailing(displayed: .always)))]
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
#objc private func starButtonPressed(_ sender: UIButton) {
link?.onStarTapped(self)
}
override func updateConfiguration(using state: UICellConfigurationState) {
// Create new configuration object and update it base on state
var newConfiguration = TranslationContentConfiguration().updated(for: state)
// Update any configuration parameters related to data item
newConfiguration.inputText = translation?.inputText
newConfiguration.outputText = translation?.outputText
contentConfiguration = newConfiguration
}
}
I subclass UICollectionViewListCell, create a button with target-action handler and add it to accessories array. I also have my own implementation of cell configuration.
Now, I create a protocol where I delegate action handling to my view controller (I also implemented new cell registration API and set cell.link = self).
My problem here is that my accessory button doesn't call starButtonPressed although this accessory view is responsive (it changes color when highlighted).
My idea is that there might be something wrong with the way I implement my action handling with a custom accessory but there seems to be little to none information about this new api.
Moreover, when choosing between predefined accessories, some of them have actionHandler closures of type UICellAccessory.ActionHandler but I don't seem to understand how to properly implement that.
Any ideas would be much appreciated.
iOS 14, using UIActions
Since iOS 14 we can initialise UIButton and other UIControls with primary actions. It becomes similar to handlers of native accessories. With this we can use any parametrized method we want. And parametrising is important, because usually we want to do some action with specific cell. #selector's cannot be parametrised, so we can't pass any information to method about which cell is to be updated.
But this solution works only for iOS 14+.
Creating UIAction:
let favoriteAction = UIAction(image: UIImage(systemName: "star"),
handler: { [weak self] _ in
guard let self = self else { return }
self.handleFavoriteAction(for: your_Entity)
})
Creating UIButton:
let favoriteButton = UIButton(primaryAction: favoriteAction)
Creating accessory:
let favoriteAccessory = UICellAccessory.CustomViewConfiguration(
customView: favoriteButton,
placement: .leading(displayed: .whenEditing)
)
Using
cell.accessories = [.customView(configuration: favoriteAccessory)]
I solved my issue by adding tap gesture recognizer to my accessory's custom view. So it works like this:
let customAccessory = UICellAccessory.CustomViewConfiguration(
customView: starButton,
placement: .trailing(displayed: .always))
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(starButtonPressed(_:)))
customAccessory.customView.addGestureRecognizer(tapGesture)
accessories = [.customView(configuration: customAccessory)]
Haven't seen it documented anywhere so hope it helps somebody.
I followed a similar approach to yours, but instead of a UITapGestureRecognizer, I added a target to the button.
var starButton = UIButton(type: .contactAdd)
starButton.addTarget(self, action: #selector(self.starButtonTapped), for: .touchUpInside)
let customAccessory = UICellAccessory.CustomViewConfiguration(customView: starButton, placement: .trailing(displayed: .always))
cell.accessories = [.customView(configuration: customAccessory)]
I first tried the tap gesture recognizer and it didn't work for me.
I'm trying to invoke my method from selector (action) but getting error Variable used within its own initial value Below is my code snippet
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(sendRequest(tapGestureRecognizer, userID)))
Below is the method which I'm calling
#objc func sendRequest(tapGestureRecognizer: UITapGestureRecognizer, identifer: String) {
print("hello world")
}
My method accepts 2 paramters. I don't know how to call it. The way I'm currently calling the sendRequestMethod is throwing error.
Please help me to get it resolved.
when those functions are inside an UIView, as GestureRecognizers usually are, then you can make a hittest and find what UIView was under your touch.
Consider this is not the ideal way to catch touched views. As every UIView also offers you a .tag you can set a numbering and use it to make assumtions to whom the hit UIView fits. See Example
class checkCheckView : UIView {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(sendRequest(recognizer:identifer:)) )
#objc func sendRequest(recognizer:UITapGestureRecognizer, identifer:String) {
print("hello world")
// your problem will be, how is identifier passed in here?
}
let PanGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panning(gesture:)))
#objc func panning(gesture:UIPanGestureRecognizer) {
//print("pan-translation %#", gesture.translation(in: self).debugDescription)
let location = gesture.location(ofTouch: 0, in: self)
//print("pan-location %#",location.debugDescription);
let hitview = self.hitTest(location, with: nil)
if hitview != nil {
let UserIDTag = hitview?.tag ?? 0
print("userId by UIView tag",UserIDTag)
}
}
}
Hint: Much easier is to subclass UIView and prepare a property that holds UserID given when allocation is done. addtarget: action:#selector() to each of them. The action/selector method gets invoked and the sender is passed in and because you know the type of the sender (YourUserUIView) you find the UserID.
With GestureRecognizer's you tend to write recognition code that can become very complex the more complex your UI will be, instead of just passing objects to actions that tell you what you are looking for.
I'm tired that UIButton's image is too complicated to customize. And it's long to create tappable UIImageView. Than i want to create own mixed element of UIImage and UIButton. It would let to create custom animation, touch areas and lot more.
I decided to take a UIImageView and add a gesture or a button to it to detect touches. But i need to have a TouchUpInside or just touch event to easily connect it in IB. Look at the code, what is already written:
class ClickableImageView: UIImageView {
let embeddedButton = UIButton()
override func awakeFromNib() {
super.awakeFromNib()
setup()
}
fileprivate func setup() {
isUserInteractionEnabled = true
sv(embeddedButton)
embeddedButton.fillContainer()
embeddedButton.addTarget(self, action: #selector(didPress), for: .touchUpInside)
}
#objc fileprivate func didPress() {
embeddedButton.preventRepeatedPresses()
}
}
I did the same with the UIButton subclassing and it works, but it not showing the image in IB, so it confuse the developer.
The last problem to solve is how to add the event?
UIButton image can be easily resized by giving distance from (top,left,bottom and right) of the UIButton like below.
yourButton.imageEdgeInsets = UIEdgeInsetsMake(10, 50, 10, 95)
If you are looking for adding an event to the UIImageView, do the following,
yourImage.isUserInteractionEnabled = true
let tapImage = UITapGestureRecognizer(target: self, action: #selector(myFunction(tapGestureRecognizer:)))
yourImage.addGestureRecognizer(tapImage)
Then handle your method,
#objc func myFunction(tapGestureRecognizer: UITapGestureRecognizer) {
//do something here
}
I have an image view on scroll view. I added more than one sub views on image view.
->UIScrollView
-->UIImageView
--->UIViews
I want to add gesture recognizer to subviews. But It doesn't work. Where is my mistake? function handleTap() not triggered.
func addSubViewOnImageView(mPoint:CGPoint, mSize: CGSize){
let rect = CGRect(origin: mPoint, size: mSize)
let sView = UIView(frame: rect)
sView.isUserInteractionEnabled = true
let tap = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
tap.delegate = self
sView.addGestureRecognizer(tap)
imageView.addSubview(sView)
}
#objc func handleTap(_ sender: UITapGestureRecognizer) {
print("tapped any sub view")
}
This property is inherited from the UIView parent class. This class changes the default value of this property to NO.
enable the user interaction for your imageview, by default its false , for more info you get the info from apple document
imageView.isUserInteractionEnabled = true
just check
yourImageView.isUserInteractionEnabled = true
is there or not?
An Alternate if you don't want to do
yourImageView.isUserInteractionEnabled = true
Just make your UIView class a subclass of UIControl from storyboard refer from screenshot.
it will act as an UIButton no need to write the code for adding an tap gesturejust add an action and likenormally we do for UIImageView`
I've the following code to add a gesture recognizer to a UILabel. User Interaction Enabled is ticked on for the label in the storyboard, but when I tap on the label the onUserClickingSendToken method is not being called.
class ViewController: UIViewController, MFMailComposeViewControllerDelegate {
#IBOutlet weak var tokenDisplay: UILabel!
var tapGestureRecognizer:UITapGestureRecognizer = UITapGestureRecognizer(target:self, action: #selector(onUserClickingSendToken(_:)))
override func viewDidLoad() {
super.viewDidLoad()
tapGestureRecognizer.numberOfTapsRequired = 1
tokenDisplay.addGestureRecognizer(tapGestureRecognizer)
}
func onUserClickingSendToken(_ sender: Any)
{
....
Initializing the tapRecognizer in viewDidLoad should do it, cause you were targeting self before the view was initialized
class ViewController: UIViewController, MFMailComposeViewControllerDelegate {
#IBOutlet weak var tokenDisplay: UILabel!
var tapGestureRecognizer:UITapGestureRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
tapGestureRecognizer = UITapGestureRecognizer(target:self, action: #selector(onUserClickingSendToken(_:)))
tapGestureRecognizer.numberOfTapsRequired = 1
tokenDisplay.isUserInteractionEnabled = true
tokenDisplay.addGestureRecognizer(tapGestureRecognizer)
}
#objc func onUserClickingSendToken(_ sender: Any)
{
....
In general for UILabel clickable issue:
There are couple of reasons why your UILabel will not work as clickable
Make sure to mark UILabel UserInteraction true and if yourLabel is inside other view than mark that view UserInteraction true too.
yourLabel.isUserInteractionEnabled = true
Assigning self to target before the view initialisation, move your code inside viewDidLoad/awakeFromNib or after view Load
let tap = UITapGestureRecognizer(target: self, action: #selector(onClickLabel(_:)))
self.yourLabel.addGestureRecognizer(tap)
If you are adding UILabel programmatically, than you have to assign it's frame too.
yourLabel.frame = CGRect(x: 0, y: 0, width: 300, height: 20)
Super view in which your UILabel is should be large enough to fit/display label properly, if your label is inside some view than that view's height/weight should assign in a way that it cover the whole UILabel frame.
In my case this was the problem I had, I tried so many thing but at last it turns out that the view in which I had my label had fixed width, the label was displaying properly but onClick event was't working due to out of frame.
It was missing UILabel's trailing constraints, so half of the thing is not clickable.
Using same tab gesture for multiple view, you cannot use one tab gesture for more than one view or labels,use different for each UILabel.
try this:
let tap = UITapGestureRecognizer(target: self, action: #selector(onClickLabel(_:)))
self.yourLabel.isUserInteractionEnabled = true
self.yourLabel.addGestureRecognizer(tap)
let tap2 = UITapGestureRecognizer(target: self, action: #selector(onClickLabel(_:)))
self.someView.isUserInteractionEnabled = true
self.someView.addGestureRecognizer(tap2)
instead of this:
let tap = UITapGestureRecognizer(target: self, action: #selector(onClickLabel(_:)))
self.yourLabel.isUserInteractionEnabled = true
self.yourLabel.addGestureRecognizer(tap)
self.someView.isUserInteractionEnabled = true
self.someView.addGestureRecognizer(tap)
// tapGesture will work on only one element
Try changing your selector setup to:
#selector(ViewController .onUserClickingSendToken(_:).