Doubletap recognizer for message bubbles (Messagekit) - swift

I want to add the feature to double tap a given message bubble.
Code so far:
#objc func doubleTap(gesture: UITapGestureRecognizer) {
print("double tap called")
}
func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageStyle {
let tapGesture = UITapGestureRecognizer(target: messagesCollectionView, action: #selector(doubleTap(gesture:)))
tapGesture.numberOfTapsRequired = 2
view.addGestureRecognizer(tapGesture)
let sender = message.sender
if sender.senderId == selfSenderEmail?.senderId {
// Self Message
return .bubbleTailOutline(.blue, .bottomRight, .curved)
}
// More if statements for groupchat color coding
return .bubbleTailOutline(.darkGray, .bottomLeft, .curved)
}
I get this Error Thread 1: "-[MessageKit.MessagesCollectionView doubleTapWithGesture:]: unrecognized selector sent to instance 0x7f94c7112a00".
If you think my post isn't clear in anyway, please let me know so I can clarify it. Thank you

You are adding UITapGestureRecognizer on view from MessagesViewController, which will attach it to the same controller view for each cell.
I suppose we are trying to get a double-tap for message cell and in order to achieve that, add UITapGestureRecognizer in the cell that you are trying to get the double tap on.
Suppose, we have DoubleTappableCell, which can be a text cell, media cell, or any other message.
class DoubleTappableCell: MessageContentCell {
lazy var doubleTapRecognizer: UITapGestureRecognizer = {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(doubleTapped))
tapGesture.numberOfTapsRequired = 2
return tapGesture
}()
var onDoubleTapped: () -> Void = {}
override init(frame: CGRect) {
super.init(frame: frame)
addGestureRecognizer(doubleTapRecognizer)
}
#objc func doubleTapped() {
onDoubleTapped()
}
}
This cell will be returned from the MessageCollectionView.
public override func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell: VideoCell = collectionView
.dequeueReusableCell(indexPath: index) else { return UICollectionViewCell() }
cell.data = anyData
cell.onDoubleTapped = {
print("DoubleTapped")
}
}
Another alternate but improved approach would be to use one UITapGestureRecognizer from ViewController to detect the indexpath of the touched cell, get the relevant cell from that index, and execute actions and view updates based on that.

Related

navigationController?.pushViewController is not working

I have a collection view controller. In collectionView cell I have label which I made clickable to push to the nextViewController.
I know that problem in navigationController. But I'm new in swift so can't fix. Hope you guys can help me.
Here's my SceneDelegate:
let layout = UICollectionViewFlowLayout()
// Create the root view controller as needed
let nc = UINavigationController(rootViewController: HomeController(collectionViewLayout: layout))
let win = UIWindow(windowScene: winScene)
win.rootViewController = nc
win.makeKeyAndVisible()
window = win
and my label:
let text = UILabel()
text.text = "something"
text.isUserInteractionEnabled = true
self.addSubview(text)
let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(PopularCellTwo.labelTapped))
text.addGestureRecognizer(gestureRecognizer)
}
#objc func labelTapped() {
let nextVC = NextViewController()
self.navigationController?.pushViewController(nextVC, animated: true)
print("labelTapped tapped")
}
I also added screenshot. When I click on "Something" It should go next page.
[1]: https://i.stack.imgur.com/4oYwb.png
You can use delegate or closure to do this
class ItemCollectionViewCell: UICollectionViewCell {
var onTapGesture: (() -> ())?
}
Then in your function you do
#objc func labelTapped() {
onTapGesture?()
}
And in your controller
class HomeController: UICollectionViewController {
//...
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = // dequeue cell
cell.onTapGesture = { [unowned self] in
let nextVC = NextViewController()
self.navigationController?.pushViewController(nextVC, animated: true)
}
return cell
}
}
self.navigationController?.pushViewController(nextVC, animated: true)
what self are you referring to ? because you cant make push in child class
you have HomeController i assume its your parent controller .
just try to debug what self is this could attempt by debugging or debug by condition
print (self)
if (self.isKind(of: YourParentController.self)) {
// make push
}
or try to check , see if navigationcontroller somehow has nil value
Here is how you do it using closures. I've created a closure parameter in UICollectionViewCell sub-class. When the label gesture target is hit I call the closure which then executed the navigation in HomeController.
class HomeController: UICollectionViewController {
//...
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = // dequeue cell
cell.labelTap = { [weak self] in
guard let self = self else { return }
let nextVC = NextViewController()
self.navigationController?.pushViewController(nextVC, animated: true)
print("navigated")
}
return cell
}
}
class CollectionViewCell: UICollectionViewCell {
var labelTap: (() -> Void)?
#objc func labelTapped() {
print("labelTapped tapped")
labelTap?()
}
}

