Swift - remove cell from TableView after Button-Tap - ios

in my project I have a UITableView. The cells inside of that contains among other things a UIButton which acts like a "check-box", so the user can tick of a task.
My question: How can I delete a cell after the user presses the UIButton inside of it?
This is my customCell :
import UIKit
class WhishCell: UITableViewCell {
let label: UILabel = {
let v = UILabel()
v.font = UIFont(name: "AvenirNext", size: 23)
v.textColor = .white
v.font = v.font.withSize(23)
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let checkButton: UIButton = {
let v = UIButton()
v.backgroundColor = .darkGray
// v.layer.borderColor = UIColor.red.cgColor
// v.layer.borderWidth = 2.0
v.translatesAutoresizingMaskIntoConstraints = false
v.setBackgroundImage(UIImage(named: "boxUnchecked"), for: .normal)
return v
}()
public static let reuseID = "WhishCell"
required init?(coder: NSCoder) {fatalError("init(coder:) has not been implemented")}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.backgroundColor = .clear
// add checkButton
self.contentView.addSubview(checkButton)
self.checkButton.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 20).isActive = true
self.checkButton.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
self.checkButton.widthAnchor.constraint(equalToConstant: 40).isActive = true
self.checkButton.heightAnchor.constraint(equalToConstant: 40).isActive = true
self.checkButton.addTarget(self, action: #selector(checkButtonTapped), for: .touchUpInside)
// add label
self.contentView.addSubview(label)
self.label.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 70).isActive = true
self.label.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
}
#objc func checkButtonTapped(){
self.checkButton.setBackgroundImage(UIImage(named: "boxChecked"), for: .normal)
self.checkButton.alpha = 0
self.checkButton.transform = CGAffineTransform(scaleX: 1.3, y: 1.3)
UIView.animate(withDuration: 0.3) {
self.checkButton.alpha = 1
self.checkButton.transform = CGAffineTransform.identity
}
}
}

You can access the tableView and the indexPath within the cell with this extension:
extension UITableViewCell {
var tableView: UITableView? {
return (next as? UITableView) ?? (parentViewController as? UITableViewController)?.tableView
}
var indexPath: IndexPath? {
return tableView?.indexPath(for: self)
}
}
With the help of this other extension:
extension UIView {
var parentViewController: UIViewController? {
var parentResponder: UIResponder? = self
while parentResponder != nil {
parentResponder = parentResponder!.next
if let viewController = parentResponder as? UIViewController {
return viewController
}
}
return nil
}
}
So then you can delete the cell as usual:
tableView!.deleteRows(at: [indexPath!], with: .automatic)
Note that the tableView should be responsible for managing cells.

When you are creating the cell and setting the button selector all you need to set button tag and selector like this:
self.checkButton.tag = indexPath.row
self.checkButton.addTarget(self, action: #selector(checkButtonTapped(sender:)), for: .touchUpInside)
and make change this in selector:
#objc func checkButtonTapped(sender : UIButton){
let index = sender.tag
self.tableView.deleteRows(at: [index], with: .automatic)
//Add all your code here..
}
Thanks

Change your checkButtonTapped whit this
#objc func checkButtonTapped() {
self.checkButton.setBackgroundImage(UIImage(named: "boxChecked"), for: .normal)
self.checkButton.alpha = 0
self.checkButton.transform = CGAffineTransform(scaleX: 1.3, y: 1.3)
UIView.animate(withDuration: 0.3) {
self.checkButton.alpha = 1
self.checkButton.transform = CGAffineTransform.identity
}
if let tableView = self.superview as? UITableView {
let indexPath = tableView.indexPath(for: self)
tableView.dataSource?.tableView!(tableView, commit: .delete, forRowAt: indexPath!)
}
}
And add the next method to your UITableViewDelegate implementation
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
print("Deleted")
self.texts.remove(at: indexPath.row)
self.tableview.deleteRows(at: [indexPath], with: .automatic)
}
}

A swifty way is a callback closure.
In the cell add a property
var callback : ((UITableViewCell) -> Void)?
In the action call the callback and pass the cell
#objc func checkButtonTapped(){
...
callback?(self)
}
In the controller in cellForRowAt assign a closure to the callback property
cell.callback = { cell in
let indexPath = tableView.indexPath(for: cell)!
// insert here a line to remove the item from the data source array.
tableView.deleteRows(at: [indexPath], with: .automatic)
}
This native Swift solution is much more efficient than assigning tags, objective-c-ish target/action or cumbersome view hierarchy math.

