This is the result I currently get (unwanted): https://vimeo.com/459984986.
This is the result I get using a workaround (almost what I want): https://vimeo.com/459986233.
The summary of what my problem is, is the following: (1) I have installed the "IQKeyboardManagerSwift" cocoapod, (2) I have a custom cell in my UITableViewController that is a FolingCell (another cocoapod) and contains a textfield, (3) when I tap inside the textfield, I want the view to automatically scroll up to make the textfield visible (above the keyboard), which should be automatically taken care of by IQKeyboardManager (yes, I have added the "enable" code in my AppDelegate).
As seen in the "unwanted" result video above, the view seemingly attempts to scroll but, ultimately, just ends up jittering up and down slightly before ending up at the original position while the keyboard slides up and covers the textfield undesirably.
Further, the little "workaround" I found which I do not want to settle for is adding the following line to the textfield's IBAction editingDidEnd outlet/action function: "sender.becomeFirstResponder()". Before I updated to XCode 12 and iOS 14 on my iPhone on which I run and test my app, I did not have to hassle with ".becomeFirstResponder()" or ".resignResponder()" because that's what IQKeyboardManager does automatically. Now, adding the aforementioned line of code allows the UITableViewController to move the view above the keyboard. BUT, if you look closely, it sits the textfield right on top of the keyboard which is clearly not done by IQKeyboardManager because IQKeyboardManager has a default offset of CGFloat(10) between the textfield and keyboard.
I think the relevant parts of code are my custom cell (called "SavedImageFoldingImageCell") because that is where I create the textfield in question and my UITableViewController (called "SavedImageTableViewController").
Please, any help, suggestion, advice you can offer would help me a ton and be super appreciated. Thanks!
Yes, my code looks horrific because I have no proper training/education in best practices for coding or coding in general. Suggestions there would be appreciated as well! If you see ways I can shorten and organize my code, by all means divulge your secrets!
Custom Cell's Code:
import UIKit
import FoldingCell
import LGButton
import TextFieldEffects
import SCLAlertView
class SavedImageFoldingImageCell: FoldingCell, UITextFieldDelegate {
// CLOSED
@IBOutlet weak var enterAGreetingLabelClosed: UILabel!
@IBOutlet weak var savedImageView1Closed: UIImageView!
@IBOutlet weak var savedImageView2Closed: UIImageView!
@IBOutlet weak var openCellButton: LGButton!
// OPEN
@IBOutlet weak var confirmLabelOpen: UILabel!
@IBOutlet weak var englishLabelOpen: UILabel!
@IBOutlet weak var spanishLabelOpen: UILabel!
@IBOutlet weak var savedImageView1Open: UIImageView!
@IBOutlet weak var savedImageView2Open: UIImageView!
@IBOutlet weak var enterAGreetingLabelOpen: UILabel!
@IBOutlet weak var enterAGreetinTextFieldOpen: HoshiTextField!
@IBOutlet weak var barViewOpen: UIView!
// 'Continue' Button
@IBOutlet weak var continueButton: LGButton!
// Hamburger Button
@IBOutlet weak var hamburgerButton: UIButton!
// MARK: - Setting-up Labels
// CLOSED Labels
var enterAGreetingClosed: String = "" {
didSet {
enterAGreetingLabelClosed.text = String(enterAGreetingClosed)
}
}
// OPEN Labels
var englishOpen: String = "" {
didSet {
englishLabelOpen.text = String(englishOpen)
}
}
var spanishOpen: String = "" {
didSet {
spanishLabelOpen.text = String(spanishOpen)
}
}
var enterAGreetingOpen: String = "" {
didSet {
enterAGreetingLabelOpen.text = String(enterAGreetingOpen)
}
}
var confirmOpen: String = "" {
didSet {
confirmLabelOpen.text = String(confirmOpen)
}
}
override func awakeFromNib() {
barViewOpen.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner] // Top right corner, Top left corner respectively
barViewOpen.clipsToBounds = true
foregroundView.layer.cornerRadius = 10
foregroundView.layer.masksToBounds = true
super.awakeFromNib()
// Initialization code
enterAGreetinTextFieldOpen.delegate = self
}
override func animationDuration(_ itemIndex: NSInteger, type: FoldingCell.AnimationType) -> TimeInterval {
let durations = [0.26, 0.2, 0.2]
return durations[itemIndex]
}
@IBAction func enterAGreetingTextFieldEditingDidBegin(_ sender: HoshiTextField) {
print("@enterAGreetingTextFieldEditingDidBegin -> cell.swift: does nothing as of now.")
}
@IBAction func enterAGreetingTextFieldEditingDidEnd(_ sender: HoshiTextField) {
if sender.text!.isEmpty != true {
// Activates and shows the 'Continue' button
continueButton.isEnabled = true
continueButton.alpha = 1
print("enterAGreetingTextFieldEditingDidEnd@Cell -> cell.swift: activated 'Continue' button because textField contained text after editing ended.")
} else if sender.text!.isEmpty == true {
// Deactivates and hides the 'Continue' button
continueButton.isEnabled = false
continueButton.alpha = 0.5
print("enterAGreetingTextFieldEditingDidEnd@Cell: deactivated 'Continue' button because textField was empty after editing ended.")
}
}
// JUST FYI, THIS DOES NOT GET CALLED
private func textFieldShouldReturn(_ textField: HoshiTextField) -> Bool {
let storyboard = UIStoryboard.init(name: "Main", bundle: nil)
let savedImageTableVC = storyboard.instantiateViewController(withIdentifier: "SavedImageTableViewController") as! SavedImageTableViewController
savedImageTableVC.loadViewIfNeeded()
if textField.text?.isEmpty == false {
savedImageTableVC.liveGreeting = textField.text!
savedImageTableVC.savedImageTableView.reloadData()
print(".CELL @textFieldShouldReturn() -> savedImageTableVC.savedImagesArray: \(savedImageTableVC.savedImagesArray).")
let indexPathRow = textField.tag
StructOperation.globalVariable.tappedCellIndexRow = indexPathRow
print(".CELL @textFieldShouldReturn() -> StructOperation.globalVariable.tappedCellIndexRow: \(StructOperation.globalVariable.tappedCellIndexRow).")
savedImageTableVC.goToSend()
print("User entered a greeting in enterAGreetingTextField: \(savedImageTableVC.liveGreeting).")
} else if textField.text?.isEmpty == true {
savedImageTableVC.liveGreeting = ""
SCLAlertView().showError("Error", subTitle: "To send an image, a greeting must also be specified.", closeButtonTitle: "Done", timeout: nil, colorStyle: SCLAlertViewStyle.error.defaultColorInt, colorTextButton: 0xFFFFFF, circleIconImage: nil, animationStyle: .topToBottom)
print("User did not enter a greeting in enterAGreetingTextField.")
}
return true
}
}
// MARK: - Actions ⚡️
extension SavedImageFoldingImageCell {
@IBAction func openCellButtonTapped() {
// print("The open-cell button was tapped (just a downward arrow).")
}
@IBAction func enterAGreetingTextfieldOpenEditingDidEnd() {
// print("'enterAGreetingTextField' finished editing.")
}
@IBAction func continueButtonTapped(_: AnyObject) {
// print("The 'Continue' button was tapped.")
}
@IBAction func hamburgerButtonTapped(_: AnyObject) {
// print("The hamburger button was tapped.")
}
}
UITableViewController's Code:
import UIKit
import TextFieldEffects
import SCLAlertView
import MessageUI
import FoldingCell
import MLKitTranslate
class SavedImageTableViewController: UITableViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate, MFMessageComposeViewControllerDelegate, UITextFieldDelegate, UIContextMenuInteractionDelegate {
@IBOutlet var savedImageTableView: UITableView!
*** Omitted other irrelevant outlets, vars, & constants ***
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
activityIndicator() // Omitted because nothing regarding the textfield is called here
refresh() // Omitted ""
}
override func viewDidLoad() {
super.viewDidLoad()
setup() // Omitted ""; (assigned self to tableview's delegate and dataSource here)
checkIfSavedImages() // Omitted ""
getDeadlineInSeconds() // Omitted ""
// For deadline countdown timer
countdownTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateCounter), userInfo: nil, repeats: true)
// Stops loading spinner and hides view
self.indicator.stopAnimating()
self.indicator.hidesWhenStopped = true
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
}
@IBAction func enterAGreetingTextFieldEditingDidEnd(_ sender: HoshiTextField) {
let cell = savedImageTableView.dequeueReusableCell(withIdentifier: "SavedImageFoldingImageCell") as! SavedImageFoldingImageCell
if sender.text?.isEmpty == true {
sender.text = ""
cell.continueButton.isEnabled = false
cell.continueButton.alpha = 0.5
// sender.resignFirstResponder()
} else if sender.text?.isEmpty == false {
self.liveGreeting = sender.text!
cell.continueButton.isEnabled = true
cell.continueButton.alpha = 1
}
}
@IBAction func enterAGreetingTextFieldEditingDidBegin(_ sender: HoshiTextField) {
sender.becomeFirstResponder() // Fixes IQKeyboardManager (rather, allows UITableViewController to properly scroll)
let indexPathRow = sender.tag
StructOperation.globalVariable.tappedCellIndexRow = indexPathRow
print("enterAGreetingTextFieldEditingDidEnd()@ViewController -> StructOperation.globalVariable.tappedCellIndexRow: \(StructOperation.globalVariable.tappedCellIndexRow).")
}
//MARK: - TableView Functions
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
guard case let cell as SavedImageFoldingImageCell = cell else {
return
}
cell.enterAGreetingClosed = "Enter a Greeting"
cell.enterAGreetingOpen = "Enter a Greeting"
cell.englishOpen = "English"
cell.spanishOpen = "Spanish"
cell.confirmOpen = "Confirm"
// Greeting TextField
cell.enterAGreetinTextFieldOpen.delegate = self
cell.enterAGreetinTextFieldOpen.tag = indexPath.row
// Open-cell button (downward arrow)
cell.openCellButton.tag = indexPath.row
// 'Continue' button
cell.continueButton.tag = indexPath.row
cell.continueButton.isEnabled = false
cell.continueButton.alpha = 0.5
// Hamburger button
cell.hamburgerButton.tag = indexPath.row
// Closed/Open Images (English, Spanish)
let calculatedIndex = (indexPath.row * 2) + 1
cell.savedImageView1Closed.image = savedImagesArray[calculatedIndex - 1]
cell.savedImageView2Closed.image = savedImagesArray[calculatedIndex]
cell.savedImageView1Open.image = savedImagesArray[calculatedIndex - 1]
cell.savedImageView2Open.image = savedImagesArray[calculatedIndex]
cell.backgroundColor = .clear
if cellHeights[indexPath.row] == Constants.closeCellHeight {
cell.unfold(false, animated: false, completion: nil)
} else {
cell.unfold(true, animated: false, completion: nil)
}
// Allows recognition of tapping the 'Continue' button by connecting that button's outlet to a newly created function down below a little
cell.continueButton.addTarget(self, action: #selector(SavedImageTableViewController.continueButtonTapped(_:)), for: .touchUpInside)
// Allows recognition of tapping the 'open cell' button (just a downward arrow) by connecting that button's outlet to a newly created function down below a little
cell.openCellButton.addTarget(self, action: #selector(openCellButtonTapped(_:)), for: .touchUpInside)
// Allows recognition of tapping the 'hamburger' button (just three horizontal lines as a button) by connecting that button's outlet to a newly created function down below a little
cell.hamburgerButton.addTarget(self, action: #selector(hamburgerButtonTapped(_:)), for: .touchUpInside)
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = savedImageTableView.dequeueReusableCell(withIdentifier: "SavedImageFoldingImageCell", for: indexPath) as! FoldingCell
let durations: [TimeInterval] = [0.26, 0.2, 0.2]
cell.durationsForExpandedState = durations
cell.durationsForCollapsedState = durations
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard case let cell as FoldingCell = tableView.cellForRow(at: indexPath) else {
return
}
if cell.isAnimating() {
return
}
var duration = 0.0
let cellIsCollapsed = cellHeights[indexPath.row] == Constants.closeCellHeight
if cellIsCollapsed {
cellHeights[indexPath.row] = Constants.openCellHeight
cell.unfold(true, animated: true, completion: nil)
duration = 0.5
} else {
cellHeights[indexPath.row] = Constants.closeCellHeight
cell.unfold(false, animated: true, completion: nil)
duration = 0.8
}
UIView.animate(withDuration: duration, delay: 0, options: .curveEaseOut, animations: {
tableView.beginUpdates()
tableView.endUpdates()
// fix https://github.com/Ramotion/folding-cell/issues/169
if cell.frame.maxY > tableView.frame.maxY {
tableView.scrollToRow(at: indexPath, at: UITableView.ScrollPosition.bottom, animated: true)
}
}, completion: nil)
// Provide haptic feedback of success
let generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(.success)
}
***Rest is Omitted because I think its irrelevant to the problem***
I solved my own problem by simply switching the class from UITableViewController -> UIViewController. I went through the closed issues on IQKeyboardManager's Github page and discovered that since Apple's UITableViewController automatically handles movement of the view and keyboard, IQKeyboardManager's developer chose to ignore textfield's that are in a UITableViewController. Therefore, you must change the class to a UIViewController or another supported class to have textfields recognized by IQKeyboardManager.