Show number of items selected in UICollectionView - ios

I am using UICollectionView to show the images. From that collectionView users can choose multiple images. Right now I am able to change view of selected images by overriding selected variable of UICollectionViewCell.
override var selected: Bool {
didSet {
if self.selected {
imageView.layer.borderWidth = 5.0
} else {
imageView.layer.borderWidth = 0.0
}
}
}
I put a UILabel at the top of UIImageView of cell to show the count. But I am not able to update the number of selected images. The count should be updated when user deSelect the image. How can i achieve this? It should be similar to Facebook image picker.I do not want to reload collectionView.

As you clarified in your comments, you want the number in the cell to show the order of the selected image.
Add a singleton class to hold an array of selected image IDs. The characteristic of a singleton is that its instance is accessible from everywhere in your app:
class ImageSelectionManager {
/// The singleton instance
static let instance = ImageSelectionManager()
/// This is the array that holds the order of selected image IDs
var selectedImageIds: [String] = []
}
In your UICollectionViewController implement the following delegate functions:
class YourViewController: UICollectionViewController {
override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
// Get the selected cell
let cell = collectionView.cellForItemAtIndexPath(indexPath) as! YourCell
// Get the selected image ID
let imageId = cell.imageId
// Add ID
ImageSelectionManager.instance.selectedImageIds.append(imageId)
// Notify cells that image array has changed
NSNotificationCenter.defaultCenter().postNotificationName(
"ImageSelectionChangedNotification",
object: self)
}
override func collectionView(collectionView: UICollectionView, didDeselectItemAtIndexPath indexPath: NSIndexPath) {
// Get the deselected cell
let cell = collectionView.cellForItemAtIndexPath(indexPath) as! YourCell
// Get the deselected image ID
let imageId = cell.imageId
// Remove ID
ImageSelectionManager.instance.selectedImageIds = ImageSelectionManager.instance.selectedImageIds.filter({ $0 != imageId })
// Notify cells that image array has changed
NSNotificationCenter.defaultCenter().postNotificationName(
"ImageSelectionChangedNotification",
object: self)
}
}
In your UICollectionViewCell add an observer to notifications and a function to update the counter label:
class YourCell: UICollectionViewCell {
/// The ID of your image that the cell is showing
var imageId: String!
/// The counter label
var counterLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Start listening to notifications
registerForNotifications()
}
override func prepareForReuse() {
super.prepareForReuse()
counterLabel.text = nil
}
deinit {
unregisterFromNotifications()
}
func registerForNotifications() {
// Make the cell listen to changes in the selected images array
NSNotificationCenter.defaultCenter().addObserver(
self,
selector: #selector(self.handleNotification(_:)),
name: "ImageSelectionChangedNotification",
object: nil)
}
func unregisterFromNotifications() {
// Stop listening to notifications
NSNotificationCenter.defaultCenter().removeObserver(self)
}
func handleNotification(notification: NSNotification) {
// If the notification is the one we are listening for
if notification.name == "ImageSelectionChangedNotification" {
// Execute on main thread because you are changing a UI element
dispatch_async(dispatch_get_main_queue()) {
// Get the position in the selected images array
if let position = ImageSelectionManager.instance.selectedImageIds.indexOf({ $0 == self.imageId }) {
// Image position was found so set the label text
self.counterLabel.text = String(position)
} else {
// Image was not found no remove the label text
self.counterLabel.text = nil
}
}
}
}
}
It works like this:
A cell is selected / deselected.
The cell's image ID is added / removed from the singleton's array.
A notification is sent to the cells.
Each cell receives a notification and checks the position of its image ID in the singleton's array.
If the image ID has a position in the array, the label text is updated with the position. If the image ID is not found that means the cell is not selected so the label text is removed.