Related

Presenting UICollectionView from a UIButton swift

first, I hope I don't break any rules or make it long but I'm really stuck and spent the whole day about that so I would love to get help with that :).
Okay so I'm having a popped up view controller using Panmodel 3rd party and I've got a UIButton inside of a UITableView custom cell, now I want to present my custom calendar when the user press the button, now I followed Ray tutorial(Link) on how to do a custom calendar and it still doesn't work for me, for some reason when I press the button it just shows a clear view + freezes my screen :|, I'm going to post the code of my tableView setup + the cell of the UIButton, not going to post the code of the calendar because it's long and I don't want to spam, if needed I will post that :) once again thanks for the help really spent the whole day about it and found myself with no solution, so here's my code:
Code: TableViewVC:
import UIKit
import PanModal
class FilterTableViewController: UITableViewController, PanModalPresentable {
var panScrollable: UIScrollView? {
return tableView
}
var albumsPickerIndexPath: IndexPath? // indexPath of the currently shown albums picker in tableview.
var datesCell = DatesCell()
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
// registerTableViewCells()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// tableView.frame = view.bounds
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
// MARK: - View Configurations
func setupTableView() {
tableView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true
tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
tableView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
tableView.separatorStyle = .singleLine
tableView.isScrollEnabled = false
tableView.allowsSelection = true
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 600
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
}
func indexPathToInsertDatePicker(indexPath: IndexPath) -> IndexPath {
if let albumsPickerIndexPath = albumsPickerIndexPath, albumsPickerIndexPath.row < indexPath.row {
return indexPath
} else {
return IndexPath(row: indexPath.row + 1, section: indexPath.section)
}
}
// MARK: - UITableViewDataSource
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// If datePicker is already present, we add one extra cell for that
if albumsPickerIndexPath != nil {
return 5 + 1
} else {
return 5
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch indexPath.row {
case 0:
let byActivityCell = UINib(nibName: "byActivityCell",bundle: nil)
self.tableView.register(byActivityCell,forCellReuseIdentifier: "byActivityCell")
let activityCell = tableView.dequeueReusableCell(withIdentifier: "byActivityCell", for: indexPath) as! byActivityCell
activityCell.selectionStyle = .none
return activityCell
case 1:
let byTypeCell = UINib(nibName: "ByType",bundle: nil)
self.tableView.register(byTypeCell,forCellReuseIdentifier: "byTypeCell")
let typeCell = tableView.dequeueReusableCell(withIdentifier: "byTypeCell", for: indexPath) as! ByType
typeCell.selectionStyle = .none
return typeCell
case 2:
let byHashtagsCell = UINib(nibName: "ByHashtags",bundle: nil)
self.tableView.register(byHashtagsCell,forCellReuseIdentifier: "byHashtagsCell")
let hashtagsCell = tableView.dequeueReusableCell(withIdentifier: "byHashtagsCell", for: indexPath) as! ByHashtags
hashtagsCell.selectionStyle = .none
return hashtagsCell
case 3:
let byDatesCell = UINib(nibName: "DatesCell",bundle: nil)
self.tableView.register(byDatesCell,forCellReuseIdentifier: "byDatesCell")
let datesCell = tableView.dequeueReusableCell(withIdentifier: "byDatesCell", for: indexPath) as! DatesCell
datesCell.selectionStyle = .none
datesCell.datesTableViewCellDelegate = self
return datesCell
case 4:
let byAlbumCell = UINib(nibName: "AlbumCell",bundle: nil)
self.tableView.register(byAlbumCell,forCellReuseIdentifier: "byAlbumCell")
let albumCell = tableView.dequeueReusableCell(withIdentifier: "byAlbumCell", for: indexPath) as! AlbumCell
albumCell.configureCell(choosenAlbum: "Any")
albumCell.selectionStyle = .none
return albumCell
case 5:
let albumPickerCell = UINib(nibName: "AlbumsPickerTableViewCell", bundle: nil)
self.tableView.register(albumPickerCell, forCellReuseIdentifier: "albumPickerCell")
let albumsPicker = tableView.dequeueReusableCell(withIdentifier: "albumPickerCell", for: indexPath) as! AlbumsPickerTableViewCell
return albumsPicker
default:
return UITableViewCell()
}
}
// MARK: - footer Methods:
override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
return getfooterView()
}
func getfooterView() -> UIView
{
let footerView = UIView(frame: CGRect(x: 0, y: 0, width: tableView.frame.width, height: 400))
let applyFiltersBtn = UIButton(frame: CGRect(x: 0, y: 0, width: 380, height: 35))
applyFiltersBtn.center = footerView.center
applyFiltersBtn.layer.cornerRadius = 12
applyFiltersBtn.layer.masksToBounds = true
applyFiltersBtn.setTitle("Apply Filters", for: .normal)
applyFiltersBtn.backgroundColor = #colorLiteral(red: 0.1957295239, green: 0.6059523225, blue: 0.960457623, alpha: 1)
// doneButton.addTarget(self, action: #selector(hello(sender:)), for: .touchUpInside)
footerView.addSubview(applyFiltersBtn)
return footerView
}
override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return 10
}
// MARK: TableViewDelegate Methods:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: false)
tableView.beginUpdates()
// 1 - We Delete the UIPicker when the user "Deselect" the row.
if let datePickerIndexPath = albumsPickerIndexPath, datePickerIndexPath.row - 1 == indexPath.row {
tableView.deleteRows(at: [datePickerIndexPath], with: .fade)
self.albumsPickerIndexPath = nil
} else {
// 2
// if let datePickerIndexPath = albumsPickerIndexPath {
// tableView.deleteRows(at: [datePickerIndexPath], with: .fade)
// }
albumsPickerIndexPath = indexPathToInsertDatePicker(indexPath: indexPath)
tableView.insertRows(at: [albumsPickerIndexPath!], with: .fade)
tableView.deselectRow(at: indexPath, animated: true)
}
tableView.endUpdates()
if indexPath.row == 4 {
let pickerController = CalendarPickerViewController(
baseDate: Date(),
selectedDateChanged: { [weak self] date in
guard let self = self else { return }
// self.item.date = date
self.tableView.reloadRows(at: [IndexPath(row: 3, section: 0)], with: .fade)
})
present(pickerController, animated: true, completion: nil)
}
}
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
if indexPath.row == 4 {
return indexPath
} else {
return nil
}
}
}
extension FilterTableViewController: DatesTableViewCellDelegate {
func didButtonFromPressed() {
print("Button From is Pressed")
let pickerController = CalendarPickerViewController(
baseDate: Date(),
selectedDateChanged: { [weak self] date in
guard let self = self else { return }
// self.item.date = date
self.tableView.reloadRows(at: [IndexPath(row: 3, section: 0)], with: .fade)
})
present(pickerController, animated: true, completion: nil)
}
func didButtonToPressed() {
print("Button To is Pressed")
//TODO: Present our custom calendar
let vcToDisplay = CalendarPickerViewController(baseDate: Date()) { (date) in
self.tableView.reloadRows(at: [IndexPath(row: 3, section: 0)], with: .fade)
}
self.present(vcToDisplay, animated: true, completion: nil)
}
}
custom cell code:
import UIKit
// MARK: - Class Protocols:
protocol DatesTableViewCellDelegate { // a delegate to tell when the user selected the button:
func didButtonFromPressed()
func didButtonToPressed()
}
class DatesCell: UITableViewCell {
#IBOutlet var fromDate: UIButton!
#IBOutlet var toDate: UIButton!
var datesTableViewCellDelegate: DatesTableViewCellDelegate?
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
fromDate.layer.cornerRadius = 5
toDate.layer.cornerRadius = 5
fromDate.layer.borderWidth = 1
toDate.layer.borderWidth = 1
fromDate.layer.borderColor = #colorLiteral(red: 0.2005972862, green: 0.6100016236, blue: 0.9602670074, alpha: 1)
toDate.layer.borderColor = #colorLiteral(red: 0.2005972862, green: 0.6100016236, blue: 0.9602670074, alpha: 1)
fromDate.layer.masksToBounds = true
toDate.layer.masksToBounds = true
self.preservesSuperviewLayoutMargins = false
self.separatorInset = UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 15)
self.layoutMargins = UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 15)
}
// MARK: - UIButton Methods:
#IBAction func fromDateButtonIsPressed(_ sender: UIButton) {
datesTableViewCellDelegate?.didButtonFromPressed()
}
#IBAction func toDateButtonIsPressed(_ sender: UIButton) {
datesTableViewCellDelegate?.didButtonToPressed()
}
}
Found the problem, the problem was with the auto-layout, I changed the auto layout to be like this:
override func viewDidLoad() {
super.viewDidLoad()
collectionView.backgroundColor = .yellow
view.addSubview(dimmedBackgroundView)
view.addSubview(collectionView)
var constraints = [
dimmedBackgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
dimmedBackgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
dimmedBackgroundView.topAnchor.constraint(equalTo: view.topAnchor),
dimmedBackgroundView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
]
constraints.append(contentsOf: [
//1
collectionView.leadingAnchor.constraint(
equalTo: view.readableContentGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(
equalTo: view.readableContentGuide.trailingAnchor),
//2
collectionView.centerYAnchor.constraint(
equalTo: view.centerYAnchor,
constant: 10),
//3
collectionView.heightAnchor.constraint(
equalTo: view.heightAnchor,
multiplier: 0.5)
])
NSLayoutConstraint.activate(constraints)
// NSLayoutConstraint.activate([
// dimmedBackgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
// dimmedBackgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
// dimmedBackgroundView.topAnchor.constraint(equalTo: view.topAnchor),
// dimmedBackgroundView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
// ])
}

When I change the text color of UITextField inside a UITableView, the color has applied to another UITextfield when I scroll

I really don't understand why this happen. I'm changing the text color of a UITextField that is inside a UITableView if the text has changed but when I scroll down or up the tableView other UITextFields change the text color.
How can solve this problem?
Here is my code and some screenshot of the problem:
import Foundation
import UIKit
private let reuseIdentifier = "ApcGenericCell"
private let portIdentifier = "ApcPortCell"
private let addressIdentifier = "ApcAddressCell"
class MyViewController: UIViewController {
private var tableView: UITableView!
private var bottomConstraint: NSLayoutConstraint!
private var newBottomConstraint: NSLayoutConstraint!
var apc = [Apc]()
override func viewDidLoad() {
super.viewDidLoad()
// configure the table view for the left menu
configureTableView()
}
private func configureTableView() {
tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.delegate = self
tableView.dataSource = self
tableView.rowHeight = 60
tableView.register(UINib(nibName: "ApcGeneric", bundle: nil), forCellReuseIdentifier: reuseIdentifier)
tableView.register(UINib(nibName: "ApcPortAt", bundle: nil), forCellReuseIdentifier: portIdentifier)
tableView.register(UINib(nibName: "ApcIpAddress", bundle: nil), forCellReuseIdentifier: addressIdentifier)
self.view.addSubview(tableView)
tableView.backgroundColor = .clear
tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: view.frame.height / 8).isActive = true
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: view.frame.width / 8).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -(view.frame.width / 8)).isActive = true
bottomConstraint = tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -(view.frame.height / 8))
bottomConstraint.isActive = true
tableView.alwaysBounceVertical = false
tableView.tableFooterView = UIView(frame: .zero)
// register for notifications when the keyboard appears and disappears:
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(note:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(note:)), name: UIResponder.keyboardWillHideNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(receivedMessage), name: Notification.Name("ReceivedMessage"), object: nil)
apc = ApcSection.emptySection()
self.tableView.reloadData()
}
#objc func receivedMessage() {
DispatchQueue.global(qos: .default).async {
ApcSection.fetchData(map: self.apcConfig, { (apcResult) in
self.apc = apcResult
DispatchQueue.main.async {
self.tableView.reloadData()
}
})
}
}
// Handle keyboard frame changes here.
// Use the CGRect stored in the notification to determine what part of the screen the keyboard will cover.
// Adjust our table view's bottomAnchor so that the table view content avoids the part of the screen covered by the keyboard
#objc func keyboardWillShow(note: NSNotification) {
// read the CGRect from the notification (if any)
if let newFrame = (note.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
if bottomConstraint.isActive {
bottomConstraint.isActive = false
newBottomConstraint = tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -newFrame.height)
newBottomConstraint.isActive = true
tableView.updateConstraints()
}
}
}
// User dismiss the keyboard
#objc func keyboardWillHide(note: NSNotification) {
newBottomConstraint.isActive = false
bottomConstraint.isActive = true
tableView.updateConstraints()
}
#objc func textHasChanged(sender: UITextField) {
let cell = sender.superview?.superview as! UITableViewCell
let indexPath = tableView.indexPath(for: cell)
if let index = indexPath?.row {
if let _ = apc[index] {
// change textColor if the value has been changed
if sender.text != apc[index]!) {
sender.textColor = .systemRed
} else {
sender.textColor = .myBlue
}
}
}
}
}
extension MyViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return apc.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: portIdentifier, for: indexPath) as! ApcPortAt
cell.selectionStyle = .none
return cell
} else if indexPath.row == 7 || indexPath.row == 9 {
let cell = tableView.dequeueReusableCell(withIdentifier: addressIdentifier, for: indexPath) as! ApcIpAddress
cell.selectionStyle = .none
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! ApcGeneric
cell.value.text = apc[indexPath.row]
cell.value.delegate = self
cell.value.addTarget(self, action: #selector(textHasChanged(sender:)), for: .editingChanged)
cell.selectionStyle = .none
return cell
}
}
}
Normal View
Editing
Scroll down after editing
Return to the top
You have 2 possible solutions
1. Apply the changes in cellForRowAt of UITableViewDataSource delegate.
2. Subclass UITableViewCell and override prepareForReuse() which within it you can make your updates. And don't forget to register the cell subclass with your table view.
I consider solution #1 much easier
In UITableView dequeueReusableCell- Each UITableViewCell will be reused several times with different data(image).
In your case, When you scrolled, cell at IndexPath(row: x, section: 0) was reused by another cell that is displaying at the top. Ex: cell with red text -> this cell x have red text, because you did not reset the text color to it
Solution:
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! ApcGeneric
cell.value.text = apc[indexPath.row]
if index == selectedIndex { // condition to red text
cell.value.textColor = .systemRed
} else {
cell.value.textColor = .myBlue
}
cell.value.delegate = self
cell.value.addTarget(self, action: #selector(textHasChanged(sender:)), for: .editingChanged)
cell.selectionStyle = .none
return cell

Dynamically resize TableViewController Cell

In my project I have a SignUpViewController which looks like this:
All the textFields are custom-cells within a tableViewController.
TableView:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 7
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// 1st cell -> email textfield
if indexPath.row == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "SignUpEmailCell", for: indexPath) as! SignUpEmailCell
return cell
// 2nd cell -> anzeigename
}else if indexPath.row == 1 {
let cell = tableView.dequeueReusableCell(withIdentifier: "SignUpAnzeigeName", for: indexPath) as! SignUpAnzeigeName
return cell
// 3rd cell -> Wishlist-Handle
}else if indexPath.row == 2 {
let cell = tableView.dequeueReusableCell(withIdentifier: "SignUpHandleCell", for: indexPath) as! SignUpHandleCell
return cell
// 4th cell -> passwort textfield
}else if indexPath.row == 3 {
let cell = tableView.dequeueReusableCell(withIdentifier: "SignUpPasswordCell", for: indexPath) as! SignUpPasswordCell
return cell
// 5th cell -> repeat password textfield
}else if indexPath.row == 4 {
let cell = tableView.dequeueReusableCell(withIdentifier: "SignUpPasswordRepeatCell", for: indexPath) as! SignUpPasswordRepeatCell
return cell
// 6th cell -> document label
}else if indexPath.row == 5 {
let cell = tableView.dequeueReusableCell(withIdentifier: "SignUpDocumentCell", for: indexPath) as! SignUpDocumentCell
return cell
}
// last cell -> signUpButton
let cell = tableView.dequeueReusableCell(withIdentifier: "SignUpButtonCell", for: indexPath) as! SignUpButtonCell
return cell
}
Password-Cell: (basic structure is the same for every cell)
class SignUpPasswordCell: UITableViewCell, UITextFieldDelegate {
public static let reuseID = "SignUpPasswordCell"
lazy var eyeButton: UIButton = {
let v = UIButton()
v.addTarget(self, action: #selector(eyeButtonTapped), for: .touchUpInside)
v.setImage(UIImage(named: "eyeOpen"), for: .normal)
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
lazy var passwordTextField: CustomTextField = {
let v = CustomTextField()
v.borderActiveColor = .white
v.borderInactiveColor = .white
v.textColor = .white
v.font = UIFont(name: "AvenirNext-Regular", size: 17)
v.placeholder = "Passwort"
v.placeholderColor = .white
v.placeholderFontScale = 0.8
v.minimumFontSize = 13
v.borderStyle = .line
v.addTarget(self, action: #selector(SignUpPasswordCell.passwordTextFieldDidChange(_:)),for: .editingChanged)
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
required init?(coder: NSCoder) {fatalError("init(coder:) has not been implemented")}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.backgroundColor = .clear
passwordTextField.delegate = self
eyeButton.isHidden = true
passwordTextField.textContentType = .newPassword
passwordTextField.isSecureTextEntry.toggle()
setupViews()
}
func setupViews(){
contentView.addSubview(passwordTextField)
contentView.addSubview(eyeButton)
passwordTextField.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
passwordTextField.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
passwordTextField.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
passwordTextField.heightAnchor.constraint(equalToConstant: 60).isActive = true
eyeButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -5).isActive = true
eyeButton.centerYAnchor.constraint(equalTo: centerYAnchor, constant: 10).isActive = true
}
var check = true
#objc func eyeButtonTapped(_ sender: Any) {
check = !check
if check == true {
eyeButton.setImage(UIImage(named: "eyeOpen"), for: .normal)
} else {
eyeButton.setImage(UIImage(named: "eyeClosed"), for: .normal)
}
passwordTextField.isSecureTextEntry.toggle()
if let existingText = passwordTextField.text, passwordTextField.isSecureTextEntry {
/* When toggling to secure text, all text will be purged if the user
continues typing unless we intervene. This is prevented by first
deleting the existing text and then recovering the original text. */
passwordTextField.deleteBackward()
if let textRange = passwordTextField.textRange(from: passwordTextField.beginningOfDocument, to: passwordTextField.endOfDocument) {
passwordTextField.replace(textRange, withText: existingText)
}
}
/* Reset the selected text range since the cursor can end up in the wrong
position after a toggle because the text might vary in width */
if let existingSelectedTextRange = passwordTextField.selectedTextRange {
passwordTextField.selectedTextRange = nil
passwordTextField.selectedTextRange = existingSelectedTextRange
}
}
#objc func passwordTextFieldDidChange(_ textField: UITextField) {
if textField.text == "" {
self.eyeButton.isHidden = true
}else {
self.eyeButton.isHidden = false
}
}
}
Problem:
I would like to be able to show some extra information on some textFields when selected.
For example: When passwordTextField is editing I would like to show the password requirements right below the textfield. But the extra information should only be displayed while editing or after editing. When the ViewController is being displayed at first it should still look like the picture above.
I hope my problem is clear and I am grateful for every help.