Can I pass an image on the SecondViewController to the FirstViewController?

From First ViewController I press a Button and open a SecondViewController that view a collection of Image, when I tap on image the view show a full screen image and return at the collection View. Can I pass the image to the first (starting) viewcontroller to use it as a background?
class SecondViewController: UIViewController {
#IBAction func returnHome(_ sender: UIButton) {
//self.dismiss(animated: true, completion: nil)
self.performSegue(withIdentifier: "backToHome", sender: nil)
}
var images = ["bg13", "bg11", "bg6", "bg10", "bg12", "bg5", "bg3", "bg4", "bg2", "bg7", "bg8", "bg9", "bg1", "bg14"]
// ...
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let imageViewCollection = UIImageView(image:UIImage(named: images [indexPath.item]))
imageViewCollection.frame = self.view.frame
imageViewCollection.backgroundColor = .black
imageViewCollection.contentMode = .top
imageViewCollection.isUserInteractionEnabled = true
let tap = UITapGestureRecognizer(target: self, action: #selector(dismissFullscreenImage))
imageViewCollection.addGestureRecognizer(tap)
imageViewCollection.addGestureRecognizer(tap)
imageViewCollection.isUserInteractionEnabled = true
self.view.addSubview(imageViewCollection)
}
#objc func dismissFullscreenImage(_ sender: UITapGestureRecognizer) {
sender.view?.removeFromSuperview()
}
}
You can use Delegate to pass the image to the first view controller.
Write this protocol on the top of the Second View Controller.
protocol SecondViewControllerDelegate: NSObject {
func sendToFirstViewController(image: UIImage)
}
Then create a variable inside Second View Controller
class SecondViewController: UIViewController {
....
weak var customDelegate: SecondViewControllerDelegate?
....
}
Call this from collectionView:didSelectItemAt:indexPath method
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let image = UIImage(named: images [indexPath.item])
customDelegate?.sendToFirstViewController(image: image)
...
}
In the First viewController, in where you instantiate the second view controller, put this line
secondVc.customDelegate = self
then create an extension to implement the delegate method
extension FirstViewController: SecondViewControllerDelegate {
func sendToFirstViewController(image: UIImage){
// use this image as your background
}
}

How do you Tap to hide keyboard first, then tap again to select cell in Swift?

I have the following extension, which hides the keyboard when ever a tap is registered anywhere in the view.
//Extension to hide the keyboard when tap anywhere
extension UIViewController {
func hideKeyboardWhenTappedAround() {
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(UIViewController.dismissKeyboard))
tap.cancelsTouchesInView = false
view.addGestureRecognizer(tap)
}
#objc func dismissKeyboard() {
view.endEditing(true)
}
}
This used in the viewDidLoad() of my ViewController which also is a delegate/datasource to a TableView Controller.
self.hideKeyboardWhenTappedAround()
The dismissal of the keyboard works perfectly well, although the behaviour that I am after is for the keyboard to be dismissed first at first tap anywhere on the view/tableview before the user taps again to select a row from the search results in a tableview.
Currently a tap anywhere is not only dismissing the keyboard but also selecting the cell where the user has tapped.
Add notification observer and Check for keyboard in didselectrow method.
var isKeyBoard = false
override func viewDidLoad() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillDisappear(_:)), name: Notification.Name.UIKeyboardWillHide, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillAppear(_:)), name: Notification.Name.UIKeyboardWillShow, object: nil)
}
#objc func keyboardWillAppear(_ notification: NSNotification) {
if let userInfo = notification.userInfo,
isKeyBoard = true
}
}
#objc func keyboardWillDisappear(_ notification: NSNotification) {
isKeyBoard = false
}
deinit {
NotificationCenter.default.removeObserver(self)
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if isKeyBoard {
self.view.endEditing(true)
return
}
}
Just a Normal check you can initialise as
Keyboard is Open That means A TextField Is acting as a First Responder
Now you just simply need to check in didSelect Method here I am Showing you in my collectionView DidSelect Method same as you can do in your Tableview DidSelect
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
{
/// Check do TF is First Responder
/// If yes Just Hide the Keyboard for first time
if propertySearchTF.isFirstResponder {
self.view.endEditing(true)
return
}
/// if TF is not First Responder
/// Execution will start from here
/// DidSelect Code
}
Tested in TableView :
TF Outlet
#IBOutlet weak var myTF: UITextField!{
didSet{
myTF.becomeFirstResponder()
}
}
TableView Method
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if self.myTF.isFirstResponder {
self.view.endEditing(true)
return
}
print(selectedKey)
selectedimage = images[indexPath.row] as UIImage
performSegue(withIdentifier: "segue", sender: self)
}
You have to add the gesture on tableView with cancelsTouchesInView = true
extension UITableView {
func hideKeyboardWhenTappedAround() {
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.dismissKeyboard))
tap.cancelsTouchesInView = true
self.addGestureRecognizer(tap)
}
#objc func dismissKeyboard() {
self.endEditing(true)
}
}
Call from viewDidLoad() tableView.hideKeyboardWhenTappedAround()