First you will need a model class to store the selection status and the other attributes.
For example you are populating data for an Image in your collectionView.
class MyImage : NSObject {
var name : String?
var selectionNumber : Int? // Using for selection update on UI
/*other properties below*/
}
Now in your Collection View Cell make a MyImage property with hooked didSet
var image : MyImage? {
didSet {
imageView.image = UIImage(named:image.name)
if image.selectionNumber != nil {
imageView.layer.borderWidth = 5.0
lblCount.text = String(selectionNumber + 1)
}
else {
imageView.layer.borderWidth = 0.0
lblCount.text = ""
}
}
Now In your view controller class or the class where you are implementing collectionView's delegate methods.
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(kYour_Cell_ID, forIndexPath: indexPath) as! YourCustomCell
let myImage = self.dataSource[indexPath.row]
myImage.selectionNumber = self.selectedIndexes.contains(indexPath)
cell.image = myImage
return cell
}
Where your dataSource will be the array containing MyImage objects and create an array for storing selected indexPaths.
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath)
{
let idx = self.selectedIndexes.indexOf(image)
if (idx != nil) {
self.selectedIndexes.remove(indexPath.row)
}
else
{
self.selectedIndexes.append(indexPath.row)
}
self.collectionView.reloadItemsAtIndexPaths([self.selectedIndexes])
}

Related

UICollectionViewCell populate with wrong data

I have simple UICollectionViewCell that contains one product. My product have image(UIImageView), name(UILabel), description(UILabel), price(UILabel), rating(UIStackView) shown with 0 to 5 stars and buy button.
I fetch my products from JSON and save it to Array of products.
After app launch my UICollectionView populate with no problems. I use:
var product: Product? {
didSet {
populateProduct()
}
}
to populate data in Cell..
private func populateProduct() {
guard let product = product else { return }
let viewModel = ProductViewModel(product: product)
productImageView.loadImage(image: product.imageString)
productBrand.text = product.brand
productName.text = product.name
productDescription.text = product.description
productPrice.text = viewModel.price
if productRating.arrangedSubviews.count != 5 { // Check if there is rating already
for star in 1...5 {
let stars = product.score
if stars != 0 { // Doesn't show rating if there is none
var starImage = UIImageView(image: UIImage(named: "star-pink"))
if star > stars {
starImage = UIImageView(image: UIImage(named: "star-grey"))
}
starImage.contentMode = .scaleAspectFit
productRating.addArrangedSubview(starImage)
}
}
}
}
Problem is when I scroll collection view and some of my cells are dequed and reused. It doesn't happen every time, but often happens, that my cells swaps a data of products. Specifically the rating and the picture - so for example my first product have his own data, but rating of second product.
I think I fixed the image problem by this code in Cell:
override func prepareForReuse() {
super.prepareForReuse()
productImageView.image = nil
}
But I don't found how to reload arrangedSubviews in rating StackView or reload whole UIStackView same as just image in UIImageView. Or is there another solution to avoid it?
CellForItemAt
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! ProductCell
cell.product = products[indexPath.row]
return cell
}
And why cells swaps just ratingView?
Remove all subview from stackView
override func prepareForReuse() {
super.prepareForReuse()
productImageView.image = nil
_ = productRating.arrangedSubviews.map { $0.removeFromSuperview() }
// or
productRating.arrangedSubviews.forEach { subview in
subview.removeFromSuperview()
}
}

How to access off-screen but existing cells in UICollectionView in Swift?