UITableViewCell delegate not working

Conditions:
Swift 4, Xcode 9.3
Target: iOS 11.3
UI Done Programatically
Using Constraints
Episode is an object that holds the source as a String
Situation:
Here is my custom cell: EpisodeCell.swift
import UIKit
protocol EpisodeCellDelegate {
func didTapPlayButton(url: String)
}
class EpisodeCell: UITableViewCell {
var delegate: EpisodeCellDelegate?
var episode: Episode!
let cellView: UIView = {
let view = UIView()
view.backgroundColor = UIColor.init(hex: "#EBE4D3")
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
let episodeTitle: UILabel = {
let label = UILabel()
label.textColor = .darkGray
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let playButton: UIButton = {
let btn = UIButton.init(type: .custom)
btn.setTitle("PLAY", for: .normal)
btn.setTitleColor(.gray, for: .normal)
btn.isUserInteractionEnabled = true
btn.addTarget(self, action: #selector(playPressed), for: .touchUpInside)
return btn
}()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setup()
}
private func setup(){
self.accessoryType = .none
self.addSubview(cellView)
cellView.addSubview(episodeTitle)
cellView.addSubview(playButton)
cellView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
cellView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
cellView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
cellView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
episodeTitle.topAnchor.constraint(equalTo: cellView.topAnchor, constant: 10).isActive = true
episodeTitle.leadingAnchor.constraint(equalTo: cellView.leadingAnchor, constant: 10).isActive = true
episodeTitle.trailingAnchor.constraint(equalTo: cellView.trailingAnchor).isActive = true
episodeTitle.centerYAnchor.constraint(equalTo: cellView.centerYAnchor).isActive = true
playButton.translatesAutoresizingMaskIntoConstraints = false
playButton.trailingAnchor.constraint(equalTo: cellView.trailingAnchor, constant: -10).isActive = true
playButton.centerYAnchor.constraint(equalTo: cellView.centerYAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("initCoder has not been implemented")
}
#objc func playPressed() {
self.delegate?.didTapPlayButton(url: episode.source)
}
}
And here is how I implemented on my View Controller with the tableview: EpisodesViewController.swift
extension EpisodesViewController: EpisodeCellDelegate {
func didTapPlayButton(url: String) {
print("WOAH: \(url)")
}
}
extension EpisodesViewController: UITableViewDelegate, UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "episodeCell") as! EpisodeCell
cell.episode = series.episodes![indexPath.row]
cell.episodeTitle.text = ep.episodeName
cell.delegate = self
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return (series.episodes?.count)!
}
}
Problem:
I'm having difficulty in working the button to be tapped in a custom table view cell. My tableview conforms to the protocol but it doesn't work.
You should do lazy initialisation for control inside the tableviewcell. Below code does the magic for me. Just change the below part of the code alone
lazy var cellView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
lazy var episodeTitle: UILabel = {
let label = UILabel()
label.textColor = .darkGray
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
lazy var playButton: UIButton = {
let btn = UIButton.init(type: .custom)
btn.setTitle("PLAY", for: .normal)
btn.setTitleColor(.gray, for: .normal)
btn.isUserInteractionEnabled = true
btn.addTarget(self, action: #selector(playPressed), for: .touchUpInside)
return btn
}()
You shouldn't have the action that a cell performs inside the cell. Cells can be reused, and a cell that was in row 1 can end up in row 4 at any time. Instead of your playPressed() routine, use the traditional didSelectRowAtIndexPath call, which uses the IndexPath, not the cell itself, to determine the action to take.

UICollectionViewCell's UiTextField, overlapping in UICollectionViewController

I am trying to design a multistep sign-up menu. For this purpose I am using UICollectionViewController with screen size cells. In these cells, I have a UITextView to ask questions and a UITextField to collect the answers. I also have a Page object for passing in information from uicollectionviewcontroller upon setting.
The problem I'm having now is that after every 3rd page my textField input from 3 pages ago repeats, instead of showing the placeholder. I have noticed yet another problem, the cells seem to be instantiating just 3 times, and not 6 times for how many pages I have. The instantiation order is very odd too. At first it does it once, then upon button click, twice more, then never again.
How can fix this, I am really struggling with this and I have no idea what's going wrong.
This is my code:
import UIKit
class OnboardingPageViewCell: UICollectionViewCell{
override init(frame: CGRect) {
super.init(frame: frame)
print("made a page")
setupView()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var oboardingPage = NewOnboardingPage() {
didSet{
reload()
}
}
private var questionTextField: UITextView = {
var q = UITextView()
q.textColor = UIColor.white
q.textAlignment = .left
q.font = UIFont(name: "Avenir-Black", size: 25)
q.isEditable = true
q.isScrollEnabled = false
q.backgroundColor = UIColor.black
q.translatesAutoresizingMaskIntoConstraints = false
print("made aquestion field")
return q
}()
private var answerField : CustomTextField = {
let tf = CustomTextField.nameField
print("made an answer field")
return tf
}()
private func setupView(){
backgroundColor = UIColor.white
addSubview(questionTextField)
questionTextField.topAnchor.constraint(equalTo: topAnchor, constant: 120).isActive = true
questionTextField.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
questionTextField.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.90).isActive = true
questionTextField.heightAnchor.constraint(equalToConstant: 90).isActive = true
addSubview(answerField)
answerField.topAnchor.constraint(equalTo: questionTextField.bottomAnchor, constant: 20).isActive = true
answerField.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
answerField.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.90).isActive = true
answerField.heightAnchor.constraint(equalToConstant: 90).isActive = true
}
private func reload(){
questionTextField.text = oboardingPage.question
answerField.placeholder = oboardingPage.answerField
}
}
class NewOnboardingPage {
var question : String?
var answerField : String?
}
import UIKit
class SignUpController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
private let cellId = "cellId"
private var pages = [NewOnboardingPage]()
override func viewDidLoad() {
super .viewDidLoad()
setupSignUpControllerView()
addPages()
}
private func addPages(){
let namePage = NewOnboardingPage()
namePage.question = "What's your name?"
namePage.answerField = "What's your name?"
pages.append(namePage)
let birthDayPage = NewOnboardingPage()
birthDayPage.question = "When's your birthdate?"
birthDayPage.answerField = "When's your birthdate?"
pages.append(birthDayPage)
let userNamePage = NewOnboardingPage()
userNamePage.question = "Choose a user name."
userNamePage.answerField = "Choose a user name."
pages.append(userNamePage)
let passWordPage = NewOnboardingPage()
passWordPage.question = "Set a password"
passWordPage.answerField = "Set a password"
pages.append(passWordPage)
let emailAuthPage = NewOnboardingPage()
emailAuthPage.question = "What's your email?"
emailAuthPage.answerField = "What's your email?"
pages.append(emailAuthPage)
let phoneNumberPage = NewOnboardingPage()
phoneNumberPage.question = "What's your phone number?"
phoneNumberPage.answerField = "What's your phone number?"
pages.append(phoneNumberPage)
}
private func setupSignUpControllerView(){
collectionView?.backgroundColor = .white
collectionView?.register(OnboardingPageViewCell.self, forCellWithReuseIdentifier: cellId)
collectionView?.isPagingEnabled = true
collectionView?.isScrollEnabled = true
if let layout = collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 0
}
view.addSubview(nextButton)
nextButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 400).isActive = true
nextButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
nextButton.widthAnchor.constraint(equalToConstant: 250).isActive = true
nextButton.heightAnchor.constraint(equalToConstant: 60).isActive = true
}
private let nextButton: UIButton = {
let button = UIButton(type: .system)
button.backgroundColor = UIColor.RED
button.setTitle("next", for: .normal)
button.setTitleColor(UIColor.white, for: .normal)
button.titleLabel?.font = UIFont(name: "Avenir-Black", size: 25)
button.layer.cornerRadius = 30
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(turnNextPage), for: .touchUpInside)
return button
}()
#objc private func turnNextPage() {
let visibleItems: NSArray = collectionView?.indexPathsForVisibleItems as! NSArray
let currentItem: IndexPath = visibleItems.object(at: 0) as! IndexPath
let nextItem: IndexPath = IndexPath(item: currentItem.item + 1, section: 0)
if nextItem.row < pages.count {
collectionView?.scrollToItem(at: nextItem, at: .left, animated: true)
}
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return pages.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! OnboardingPageViewCell
cell.oboardingPage = pages[indexPath.item]
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: view.frame.height)
}
}
import UIKit
class CustomTextField: UITextField, UITextFieldDelegate {
convenience init() {
self.init(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
self.allowsEditingTextAttributes = false
self.autocorrectionType = .no
self.tintColor = UIColor.RED
self.translatesAutoresizingMaskIntoConstraints = false
}
override func willMove(toSuperview newSuperview: UIView?) {
addTarget(self, action: #selector(editingChanged), for: .editingChanged)
editingChanged(self)
}
#objc func editingChanged(_ textField: UITextField) {
guard let text = textField.text else { return }
textField.text = String(text.prefix(30))
}
override func selectionRects(for range: UITextRange) -> [Any] {
return []
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if action == #selector(paste(_:)) ||
action == #selector(cut(_:)) ||
action == #selector(copy(_:)) ||
action == #selector(select(_:)) ||
action == #selector(selectAll(_:)){
return false
}
return super.canPerformAction(action, withSender: sender)
}
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! OnboardingPageViewCell
cell.answerField.text = nil
cell.oboardingPage = pages[indexPath.item]
return cell
}
1- textFeild showing same data instead of placeholder bacause of cell dequeuing so you must hook these properties and clear their content in cellForRowAt
2- Instantiation is 3 not 6 aslo cell dequeuing
Solve:
Add two properties to your model NewOnboardingPage name them currentQuestion and currentAnswer and as the user inputs and scroll to next page save them in the modelarray that you should make global to be accessed indside cell and outside set these values to the textfeild and textView as you scroll in cellForRowAt

Resources