UITextField in UITableViewCell and becomeFirstResponder on selected cell keyboard show/hide glitch - ios

I have such view hierarchy:
View Controller
UITableView
UITableViewCell
UITextField
I have in my custom UITableViewCell a textField and my plan is to make this textField becomeFirstResponder as soon as user tap on cell. The first problem is that textField is getting touches events and cell is not selected because of that so when user tap right onto the textField then cell is not selected and I'm not able to update selected cell UI. I though that I can make isUserInteractionEnabled=false on textField so after user taps on textField I will have proper selection behavior in my cells. So far so good. Now I want to handle this selection in setSelected method in my custom cell. The problem is that if keyboard is already on screen and I execute becomeFirstResponder() on textField then the keyboard is hiding and immediately showing again. Normally I expect that this keyboard will stay on its place without trying to hide and show again. Here is the code:
class StringFieldTableViewCell: UITableViewCell {
#IBOutlet weak var pinLine: UIView!
#IBOutlet weak var pinLineFocused: UIView!
#IBOutlet weak var textField: UITextField!
override func awakeFromNib() {
super.awakeFromNib()
self.selectionStyle = .none
self.textField.isUserInteractionEnabled = false
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
self.textField.isUserInteractionEnabled = selected
if selected {
self.textField.becomeFirstResponder()
}
self.pinLine.isHidden = selected
self.pinLineFocused.isHidden = !selected
}
}
Does anybody knows how to fix this keyboard showing/hiding problem and why keyboard tries first to hide itself and then show in very short time?

The problem in your code is that you are kind of disabling the textField interaction because you have textField in your cells and if you tap on the cell you will interact with the textField, not the cell itself.
So there is a way to select a row even if you have a textField in your cell.
First, you need to get when you tapped on that textField.
for that you can use this....
let pointInTable = textField.convert(textField.bounds.origin, to: self.tableView)
It will give you a value in a CGPoint([x, y]), for E.g [0.0, 0.0] or [0.0, 44.0]. Depending upon the height of your Cell.
Now, there is a function that converts CGPoint to IndexPath.
let textFieldIndexPath = tableView.indexPathForRow(at: pointInTable)
Now this will be our indexPath.
Now you can use cellSelected Fuction.
tableView.selectRow(at: textFieldIndexPath, animated: true, scrollPosition: .none)
The whole code will be like this:
let pointInTable = textField.convert(textField.bounds.origin, to: self.tableView)
let textFieldIndexPath = self.tableView.indexPathForRow(at: pointInTable)
tableView.selectRow(at: textFieldIndexPath, animated: true, scrollPosition: .none)
You can put this in textFieldDidBeginEditing Function.

Related

How to draw a circle around UICollectionViewCell when clicking on it and remove others circle from previous selected cells?