The title might be a little hard to understand, but this situation might help you with it.
I'm writing a multiple image picker. Say the limit is 3 pictures, after the user has selected 3, all other images will have alpha = 0.3 to indicate that this image is not selectable. (Scroll all the way down to see a demo)
First of all, this is the code I have:
PickerPhotoCell (a custom collection view cell):
class PickerPhotoCell: UICollectionViewCell {
#IBOutlet weak var imageView: UIImageView!
var selectable: Bool {
didSet {
self.alpha = selectable ? 1 : 0.3
}
}
}
PhotoPickerViewController:
class PhotoPickerViewController: UICollectionViewController {
...
var photos: [PHAsset]() // Holds all photo assets
var selected: [PHAsset]() // Holds all selected photos
var limit: Int = 3
override func viewDidLoad() {
super.viewDidLoad()
// Suppose I have a func that grabs all photos from photo library
photos = grabAllPhotos()
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell ...
let asset = photos[indexPath.row]
...
// An image is selectable if:
// 1. It's already selected, then user can deselect it, or
// 2. Number of selected images are < limit
cell.selectable = cell.isSelected || selected.count < limit
return cell
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath) as! PickerPhotoCell
if cell.isSelected {
// Remove the corresponding PHAsset in 'selected' array
} else {
// Append the corresponding PhAsset to 'selected' array
}
// Since an image is selected/deselected, I need to update
// which images are selectable/unselectable now
for visibleCell in collectionView.visibleCells {
let visiblePhoto = visibleCell as! PickerPhotoCell
visiblePhoto.selectable = visiblePhoto.isSelected || selected.count < limit
}
}
}
This works almost perfectly, except for one thing, look at the GIF:
The problem is
After I've selected 3 photos, all other visible photos have alpha = 0.3, but when I scroll down a little more, there are some photos that still have alpha = 1. I know why this is happening - Because they were off-screen, calling collectionView.visibleCells wouldn't affect them & unlike other non-existing cells, they did exist even though they were off-screen. So I wonder how I could access them and therefore make them unselectable?
The problem is that you are trying to store your state in the cell itself, by doing this: if cell.isSelected.... There are no off screen cells in the collection view, it reuses cells all the time, and you should actually reset cell's state in prepareForReuse method. Which means you need to store your data outside of the UICollectionViewCell.
What you can do is store selected IndexPath in your view controller's property, and use that data to mark your cells selected or not.
pseudocode:
class MyViewController {
var selectedIndexes = [IndexPath]()
func cellForItem(indexPath) {
cell.isSelected = selectedIndexes.contains(indexPath)
}
func didSelectCell(indexPath) {
if selectedIndexes.contains(indexPath) {
selectedIndexes.remove(indexPath)
} else if selectedIndexes.count < limiit {
selectedIndexes.append(indexPath)
}
}
}

How to access custom cell textfield values Swift 3.0

I have created a custom tableViewCell class for a prototype cells which is holding a text field. Inside ThirteenthViewController, I would like to reference the tableViewCell class so that I can access its doorTextField property in order to assign to it data retrieved from UserDefaults. How can I do this?
class ThirteenthViewController: UIViewController,UITableViewDelegate,UITableViewDataSource,UITextFieldDelegate {
var options = [
Item(name:"Doorman",selected: false),
Item(name:"Lockbox",selected: false),
Item(name:"Hidden-Key",selected: false),
Item(name:"Other",selected: false)
]
let noteCell:NotesFieldUITableViewCell! = nil
#IBAction func nextButton(_ sender: Any) {
//save the value of textfield when button is touched
UserDefaults.standard.set(noteCell.doorTextField.text, forKey: textKey)
//if doorTextField is not empty assign value to FullData
guard let text = noteCell.doorTextField.text, text.isEmpty else {
FullData.finalEntryInstructions = noteCell.doorTextField.text!
return
}
FullData.finalEntryInstructions = "No"
}
override func viewDidLoad() {
let index:IndexPath = IndexPath(row:4,section:0)
let cell = tableView.cellForRow(at: index) as! NotesFieldUITableViewCell
self.tableView.delegate = self
self.tableView.dataSource = self
cell.doorTextField.delegate = self
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return options.count
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
// configure the cell
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
-> UITableViewCell {
if indexPath.row < 3 {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")!
cell.textLabel?.text = options[indexPath.row].name
return cell
} else {
let othercell = tableView.dequeueReusableCell(withIdentifier: "textField") as! NotesFieldUITableViewCell
othercell.doorTextField.placeholder = "some text"
return othercell
}
}
}//end of class
class NotesFieldUITableViewCell: UITableViewCell {
#IBOutlet weak var doorTextField: UITextField!
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
}
In order to access the UITextField in your cell, you need to know which row of the UITableView contains your cell. In your case, I believe the row is always the 4th one. So, you can create an IndexPath for the row and then, you can simply do something like this:
let ndx = IndexPath(row:3, section: 0)
let cell = table.cellForRow(at:ndx) as! NotesFieldUITableViewCell
let txt = cell.doorTextField.text
The above might not be totally syntactically correct since I didn't check for syntax, but I'm sure you can take it from there, right?
However, do note that in order for the above to work, the last row (row 4) has to be always visible. If you try to fetch rows which are not visible, you do run into issues with accessing them since UITableView reuses cells and instantiates cells for the visible rows of data.
Also, if you simply want to get the text that the user types and text input ends when they tap "Enter", you can always simply bypass accessing the table row at all and add a UITextFieldDelegate to your custom cell to send a notification out with the entered text so that you can listen for the notification and take some action.
But, as I mentioned above, this all depends on how you have things set up and what you are trying to achieve :)
Update:
Based on further information, it appears as if you want to do something with the text value when the nextButton method is called. If so, the following should (theoretically) do what you want:
#IBAction func nextButton(_ sender: Any) {
// Get the cell
let ndx = IndexPath(row:4, section: 0)
let cell = table.cellForRow(at:ndx) as! NotesFieldUITableViewCell
//save the value of textfield when button is touched
UserDefaults.standard.set(cell.doorTextField.text, forKey: textKey)
//if doorTextField is not empty assign value to FullData
guard let text = cell.doorTextField.text, text.isEmpty else {
FullData.finalEntryInstructions = cell.doorTextField.text!
return
}
FullData.finalEntryInstructions = "No"
}
You can create a tag for the doorTextField (for instance 111)
Now you can retrieve the value.
#IBAction func nextButton(_ sender: Any) {
//save the value of textfield when button is touched
guard let textField = self.tableViewview.viewWithTag(111) as! UITextField? else { return }
prit(textField.text)
.....
}