Handling Previous, Next and Done buttons for keyboard across UITableView custom cells

First of all, I'm a fairly new to Swift and I've been looking for a good solution to handle Previous, Next and Done buttons on a keyboard across different custom cells in UITableView. I've looked at the various solutions on Stack Overflow but none of them fit 100% for what I need.
My tableview has one field (UITextField, UITextView, etc.) per row and need a generic way to move from one cell to the next. Some of the solutions don't account for scenarios where the next cell might be offscreen.
I've come up with a solution which I'll post as an answer. Feel free to comment on ways to improve if you have suggestions!
Please check this library. Simple and effective. You just to need to install via cocoa pods and single line code in appDelegate
pod 'IQKeyboardManagerSwift'
https://github.com/hackiftekhar/IQKeyboardManager
In App delegate
IQKeyboardManager.sharedManager().enable = true
For my custom cells, I have a base class as a foundation since I'm creating everything programmatically. It looks like this:
class BaseTableViewCell: UITableViewCell {
weak var delegate: BaseTableViewCellDelegate? = nil
var indexPath: IndexPath? = nil
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupViews()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupViews() {
fatalError("BaseTableViewCell setupViews not overridden")
}
func handleBecomeFirstResponser() {
// handle in derived class
}
func handleResignFirstResponder() {
// handle in derived class
}
}
Also, I've got a delegate for this base class that looks like this:
protocol BaseTableViewCellDelegate: class {
func cellEdited(indexPath: IndexPath)
func cellPreviousPressed(indexPath: IndexPath)
func cellNextPressed(indexPath: IndexPath)
func cellNeedsResize(indexPath: IndexPath)
}
// Using extension to provide default implementation for previous/next actions
// This makes then optional for a cell that doesn't need them
extension BaseTableViewCellDelegate {
func cellPreviousPressed(indexPath: IndexPath) {}
func cellNextPressed(indexPath: IndexPath) {}
func cellNeedsResize(indexPath: IndexPath) {}
}
I'm using an extension for a pure-swift mechanism to make the previous and next implementations optional in case we have a situation where we don't need these buttons.
Then, in my BaseTableViewCell class, I've got a function to setup the keyboard toolbar like this (below). I've got another function to support a UITextView as well (there might be a better way to do this; not sure).
func setupKeyboardToolbar(targetTextField: UITextField, dismissable: Bool, previousAction: Bool, nextAction: Bool) {
let toolbar: UIToolbar = UIToolbar()
toolbar.sizeToFit()
var items = [UIBarButtonItem]()
let previousButton = UIBarButtonItem(image: UIImage(imageLiteralResourceName: "previousArrowIcon"), style: .plain, target: nil, action: nil)
previousButton.width = 30
if !previousAction {
previousButton.isEnabled = false
} else {
previousButton.target = self
previousButton.action = #selector(toolbarPreviousPressed)
}
let nextButton = UIBarButtonItem(image: UIImage(imageLiteralResourceName: "nextArrowIcon"), style: .plain, target: nil, action: nil)
nextButton.width = 30
if !nextAction {
nextButton.isEnabled = false
} else {
nextButton.target = self
nextButton.action = #selector(toolbarNextPressed)
}
items.append(contentsOf: [previousButton, nextButton])
let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(dismissKeyboard))
items.append(contentsOf: [spacer, doneButton])
toolbar.setItems(items, animated: false)
targetTextField.inputAccessoryView = toolbar
}
Here are the associated action routines:
func toolbarPreviousPressed() {
if delegate != nil && indexPath != nil {
delegate?.cellPreviousPressed(indexPath: indexPath!)
}
}
func toolbarNextPressed() {
if delegate != nil && indexPath != nil {
delegate?.cellNextPressed(indexPath: indexPath!)
}
}
In my view controller where I have the tableview, my cellForRowAt function has this code:
let cell = tableView.dequeueReusableCell(withIdentifier: "textFieldCell") as! TextFieldCell
let addressItem = (item as! XXXXXXAddressViewModelItem)
cell.textField.placeholder = addressItem.placeHolderText
cell.textField.text = addressItem.getValue(row: 0)
cell.indexPath = indexPath
cell.delegate = self
cell.setupKeyboardToolbar(targetTextField: cell.textField, dismissable: true, previousAction: false, nextAction: true)
return cell
Here is how I handle the delegate methods for the previous and next buttons being pressed:
func cellPreviousPressed(indexPath: IndexPath) {
// Resign the old cell
let oldCell = tableView.cellForRow(at: indexPath) as! BaseTableViewCell
oldCell.handleResignFirstResponder()
// Scroll to previous cell
let tempIndex = IndexPath(row: indexPath.row, section: indexPath.section - 1)
tableView.scrollToRow(at: tempIndex, at: .middle, animated: true)
// Become first responder
let cell = tableView.cellForRow(at: tempIndex) as! BaseTableViewCell
cell.handleBecomeFirstResponser()
}
func cellNextPressed(indexPath: IndexPath) {
// Resign the old cell
let oldCell = tableView.cellForRow(at: indexPath) as! BaseTableViewCell
oldCell.handleResignFirstResponder()
// Scroll to next cell
let tempIndex = IndexPath(row: indexPath.row, section: indexPath.section + 1)
self.tableView.scrollToRow(at: tempIndex, at: .middle, animated: true)
// Become first responder for new cell
let cell = self.tableView.cellForRow(at: tempIndex) as! BaseTableViewCell
cell.handleBecomeFirstResponser()
}
Finally, in my cell class derived from BaseTableViewCell, I override the handleBecomeFirstResponder and handleResignFirstResponder like so:
override func handleBecomeFirstResponder() {
textField.becomeFirstResponder()
}
override func handleResignFirstResponder() {
textField.resignFirstResponder()
}
On a related note, I handle the keyboard show and hide notifications by using insets on the tableview:
self.tableView.contentInset = UIEdgeInsetsMake(0, 0, (keyboardFrame?.height)!, 0)
self.tableView.scrollIndicatorInsets = UIEdgeInsetsMake(0, 0, (keyboardFrame?.height)!, 0)
and:
self.tableView.contentInset = UIEdgeInsets.zero
self.tableView.scrollIndicatorInsets = UIEdgeInsets.zero
This took me a lot of trial-and-error to get this right and not seriously convolute my view controller with code that should be in the cell class.
I'm always looking for better ways to do this. Let me know your thoughts!