I've been working with UICollectionView lately. There is a requirement that needs to be implemented like: "There are several imageviews in several collectionview cell. When user selects one of the image/cell, the app will draw a blue circle around that image/cell."
Currently, I'm able to do the draw on the cell. But the problem now is that I am able only to draw all cells but not one cell at the time (as screenshot below)
So my question is: how can I select one image/cell, the blue circle of previous selected cell should be removed?
Thanks so much for the answers in advance.
It sounds like you want this:
You didn't say how you're putting the blue circle in the cell. Here's how I think you should handle selection: use the collection view's built-in selection support as much as possible.
A UICollectionView already has support for selecting cells. By default, its allowsSelection property is true and its allowsMultipleSelection property is false, so it allows the user to select one item at a time by tapping the item. This sounds like almost exactly what you want.
The collection view makes the current selection available in its indexPathsForSelectedItems property, which is either nil or empty when no cell is selected, and contains exactly one index path when one item is selected.
When an item is selected, and there is a visible cell for the item, the cell shows that its item is selected by making its selectedBackgroundView visible. So make a UIView subclass that shows a blue circle:
class CircleView: UIView {
override class var layerClass: AnyClass { return CAShapeLayer.self }
override func layoutSubviews() {
super.layoutSubviews()
let layer = self.layer as! CAShapeLayer
layer.strokeColor = UIColor.blue.cgColor
layer.fillColor = nil
let width: CGFloat = 3
layer.lineWidth = width
layer.path = CGPath(ellipseIn: bounds.insetBy(dx: width / 2, dy: width / 2), transform: nil)
}
}
Then use an instance of CircleView as the cell's selectedBackgroundView. You can create the instance lazily the first time the cell becomes selected:
class MyCell: UICollectionViewCell {
override var isSelected: Bool {
willSet {
if newValue && selectedBackgroundView == nil {
selectedBackgroundView = CircleView()
}
}
}
var title: String = "???" {
didSet {
label.text = title
}
}
#IBOutlet private var label: UILabel!
}
With this code in place, the user can tap a cell to select its item, and the cell will show a blue circle when selected. Tapping another cell will deselect the previously-selected item, and the blue circle will “move” to the newly-selected item's cell.
You might want to let the user deselect the selected item by tapping it again. UICollectionView doesn't do that by default if allowsMultipleSelection is false. One way to enable tap-again-to-deselect is by implementing collectionView(_:shouldSelectItemAt:) in your UICollectionViewDelegate:
override func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
if (collectionView.indexPathsForSelectedItems ?? []).contains(indexPath) {
// Item is already selected, so deselect it.
collectionView.deselectItem(at: indexPath, animated: false)
return false
} else {
return true
}
}

UICollectionView not showing selected data after long scroll

I have one UICollectionView with five custom cells that need to be render on specific conditions, cells are getting generated. Now problem I am going through is consider I have selected one cell at specific index, and now I scroll downwards and now again I long scroll. When UICollectionView stops scrolling, if the index is the same as which we selected, UICollectionView doesn't show that cell as selected. But now if I even try to move the cell a little bit, even a bit, UICollectionView shows that cell as selected cell.
Following is my code, that I have wrote in prefetchItem:
(cell as? PATemplateTypeOneCollectionCell)?.fillCellData(row: indexPath.row,section:indexPath.section, paCategoryQuestions: currentIndexQuestion, paQuestionCollection: currentIndexCollection)
cell!.alpha = 0.4
if self.multipleIndexPathsArray[indexPath.section][0] != []{
collectionView.selectItem(at: self.multipleIndexPathsArray[indexPath.section][0], animated: true, scrollPosition: .right)
}
else{
print("self.multipleIndexPathsArray[indexPath.section][0] is empty")
}
UICollectionViewCell:
override var isSelected: Bool {
didSet {
if isSelected {
self.alpha = 1.0
self.layer.borderWidth = 2.0
self.layer.borderColor = ColorConstants.colorFromHexString(hexString: paCategoryQuestions.selection_color).cgColor
}else {
self.alpha = 0.4
self.layer.borderWidth = 1.0
self.layer.borderColor = UIColor.lightGray.cgColor
}
}
Kindly request you guys to help with this issue.
So after hours of thinking I came across one solution. So as my cellForItem was not rendering selected scroll properly after long scroll, what I tried is recognizing UIScrollView's delegate scrollViewDidEndDecelerating. And recognizing if currently visible indexPath cells were selected previously or not by using my saved values array. Following is the code that worked for me:
fileprivate func checkIfCellIsSelected(){
for eachCell in afterPaymentPACollectionView.visibleCells{
let indexPath = afterPaymentPACollectionView.indexPath(for: eachCell)
for eachIndexSelected in multipleIndexPathsArray{
if eachIndexSelected.contains(indexPath!){
afterPaymentPACollectionView.selectItem(at: indexPath, animated: true, scrollPosition: .right)
}
}
}
}
//MARK: UIScrollViewDelegate
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
checkIfCellIsSelected()
}

UITableViewCell custom class - Reload cell height after change in subview height constraint

