Search code examples
swiftuitableviewscrollcustom-celliqkeyboardmanager

Why doesn't IQKeyboardManager scroll my UITableView to make my custom cell's textfield visible


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***

Solution

  • 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.