Swift: Update CollectionView Cell Image

Im making a game that has a collectionView character select at the bottom of the GameScene. Many of the characters are locked, so i display a "Locked" image as the collection view cell image. However, when the user unlocks the character and they load the GameScene again(move to gameOver scene, restart the game back to present GameScene), i need to update the collectionView cells images and swap out the "Locked" image with the regular image. Ive tried so many different things but it isn't working. It won't swap out/reload the new images. I know its just something small I'm missing but heres all my code associated with the collection view.
class CharactersCollectionViewCell: UICollectionViewCell {
//MARK : Public API
var characters : NACharacters!{
didSet{
updateUI()
}
}
//MARK : Private
#IBOutlet weak var featuredImageView: UIImageView!
private func updateUI(){
self.layer.borderColor = UIColor.whiteColor().CGColor
self.layer.borderWidth = 2.5
self.layer.masksToBounds = true
self.layer.cornerRadius = CGFloat(roundf(Float(self.frame.size.width/2.0)))
featuredImageView.image = characters.featuredImage
}
}
class NACharacters {
var featuredImage : UIImage!
static var characterLineUp = [NACharacters]()
init(featuredImage: UIImage){
self.featuredImage = featuredImage
}
static func createCharacters() -> [NACharacters]{
print("Inside create character")
characterLineUp.insert(NACharacters(featuredImage: UIImage(named: "DiploSquadCollectionView")!), atIndex: 0)
characterLineUp.insert(NACharacters(featuredImage: UIImage(named: "LockedNBAPlayersCollectionView")!), atIndex: 1)
if(NSUserDefaults().unlockNBAPlayers == true){
characterLineUp[1].featuredImage = UIImage(named: "NBAPlayersCollectionView")!
}
characterLineUp.insert(NACharacters(featuredImage: UIImage(named: "LockedLeanSquadCollectionView")!), atIndex: 2)
if(NSUserDefaults().totalDabs >= 10000){
print("setting up lean to be unlocked")
characterLineUp[2].featuredImage = UIImage(named: "LeanSquadCollectionView")!
}
return characterLineUp
}
}
//Inside Game View Controller
#IBOutlet var collectionView: UICollectionView!
//MARK : Data Source
internal var characters = NACharacters.createCharacters()
extension GameViewController : UICollectionViewDataSource, UICollectionViewDelegate{
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return characters.count
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(Storyboard.CellIdentifier, forIndexPath: indexPath) as! CharactersCollectionViewCell
cell.characters = self.characters[indexPath.item]
return cell
}
//Inside Game Scene
var collectionView: UICollectionView? {
return self.view?.subviews.filter{ $0 is UICollectionView }.first as? UICollectionView
}
override func didMoveToView(view: SKView) {
//INSIDE HERE I NEED TO UPDATE THE COLLECTION VIEW CELL IMAGES
}
I know this looks like a lot of code but most of it is pretty standard. I show you all of this so that we can pin point exactly whats wrong or what needs to be done to get this to work. This is all of my code associated with the collectionView.
Have you try this in same place ::
imageView.image = nil
then set the image you want to update as ..
imageView.image = UIImage(named : "YourImage")
When you need to 'refresh' a Collection View, tell it to reload its data, then all the images should update as needed.
collectionView.reloadData()