I have a button in custom UITableViewCell class. It shows/hides a view (part of same cell). The height of cell should change on doing that.
This is the button action (in UITableViewCell custom class):
#IBAction func showHideCartView(sender: UIButton)
{
if sender.tag == 1
{
// Show cart view
self.buttonArrow.tag = 2
self.viewAddToCart.isHidden = false
self.constraint_Height_viewAddToCart.constant = 50
self.buttonArrow.setImage(UIImage(named: "arrowUp.png"), for: .normal)
}
else
{
// Hide cart view
self.buttonArrow.tag = 1
self.viewAddToCart.isHidden = true
self.constraint_Height_viewAddToCart.constant = 0
self.buttonArrow.setImage(UIImage(named: "arrowDown.png"), for: .normal)
}
self.setNeedsUpdateConstraints()
self.setNeedsLayout()
self.layoutIfNeeded()
}
The height of cell remains unchanged. It is only when I scroll the UITableView and revisit the cell, it's height gets updated.
Add the following method to the viewController with the tableView to refresh the table when a cell is expanded:
func refreshTableAfterCellExpansion() {
self.tableView.beginUpdates()
self.tableView.setNeedsDisplay()
self.tableView.endUpdates()
}
Call the method after the constraints have been updated. If the button manipulates the constraints inside the cell, you will need to notify the viewController about the change so that you can call the method. Either use a delegate pattern (or pass directly a tableView reference to each expandable cell - just remember to store it as weak variable), or post a notification using NotificationCenter.default.
As this code is in the TableViewCell, You need to make a action method for your cell in the TableViewController so when the button is clicked you would know. You can keep a reference of your button in your tableviewCell and then inside the cell cellForRowAt you can do
cell.yourButtonRef.addTarget(self, action: #selector(expandButtonTapped(_:)), for: .touchDown)
and the make a function called "expandButtonTapped" and add the tableview beign and end update there
#objc func expandButtonTapped(_ button: UIButton) {
self.tableView.beginUpdates()
self.tableView.endUpdates()
}
Please let me know if this has worked or not

UISwipeGestureRecognizer and CollectionView scroll to next item indexPath

