I have a UITableView where each cell get's its data from my model class, which is an array in my TableViewController class. The model for each cell is set in cellForRowAtIndexPath. My model has a "isFavorited" property. My custom cell class has a UIButton with an image of a star.
If model.isFavorited == false, the UIButton image is a gray star.
If model.sFavorited == true, the UIButton image should change to a yellow star.
So far, I have tried to approach this like so:
private let normal = UIImage(named: "star")
private let selection = UIImage(named: "star highlighted")
class CustomCell: UITableViewCell {
var model: Model? {
didSet{
favoriteButton.selected = (model?.isFavorite)!
}
}
#IBOutlet var favoriteButton: UIButton!
override func awakeFromNib() {
super.awakeFromNib()
favoriteButton.adjustsImageWhenHighlighted = true
favoriteButton.setImage(normal, forState: .Normal)
favoriteButton.setImage(selection, forState: .Highlighted)
favoriteButton.setImage(selection, forState: .Selected)
}
#IBAction func favoriteTapped(sender: AnyObject) {
//UPDATE THE MODEL
model?.isFavorite = !(model?.isFavorite)!
}
}
This code successfully changes a star from gray to yellow and vice-versa thanks to didSet being called to update the favoriteButton.selected boolean with the value from (model?.isFavorite)!.
HOWEVER, as I scroll far enough through my tableview, and try to come back to the specific cell I favorited, the image will reappear as unfavorited (gray star instead of yellow star).
If my model is being updated when I tap the button, and the UIButton's image is adjusted during didSet as a result of this change, why does it revert back to the defaulted state when scrolling back to this cell??
EDIT:
My code in cellForRowAtIndexPath
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: CustomCell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath) as! CustomCell
cell.model = modelArray[indexPath.row]
return cell
}
Basically you could subclass the UIButton with custom images set based on the tap. I had the same issue and solved it using below:
class FavioriteButton: UIButton {
//Images
let normalImage = UIImage(named: "star")! as UIImage
let selectedImage = UIImage(named: "star_highlighted")! as UIImage
//Bool property
var isSelected: Bool = false {
didSet {
if isSelected == true {
self.setImage(normalImage, forState: .Normal)
}else{
self.setImage(selectedImage, forState: .Normal)
}
}
}
override func awakeFromNib() {
super.awakeFromNib()
self.addTarget(self, action: #selector(buttonClicked(_:)), forControlEvents: .TouchUpInside)
self.isSelected = false
}
func buttonClicked(sender: UIButton) {
if sender == self {
if isSelected == true {
isSelected = false
}else{
isSelected = true
}
}
}
}
Hope it helps you.
Related
Issue: When I scroll the tableView, my configureCell() method appends too many views to the stackView inside the cell.
When the cell is first displayed, and I press the UIButton, the stack is unhidden and first shows the right amount of views, after scrolling, the amount is duplicated.
prepareForReuse() is empty right now. I want to keep the stackView unHidden after scrolling.
I set the heightAnchor for the UIView as it is programmatic layout.
Expected Outcome: User taps on the button, the cell stackView is unhidden and the cell expands to chow the uiviews related to the cell.
When I call it in the cellForRowAt, nothing happens. Because im not sure how to modify the method for IndexPath.row.
protocol DataDelegate: AnyObject {
func displayDataFor(_ cell: TableViewCell)
}
class TableViewCell: UITableViewCell {
#IBOutlet weak var stackView: UIStackView! {
didSet {
stackView.isHidden = true
}
}
#IBOutlet button: UIButton!
var model = Model()
var detailBool: Bool = false
#IBAction func action(_ sender: Any) {
self.claimsDelegate?.displayClaimsFor(self)
detailBool.toggle()
}
func configureCellFrom(model: Model) {
if let addedData = model.addedData {
if addedData.count > 1 {
for data in addedData {
let dataView = DataView()
dataView.heightAnchor.constraint(equalToConstant: 65).isActive = true
dataView.dataNumber.text = data.authDataNumber
self.stackView.addArrangedSubview(dataView)
}
}
}
}
}
How would I call this in cellForRowAt, so its only created the correct amount of uiviews and not constantly adding?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: TableViewCell.identifier, for: indexPath) as? TableViewCell else {
return UITableViewCell()
}
cell.dataDelegate = self
cell.configureCellFrom(model: model[indexPath.row])
//if let addedData = model.addedData {
//if addedData.count > 1 {
//for data in addedData {
//let dataView = DataView()
//dataView.heightAnchor.constraint(equalToConstant: 65).isActive = true
//dataView.dataNumber.text = data.authDataNumber
//self.stackView.addArrangedSubview(dataView)
// }}}
cell.layoutIfNeeded()
cell.selectionStyle = .none
return cell
}
extension ViewController: DataDelegate {
func displayDataFor(_ cell: TableViewCell) {
if tableView.indexPath(for: cell) != nil {
switch cell.detailBool {
case true:
UIView.performWithoutAnimation {
tableView.beginUpdates()
cell.detailArrow.transform = CGAffineTransform(rotationAngle:.pi)
cell.stackView.isHidden = false
tableView.endUpdates()
}
case false:
UIView.performWithoutAnimation {
tableView.beginUpdates()
cell.stackView.isHidden = true
cell.detailArrow.transform = CGAffineTransform.identity
tableView.endUpdates()
}
}
}
}
}
If I understand correctly, you would like to create an expandable TableView? If yes you can do it a lot of different ways, but you have to change your approach totally. Please refer LBTA approach:
LBTA video
My favourite the Struct approach, where you create a struct and you can save the complication with the 2D array:
Struct stackoverflow link
In my Table View Controller I'm requesting data from my backend. Then adding the data to an array with a view model for each row from the request.
let getNotifications = GETNotificationsByUserID(user_id: user_id)
getNotifications.getNotifications { notifications in
self.notifications = notifications.map { notification in
let ret = NotificationViewModel()
ret.mainNotification = notification
return ret
}
class NotificationViewModel {
var mainNotification: Notifications? {}
}
struct Notifications: Codable {
var supporter_picture:String?
}
In the same table view controller, I'm then adding each item from the array to a variable in my table view cell.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath as IndexPath) as! NotificationCell
cell.user_image.isUserInteractionEnabled = true
let item = self.notifications[indexPath.item]
cell.viewModel = item
return cell
}
Downloading the image from the data from my table view cell variable and setting my UIImageView image.
UITableViewCell
class NotificationCell: UITableViewCell {
var viewModel: NotificationViewModel? {
didSet {
if let item = viewModel {
self.username.text = item.mainNotification?.supporter_username
item.supporterImageDownloader = DownloadImage()
item.supporterImageDownloader?.imageDidSet = { [weak self] image in
self?.user_image.image = image
}
if let picture = item.mainNotification?.supporter_picture {
item.supporterImageDownloader?.downloadImage(urlString: picture)
} else {
self.user_image.image = UIImage(named: "profile-placeholder-user")
}
}
}
}
}
The UIImageView is created from a lazy closure variable. The lazy closure variable also holds a UITapGestureRecognizer.
UITableViewCell
lazy var user_image: UIImageView = {
let image = UIImageView()
let gesture = UITapGestureRecognizer()
gesture.addTarget(self, action: #selector(userImageClicked(_:)))
gesture.numberOfTapsRequired = 1
gesture.numberOfTouchesRequired = 1
image.addGestureRecognizer(gesture)
image.isUserInteractionEnabled = true
return image
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.isUserInteractionEnabled = true
addSubview(user_image)
user_imageContraints()
}
func user_imageContraints() {
user_image.translatesAutoresizingMaskIntoConstraints = false
user_image.topAnchor.constraint(equalTo:topAnchor, constant: 8).isActive = true
user_image.leadingAnchor.constraint(equalTo:leadingAnchor, constant: 8).isActive = true
user_image.heightAnchor.constraint(equalToConstant: 40).isActive = true
user_image.widthAnchor.constraint(equalToConstant: 40).isActive = true
}
#objc func userImageClicked(_ sender: UITapGestureRecognizer) {
print("image clicked")
}
For some reason when my UIImageView is clicked it doesn't do anything.
The problem is this line:
addSubview(user_image)
Change it to:
contentView.addSubview(user_image)
You must never add any views directly to a cell — only to its contentView.
I'm trying to add some buttons or labels to one of UICollectionViewCell, but it seems I'm not be able to activate UIActivityViewController when the share button tapped.
Here are my codes:
class VerticalCollectionViewCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
containerView.addSubview(shareButton)
shareButton.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -16).isActive = true
shareButton.rightAnchor.constraint(equalTo: favoriteButton.leftAnchor, constant: -16).isActive = true
shareButton.widthAnchor.constraint(equalToConstant: 24).isActive = true
shareButton.heightAnchor.constraint(equalToConstant: 24).isActive = true
shareButton.addTarget(self, action: #selector(handleShareButton), for: .touchUpInside)
}
#objc func handleShareButton(sender: UIButton) {
let shareText = NSLocalizedString("I found something interesting you may want to take a look at.", comment: "share")
let shareImage = #imageLiteral(resourceName: "Bear")
let activityViewController : UIActivityViewController = UIActivityViewController(activityItems: [shareText, shareImage as Any], applicationActivities: nil)
activityViewController.popoverPresentationController?.sourceView = self.view
self.present(activityViewController, animated: true, completion: nil)
}
}
The errors are:
Value of type 'VerticalCollectionViewCell' has no member 'view'
and
Value of type 'VerticalCollectionViewCell' has no member 'present'.
I know presentViewController is a UIViewController method, UITableViewCell does not has a method called presentViewController, and UITableViewCell should never handle any business logic. It should be implemented in a view controller.
But I have no idea how to fix this.
you call do in this way
class Cell: UITableViewCell {
#IBOutlet let shareButton: UIButton!
}
class VC: UIViewController, UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as? Cell {
cell?.shareButton.addTarget(self, action: #selector(VC.shareButtonPressed(_:)), for: .touchUpInside)
}
}
func shareButtonPressed(_ button: UIButton) {
//code here
}
}
What you can do, is define a protocol of the cell, which your tableview can conform to. When the share button on a collectionView is tapped, call this delegate method, passing in relevant information like the cell tag or the model for that collectionView cell. Using that information you can then present the ActivityController from your viewController.
I am creating a Check List app and trying to update button event which is set images itself after clicked.
Here is my code:
protocol CustomeCellDelegate {
func cell(cell: UITableViewCell, updateTheButton button: UIButton)
}
class Cells: UITableViewCell, UITextFieldDelegate {
#IBOutlet weak var checkBox: UIButton!
var buttonPressed = true
#IBAction func checkBoxAction(_ sender: UIButton) {
if buttonPressed {
sender.setImage(UIImage(named: "checkBoxB"), for: UIControlState.normal)
buttonPressed = false
} else {
sender.setImage(UIImage(named:""), for: UIControlState.normal)
buttonPressed = true
}
}
#objc func recordButtonImage(_ button: UIButton) {
cellDelegate?.cell(cell: self, updateTheButton: button) // cell notify the button that touched.
}
override func awakeFromNib() {
checkBox.addTarget(self, action: #selector(recordButtonImage(_:)), for: UIControlEvents.touchUpInside)
}
}
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate, CustomeCellDelegate {
#IBOutlet weak var tableView: UITableView!
var myImages: [UIImage?]!
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.delegate = self
self.tableView.dataSource = self
myImages = Array(repeatElement(nil, count: 50))
let nib = UINib(nibName: "Cells", bundle: nil)
tableView.register(nib, forCellReuseIdentifier: "Cells")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 50
}
internal func tableView(_ tableView:UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "theCells") as? Cells
cell?.theTextField.delegate = self
cell?.cellDelegate = self
cell?.checkBox.setImage(myImages[indexPath.row], for: UIControlState.normal)
return cell!
}
// This function from protocol and it helps to update button images.
func cell(cell: UITableViewCell, updateTheButton button: UIButton) {
print("Delegate method Update Button Images.")
if let indexPath = tableView.indexPath(for: cell), let buttonImage = button.image(for: UIControlState.normal) {
myImages[indexPath.row] = buttonImage
}
}
I would like to update both events of the button that set image after checked and unchecked. Therefore, my table view can dequeue Reusable Cell and have those events updated.
But the result is just one event of button which is checked updated but not the unchecked one. The checked button still repeats whatever I do to uncheck it.
I think you don't need recordButtonImage method, you should call the delegate method from the button tapped action i.e. checkBoxAction, just like below
class Cells: UITableViewCell, UITextFieldDelegate {
#IBOutlet weak var checkBox: UIButton!
var buttonPressed = true
#IBAction func checkBoxAction(_ sender: UIButton) {
if buttonPressed {
sender.setImage(UIImage(named: "checkBoxB"), for: UIControlState.normal)
buttonPressed = false
} else {
sender.setImage(UIImage(named:""), for: UIControlState.normal)
buttonPressed = true
}
// cell notify the button that touched.
cellDelegate?.cell(cell: self, updateTheButton: button)
}
}
Also your data source method, doesn't tell the cell whether the button was pressed or not. It just sets the image but doesn't set the variable buttonPressed
internal func tableView(_ tableView:UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "theCells") as? Cells
cell?.theTextField.delegate = self
cell?.cellDelegate = self
cell?.checkBox.setImage(myImages[indexPath.row], for: UIControlState.normal)
return cell!
}
The best way to handle UIButton is using its own state.
You need to hold current state value in one array. So you can keep the state in cellForRow and other things. By default set all state to false.
In CellForRow, just put below code:
YOUR_BUTTON.isSelected = aryState[indexPath.row] as! Bool
Set image for your button
YOUR_BUTTON.setImage(UIImage(named: NORMAL_IMAGE), for: .normal)
YOUR_BUTTON.setImage(UIImage(named: SELECTED_IMAGE), for: .selected)
Just change button state when it clicked:
#IBAction func checkBoxAction(_ sender: UIButton) {
let button = sender
button.isSelected = !button.isSelected
}
I have a UITableView in a UIViewController. I have a button at the bottom of this view controller. All the table cells contain a checkbox, which is a subclass of UIButton and a UILabel. When the button is pressed, I want to fetch the text of labels for all the 'checked' cells. Now I can't use cellForRowAtIndexPath because that returns a nil value for cells that are not visible. I therefore want to index my datasource. But the problem is, since UIButton does not have a UIButtonDelegate like UITextfield, I don't know how to capture the button clicks and store the indexPath rows for the cells for which the checkbox was checked. Please help! Thanks a lot!
This is my checkbox code, for reference
class CheckBox: UIButton {
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect) {
// Drawing code
}
*/
// images
var checkedImage = UIImage(named: "check")
var unCheckedImage = UIImage(named: "uncheck")
// bool property
var isChecked: Bool = false {
didSet {
if isChecked == true {
self.setImage(checkedImage, forState: .Normal)
} else {
self.setImage(unCheckedImage, forState: .Normal)
}
}
}
override func awakeFromNib() {
self.addTarget(self, action: "buttonClicked:", forControlEvents: UIControlEvents.TouchUpInside)
self.isChecked = false
}
func buttonClicked(sender: UIButton) {
if (sender == self) {
if isChecked == true {
isChecked = false
} else {
isChecked = true
}
}
}
}
You can implement a tableviewcell. In that you have to create all elements in you cell. And in that cell class you have to maintain this variables
var indexofRow // Set this at the time of setting cell for UITableView .
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) - > UITableViewCell {
let cell: yourFeedCell = tableView.dequeueReusableCellWithIdentifier("yourTableCell") as!yourFeedCell
cell.indexofRow = indexPath.row;
}
/*Code in you cell class*/
//Maintain one dictonary Globally
var listOfCheckedCells = [Int: Bool]()
// bool property
var isChecked: Bool = false {
didSet {
if isChecked == true {
self.setImage(checkedImage, forState: .Normal)
listOfCheckedCells[self.indexofRow] = true
} else {
self.setImage(unCheckedImage, forState: .Normal)
listOfCheckedCells[self.indexofRow] = false
}
}
}
Hope this will Help
You have already subclassed from UIButton. AFAICS, you need a method call in order to be aware when the button is pressed. There is a method on UIControl (which is also applicable to UIButton):
func sendAction(_ action: Selector,
to target: AnyObject?,
forEvent event: UIEvent?)
UIControl implements this method to forward an action message to the
singleton UIApplication object (in its
sendAction:to:fromSender:forEvent: method) for dispatching it to the
target or, if there is no specified target, to the first object in the
responder chain that is willing to handle it. Subclasses may override
this method to observe or modify action-forwarding behavior. The
implementation of sendActionsForControlEvents: might call this method
repeatedly, once for each specified control event.
You can override the method, call the super's implementation and let your observers/delegates know that the associated action is about to be performed.
Table view cells are populated only when they're visible so if you wanna save a value of a cell and keep it around even when its not visible, create a dictionary with the index path as key and the content of the cell as value. Then just update the dictionary when the check box is checked and unchecked. Finally when 'the button at the bottom of the view' is clicked fetch your data from the dictionary. here is somewhat a sample
var textOfLabels:[NSIndexPath:String]?
func buttonClicked(sender: UIButton) {
let view = sender.superview!
let cell = view.superview as! UITableViewCell //or your custom cell name
let indexPath = itemTable.indexPathForCell(cell)
if (sender == self) {
if isChecked == true {
isChecked = false
textOfLabels?.updateValue(theLableText, forKey: indexPath)
} else {
isChecked = true
textOfLabels?.removeValueForKey(indexPath)
}
}
}