Getting UITableViewCell from UITapGestureRecognizer

I have a UITableView with multiple sections and in my cellForRowAtIndexPath method I add a UITapGestureRecognizer to the cell's imageView (each cell has a small image). I have been successful in accessing that image (in order to change it) by using:
var imageView : UIImageView! = sender.view! as UIImageView
What I need to do now, however, is access the cell's data, which I believe means I need to be able to access the cell in order to get the section and row number. Can anyone advise on how to do this? The idea is that I am changing the image to an unchecked checkbox if the task is done, but then in my data I need to actually mark that task as done.
In your function that handles the tap gesture recognizer, use tableView's indexPathForRowAtPoint function to obtain and optional index path of your touch event like so:
func handleTap(sender: UITapGestureRecognizer) {
let touch = sender.locationInView(tableView)
if let indexPath = tableView.indexPathForRowAtPoint(touch) {
// Access the image or the cell at this index path
}
}
From here, you can call cellForRowAtIndexPath to get the cell or any of its content, as you now have the index path of the tapped cell.
You can get the position of the image tapped in order to find the indexPath and from there find the cell that has that image:
var position: CGPoint = sender.locationInView(self.tableView)
var indexPath: NSIndexPath = self.tableView.indexPathForRowAtPoint(position)!
var cell = self.tableView(tableView, cellForRowAtIndexPath: indexPath)
You're missing that you can use the Delegate design pattern here.
Create a protocol that tells delegates the state changes in your checkbox (imageView):
enum CheckboxState: Int {
case .Checked = 1
case .Unchecked = 0
}
protocol MyTableViewCellDelegate {
func tableViewCell(tableViewCell: MyTableViewCell, didChangeCheckboxToState state: CheckboxState)
}
And assuming you have a UITableViewCell subclass:
class MyTableViewCell: UITableViewCell {
var delegate: MyTableViewCellDelegate?
func viewDidLoad() {
// Other setup here...
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: "didTapImage:")
imageView.addGestureRecognizer(tapGestureRecognizer)
imageView.userInteractionEnabled = true
}
func didTapImage(sender: AnyObject) {
// Set checked/unchecked state here
var newState: CheckboxState = .Checked // depends on whether the image shows checked or unchecked
// You pass in self as the tableViewCell so we can access it in the delegate method
delegate?.tableViewCell(self, didChangeCheckboxToState: newState)
}
}
And in your UITableViewController subclass:
class MyTableViewController: UITableViewController, MyTableViewCellDelegate {
// Other methods here (viewDidLoad, viewDidAppear, etc)...
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) {
var cell = tableView.dequeueReusableCellWithIdentifier("YourIdentifier", forIndexPath: indexPath) as MyTableViewCell
// Other cell setup here...
// Assign this view controller as our MyTableViewCellDelegate
cell.delegate = self
return cell
}
override func tableViewCell(tableViewCell: MyTableViewCell, didChangeCheckboxToState state: CheckboxState) {
// You can now access your table view cell here as tableViewCell
}
}
Hope this helps!
Get tableView's cell location on a specific cell. call a function which holds the gesture of cell's Objects Like UIImage or etc.
let location = gesture.location(in: tableView)
let indexPath = tableView.indexPathForRow(at: location)
Print(indexPath.row)
Print(indexPath.section)

Resources