So I have a collection view of cards, and what I want is when I swipe left I want the next item to be centered in the middle, this doesn't work with paging enabled
So when I swipe left I want the next card to be centered, this doesn't work with the normal behaviour because I just want to swipe one card.
So I have added UISwipeGestureRecognizer and disabled scrolling on the collection view
class ViewController: UIViewController,UICollectionViewDataSource {
#IBOutlet var joke_cards: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad();
//Add gestures
let leftSwipeGest = UISwipeGestureRecognizer(target: self, action: #selector(funcForGesture))
leftSwipeGest.direction = .left
joke_cards.addGestureRecognizer(leftSwipeGest)
}
func funcForGesture(sender: UISwipeGestureRecognizer){
if sender.direction == .left {
//scroll to next item
}
}
Now my problem is how can I scroll to the next item? since I don't know the indexPath? because if I want to use this self.joke_cards.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true), I will need the index path, so I think I need to figure out the current indexPath on the view and add one when the user swipes.
Any suggestions?
//updated
So I have managed to get it to work, but it only works when I swipe the first time:
func funcForGesture(sender: UISwipeGestureRecognizer){
if sender.direction == .left {
//scroll to next item
let cellItems = self.joke_cards.indexPathsForVisibleItems
let next = cellItems[0] as! IndexPath
self.joke_cards.scrollToItem(at: next, at: .centeredHorizontally, animated: true)
}
}
One solution:
1: Add an NSIndexPath property to your cells and just set the indexPath to the property in your cellForItem method.
2: Get an array of visible cells.
3: Get the rect of the visible cells and calculate their position on the screen and you can allso get the correct indexPath from the cell property.
Second solution:
1: Add a UILongPressGestureRecognizer to your cell contentView
2: Set minimumPressDuration to 0.
3: Add delegate methods to fire acitons or this.
Third solution:
1: Keep your swipe gesture.
2: And use this method (same as option 2) indexPathForItemAtPoint:
Why are you saying that this does not work with paging enabled? This should actually be the best way to do it in my opinion. You can just make the width of the UICollectionViewCells to be equal to the width of the UICollectionView, set the scroll direction to be horizontal and enable paging, that should do the trick.
In this way you won't need any UIGestureRecognizers.
Ok so the solution is this one:
#IBOutlet var joke_cards: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad();
//Add gestures
let leftSwipeGest = UISwipeGestureRecognizer(target: self, action: #selector(funcForGesture))
leftSwipeGest.direction = .left
joke_cards.addGestureRecognizer(leftSwipeGest)
}
func funcForGesture(sender: UISwipeGestureRecognizer){
if sender.direction == .left {
//scroll to next item
let cellItems = self.joke_cards.indexPathsForVisibleItems
self.joke_cards.scrollToItem(at: cellItems.max()!, at: .centeredHorizontally, animated: true)
}
}

how to move inputView above keyboard when scrolling the table

I have a view controller in which it has
table view and textView below the table
When I tap on the textView the keyboard appears and when I tap outside the textView the keyboard disappar
That works fine but I have one problem which is when I Drag(scroll Down) the table view the textView doesn't move down with the keyboard and I see black background behind the keyboard as in the below image
How to solve this problem in Swift
Update:
This is my current code that observe the keyboardFrameChanges
func keyboardFrameChanged(notification: NSNotification) {
let dict = NSDictionary(dictionary: notification.userInfo!)
let keyboardValue = dict.objectForKey(UIKeyboardFrameEndUserInfoKey) as! NSValue
let bottomDistance = mainScreenSize().height - keyboardValue.CGRectValue().origin.y
let duration = Double(dict.objectForKey(UIKeyboardAnimationDurationUserInfoKey) as! NSNumber)
UIView.animateWithDuration(duration, animations: {
self.inputViewConstraint!.constant = -bottomDistance
self.view.layoutIfNeeded()
}, completion: {
(value: Bool) in
self.chatTableView.scrollToBottom(animation: true)
})
}
Update 2:
I found a solution by changing the keyboardDismissMode from Interactive to OnDrag
chatTableView.keyboardDismissMode = UIScrollViewKeyboardDismissMode.OnDrag
This will move the keyboard and the textView immediatly to down once it observe any drag movement in the table , But how to do it in the Interactive mode like in the whatsapp chat view
set you scroll view (UITableView or UICollectionView) frame equal to you UIViewController frame
set .interactive to keyboardDismissMode of you scroll view
override canBecomeFirstResponder and return true
setup input container with UITextField or UITextView in screen bottom
override inputAccessoryView and return input container
enjoy
See my simple code here on github
If you implement your own bottom constraint for input view
And change its constant on the keyboard frame notifications. Using these lines of code, the error of predictive bar layout at the end of the animation will be fixed.
override var inputAccessoryView: UIView? {
return UIView()
}
override var canBecomeFirstResponder: Bool {
return true
}
Finding the answer for this took forever but this is the correct way of implementing this feature with the interactive state enabled.
In your view controller that has the tableview or collection view displaying chat bubbles override inputAccessoryView, then return the view that contains the uitextView or uitextField. and override canBecomeFirstResponder then return true.
override var inputAccessoryView: UIView? {
get {
self.inputBar.frame.size.height = self.barHeight // 50.0
self.inputBar.clipsToBounds = true
return self.inputBar
}
}
override var canBecomeFirstResponder: Bool{
return true
}
You could observe the frame of the keyboard using the NSNotificationCenter and create a method which will be called when the frame changes. In this method you get the coordinates of the keyboard and can reposition your inputView accordingly.
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.keyboardDidChangeFrame(_:)), name: UIKeyboardWillChangeFrameNotification, object: nil)
}
func keyboardDidChangeFrame(notification: NSNotification) {
let keyboardScreenFrameEnd = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as! NSValue).CGRectValue()
let keyboardViewFrameEnd = view.convertRect(keyboardScreenEndFrame, fromView: view.window)
let keyboardHeight = keyboardViewFrameEnd.height
// calculate the offset of your inputView
inputView.frame = CGRectOffset(inputView?.frame, ..., ...)
}

Resources