How to tell if button in collection view header is selected when populating collection view?

How do I tell if I button in the collection view header is selected when populating my collection view? I have 2 tabs in the header which determine which data I populate the collection view with so I want to be able to switch the data and reload the collection view when the user selects a different tab.
some code from header class as requested...I don't see the point though it's just a button. I want to see if this button is selected while populating the cells and cell count etc.
class UserSearchHeader: UICollectionReusableView {
#IBOutlet weak var friendsButton: UIButton!
#IBOutlet weak var peopleButton: UIButton!
#IBOutlet weak var customSlider: UIView!
override func awakeFromNib() {
super.awakeFromNib()
self.friendsButton.selected = true
customSlider.layer.cornerRadius = customSlider.frame.height / 2
}
#IBAction func friendsButtonTapped(sender: AnyObject) {
if self.friendsButton.selected == false {
self.friendsButton.selected = true
self.peopleButton.selected = false
UIView.animateWithDuration(0.3, animations: { () -> Void in
self.customSlider.frame.origin.x = 0
})
}
}
UICollectionviewHeader + UI Button
UIcollectionviewHeader are using dequeueReusableSupplementaryView and for some reason it created a UIView Infront of every Headerview, this would block all gesture that are happening. the solution is to bring the view to front like so:
override func awakeFromNib() {
super.awakeFromNib()
self.bringSubview(toFront: friendsButton) // this depends on you .xib
}
this will solve your gesture issue.
Theres Multiple way to Create a UIButton Action inside a CollectionViewHeader.
AddTarget (as Answered by #derdida)
Drag and Drop Action In CollectionViewHeader Class.
Add Target :-(refer to derdida answer)
inside of viewForSupplementaryElementOfKind// ps: you need to register your CollectionView Class & Nib
Add Target to UIButton like so.
header.friendsButton.tag = 0
header.friendsButton.addTarget(self, action: #selector(self.HeaderClick(_:)), for: .touchUpInside)
Created the Function like so.
#objc func HeaderClick1(_ sender : UIButton){ print("sender.tag \(sender.tag)") }//sender.tag 0
Drag and Drop Action In CollectionViewHeader Class.
for this example i will used #Wayne Filkins Question.
Ctrl + Drag UIButton To Header Class
Select Connection Type to Action create the function like so
#IBAction func friendsButtonTapped(_ sender: Any) { print("something from friendsButtonTapped") }
and thats it.
but the main key here is to get the view to be to front. used debug view hierarchy to know more.
Someone can fixed my code view please!
Update Solution
Using UIButtonSetOnClickListener.Swift and heres the code:
extension UIControl {
func setOnClickListener(for controlEvents: UIControl.Event = .primaryActionTriggered, action: #escaping () -> ()) {
removeTarget(nil, action: nil, for: .allEvents)
let sleeve = ClosureSleeve(attachTo: self, closure: action)
addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for:
controlEvents)
}
}
class ClosureSleeve {
let closure: () -> ()
init(attachTo: AnyObject, closure: #escaping () -> ()) {
self.closure = closure
objc_setAssociatedObject(attachTo, "[\(arc4random())]", self, .OBJC_ASSOCIATION_RETAIN)
}
#objc func invoke() {
closure()
}
}
Here how to used is:
Add Target :-(refer to derdida answer)
inside of viewForSupplementaryElementOfKind// ps: you need to register your CollectionView Class & Nib
header.friendsButton.setOnClickListener {
//put your code here
}
There are 2 delegate methods who are important to decide which items will be shown. For example:
You have 2 different items, they are populated in:
let items1 = [Item]()
let items2 = [Item]()
Then you have a variable, that holds which items should be shown:
let items1Shown:Bool = true
Now implement the delegate methods with something like:
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if(items1Shown == true) {
return items1.count
} else {
return items2.count
}
}
And
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
var item:Item!
if(items1Shown == true) {
item = items1[indexPath.row]
} else {
item = items1[indexPath.row]
}
// format your cell
}
And implement any button function
func ChangeItems() {
if(items1Shown == true) {
items1Shown = false
} else {
items1Shown = true
}
// reload your collectionView
self.collectionView.reloadData()
}
Edit:
Why not giving your button a target? (Add this where you dequeue your headerView!)
headerView.friendsButton.addTarget(self, action: #selector(YourViewControllerWithFunction.cellButtonClick(_:)), forControlEvents: .TouchUpInside)
// for example:
func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView {
let headerView = collectionView.dequeueReusableSupplementaryViewOfKind(kind, withReuseIdentifier: headerId, forIndexPath: indexPath)
headerView.friendsButton.addTarget(self, action: #selector(YourViewControllerWithFunction.cellButtonClick(_:)), forControlEvents: .TouchUpInside)
return headerView
}
// Class YourViewControllerWithFunction
func cellButtonClick(button: UIButton) {
// you can do anything with that button now
}