UILongPressGestureRecognizer does not get deallocated from CollectionViewCell instance - ios

Ok so I'm out of ideas
Here is my custom UICollectionViewCell class:
class baseCViewCell: UICollectionViewCell {
var mainCV : AnyObject!
var indexPath : NSIndexPath!
class func getIdentifier() -> String {
return NSStringFromClass(self).componentsSeparatedByString(".").last!
}
var editGesture : UILongPressGestureRecognizer!
func initialize(parent:AnyObject, indexPath:NSIndexPath) {
mainCV = parent as baseCView
self.indexPath = indexPath
editGesture = UILongPressGestureRecognizer(target: self, action: Selector("edit:"))
editGesture.minimumPressDuration = 1.0
editGesture.allowableMovement = 100.0
self.addGestureRecognizer(editGesture)
}
func edit(gesture: UILongPressGestureRecognizer) {
if (gesture == editGesture) {
if (gesture.state == UIGestureRecognizerState.Began) {
testinglog("Edit pressed on (\(indexPath.row) in \(indexPath.section))")
}
}
}
deinit {
self.removeGestureRecognizer(editGesture)
}
}
My question is .. Why do the gesture objects not dealloc after the cell becomes not-visible (scrolling).
I have tried all I know to force them to dealloc .. with no success
This is how I use the cell:
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 100;
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
var cell = collectionView.dequeueReusableCellWithReuseIdentifier("CViewCell", forIndexPath: indexPath) as CViewCell
cell.initialize(collectionView, indexPath: indexPath)
return cell
}
Again .. If I scroll up and down I can see in Instruments the memory count goes up .. and up .. and only up .. being equivalent to a leak .. only not detected.
I commented the part where I add the gestureRecognizer and all is ok (just mentioned it so I don't have to answer if I am sure if the problem is there).

Collection views reuse cells. As views move offscreen, they are removed from view and placed in a reuse queue instead of being deleted. So, the deinitializer of cell probably will not be called until the collection view is deallocated.
What you are current doing now is adding a new gesture recognizer to the cell every time you dequeue it, this increases memory usage. You should create and initialize the gesture recogniser in the initializer of the cell.
By the way, your cell holds a strong reference back to the collection view, which creates a strong reference cycle, that's not a good thing.

Related

Prevent `didSelect…` for part of a UICollectionViewCell

Summary
Can a UICollectionViewCell subclass prevent didSelectItemAt: indexPath being sent to the UICollectionViewDelegate for taps on some of its sub views, but to proceed as normal for others?
Use case
I have a UICollectionViewCell that represents a summary of an article. For most articles, when they are tapped, we navigate through to show the article.
However, some article summaries show an inline video preview. When the video preview is tapped, we should not navigate through, but when the other areas of the article summary are tapped (the headline), we should navigate through.
I'd like the article summary cell to be able to decide whether a tap on it should be considered as a selection.
You have to add tapGestureRecogniser on those subviews of cell on which you don't want delegate to get called.
tapGestureRecogniser selector method will get called when you will tap on those subview and gesture will not get passed to delegate.
What you need to do is to attach UITapGestureRecognizer to your view and monitor taps from it:
class MyViewController: UIViewController, UICollectionViewDataSource, MyCellDelegate {
var dataSource: [Article] = []
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataSource.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ArticleCell", for: indexPath) as! MyCell
cell.article = dataSource[indexPath.row]
cell.delegate = self
return cell
}
func articleDidTap(_ article: Article) {
// do what you need
}
}
// your data model
struct Article {}
protocol MyCellDelegate: class {
func articleDidTap(_ article: Article)
}
class MyCell: UICollectionViewCell {
var article: Article! {
didSet {
// update your views here
}
}
weak var delegate: MyCellDelegate?
override func awakeFromNib() {
super.awakeFromNib()
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(MyCell.tap)))
}
#objc func tap() {
delegate?.articleDidTap(article)
}
}
This should work since your video view should overlap root view and prevent receiving taps from gesture recognizer.

Table view cell checkmark changes position when scrolling

I am new to Swift. I have created a simple list in a tableview. When the user long presses on a row, that row will get checked. It's working perfectly fine. But when I scroll down, check mark changes its position. I also tried to store position in NSMutableSet. But still it's not working. Maybe I am doing something wrong.
This is my code:
This method gets called on long press.
func longpress(gestureRecognizer: UIGestureRecognizer)
{
let longpress = gestureRecognizer as! UILongPressGestureRecognizer
let state = longpress.state
let locationInview = longpress.location(in: tableview1)
var indexpath=tableview1.indexPathForRow(at: locationInview)
if(gestureRecognizer.state == UIGestureRecognizerState.began)
{
if(tableview1.cellForRow(at: indexpath!)?.accessoryType ==
UITableViewCellAccessoryType.checkmark)
{
tableview1.cellForRow(at: indexpath!)?.accessoryType =
UITableViewCellAccessoryType.none
}
else{
tableview1.cellForRow(at: indexpath!)?.accessoryType =
UITableViewCellAccessoryType.checkmark
}
}
}
The problem is that cells are reused and when you update a checkmark, you're updating a cell, but not updating your model. So when a cell scrolls out of view and the cell is reused, your cellForRowAt is obviously not resetting the checkmark for the new row of the table.
Likewise, if you scroll the cell back into view, cellForRowAt has no way of knowing whether the cell should be checked or not. You have to
when you detect your gesture on the cell, you have to update your model to know that this row's cell should have a check; and
your cellForRowAt has to look at this property when configuring the cell.
So, first make sure your model has some value to indicate whether it is checked/selected or not. In this example, I'll use "Item", but you'd use a more meaningful type name:
struct Item {
let name: String
var checked: Bool
}
Then your view controller can populate cells appropriately in cellForRowAt:
class ViewController: UITableViewController {
var items: [Item]!
override func viewDidLoad() {
super.viewDidLoad()
addItems()
}
/// Create a lot of sample data so I have enough for a scrolling view
private func addItems() {
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
items = (0 ..< 1000).map { Item(name: formatter.string(from: NSNumber(value: $0))!, checked: false) }
}
}
extension ViewController {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ItemCell", for: indexPath) as! ItemCell
cell.delegate = self
cell.textLabel?.text = items[indexPath.row].name
cell.accessoryType = items[indexPath.row].checked ? .checkmark : .none
return cell
}
}
Now, I generally let the cell handle stuff like recognizing gestures and inform the view controller accordingly. So create a UITableViewCell subclass, and specify this as the base class in the cell prototype on the storyboard. But the cell needs some protocol to inform the view controller that a long press took place:
protocol ItemCellDelegate: class {
func didLongPressCell(_ cell: UITableViewCell)
}
And the table view controller can handle this delegate method, toggling its model and reloading the cell accordingly:
extension ViewController: ItemCellDelegate {
func didLongPressCell(_ cell: UITableViewCell) {
guard let indexPath = tableView.indexPath(for: cell) else { return }
items[indexPath.row].checked = !items[indexPath.row].checked
tableView.reloadRows(at: [indexPath], with: .fade)
}
}
Then, the UITableViewCell subclass just needs a long press gesture recognizer and, upon the gesture being recognized, inform the view controller:
class ItemCell: UITableViewCell {
weak var delegate: CellDelegate?
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:)))
addGestureRecognizer(longPress)
}
#IBAction func handleLongPress(_ gesture: UILongPressGestureRecognizer) {
if gesture.state == .began {
delegate?.didLongPressCell(self)
}
}
}
By the way, by having the gesture on the cell, it avoids confusion resulting from "what if I long press on something that isn't a cell". The cell is the right place for the gesture recognizer.
You are not storing the change anywhere.
To avoid using too much memory, the phone reuses cells and asks you to configure them in the TableView's dataSource.
Let's say you have an array called data that has some structs that store what you want to show as cells. You would need to update this array and tell the tableView to reload your cell.
func userDidLongPress(gestureRecognizer: UILongPressGestureRecognizer) {
// We only care if the user began the longPress
guard gestureRecognizer.state == UIGestureRecognizerState.began else {
return
}
let locationInView = gestureRecognizer.location(in: tableView)
// Nothing to do here if user didn't longPress on a cell
guard let indexPath = tableView.indexPathForRow(at: locationInView) else {
return
}
// Flip the associated value and reload the row
data[indexPath.row].isChecked = !data[indexPath.row].isChecked
tableView.reloadRows(at: [indexPath], with: .automatic)
}
And always set the accessory when you configure a cell:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
-> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: "someIdentifier",
for: indexPath
)
cell.accessoryType = data[indexPath.row].isChecked ? .checkmark : .none
}

Getting EXC_BAD_INSTRUCTION(code=EXC_1386_INVOP, subcode=0x0) in swift because of PanGesture

I have made a collection view with 4 cells and when you press the button in cell 1 (I have not made the other cells yet) it will take you to a new ViewController called "FirstCollectionViewController". I have a pangesturderecognizer that shows the slide out menu when you swipe in the collection view.
But when you are in the "FirstCollectionViewController" and you want to come back to the collection view, you can press a button up in the left side corner. But when I press it, I will get an EXC_BAD_INSTRUCTION error at the "self.view.addGestureRecognizer(self.revealViewController().panGestureRecognizer())" inside the ViewDidLoad in the CollectionViewController. I have tried to put the panGestureRecognizer in the ViewDidAppear but the same happens
How can i fix it?
BTW the segue which should send you back to the collection view is called: "SegueFirstCollectionViewController"
CollectionViewController :
import Foundation
import UIKit
class CollectionViewController: UICollectionViewController {
var Array = [String]()
var ButtonArray = [String]()
override func viewDidLoad() {
super.viewDidLoad()
Array = ["1","2","3","4"]
ButtonArray = ["1", "2", "3", "4"]
self.view.addGestureRecognizer(self.revealViewController().panGestureRecognizer())
}
override func viewDidAppear(animated: Bool) {
}
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return Array.count
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("cell", forIndexPath: indexPath) as UICollectionViewCell
let Label = cell.viewWithTag(1) as! UILabel
Label.text = Array[indexPath.row]
let Button = cell.viewWithTag(2) as! UIButton
Button.setTitle(ButtonArray[indexPath.row], forState: UIControlState.Normal)
// Button.addTarget(self,action:#selector(ButtonArray(_:)), forControlEvents:.TouchUpInside)
Button.addTarget(self, action: Selector("ButtonArray:"), forControlEvents:.TouchUpInside)
return cell
}
override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
print("Inside didSelectItemAtIndexPath")
}
func ButtonArray(sender : UIButton) {
print("m")
if sender.currentTitle == "1"{
performSegueWithIdentifier("segueFirst", sender: nil)
}
if sender.currentTitle == "2"{
}
if sender.currentTitle == "3"{
}
if sender.currentTitle == "4"{
}
}
}
FirstCollectionViewController :
class FirstCollectionViewController : UIViewController {
#IBAction func SegueFirstCollectionViewController(sender: UIButton) {
performSegueWithIdentifier("SegueFirstCollectionViewController", sender: nil)
}
}
You are missusing segues. Segues creates new instance of their destination ViewController, meaning that when you start your app you have 1 CollectionViewController, then you click a cell and you have a stack of 1 CollectionViewController AND 1 FirstViewController, then you hit the button, and event it looks like it, you are not coming back to the original CollectionViewController, but you are creating a new one, meaning you now have 1 collectionViewController, one first viewController and one CollectionViewController.
This is why you are reexcecuting viewDidLoad, this is why it fail because the pan gesture recognizer has already been added to another view....
If you want to implement flat (push pop / master -> detail / back button...) you should encapsulate your collectionViewController in a NavigationViewController and use the freely given back button : DON'T HANDLE THE BACK YOURSELF, ESPECIALLY WITH SEGUE
PS for the sake of beeing exact but for an advertized public : a special kind of segue exist to deal with back, they are called unwind segue. However, UINavigationController should always be the preferred option for flat navigation. Unwind segue find their use when dealing with vertical navigation (modal presentation / OK - Cancel)
You don't need get the cell outside
Make set value in
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell{
//set value here
if self.currentTitle == "1" ){
//
}
else {
//...
}
}
and reload the specific cell
let indexPath = IndexPath(item: i, section: 0)
self.tableView.reloadRows(at: [indexPath], with: .automatic)
Hope it would make help for you:)

UICollectionView attempts to dequeue cells beyond data source bounds while reordering is in progress

I have implemented collection view with cell reordering
class SecondViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
private var numbers: [[Int]] = [[], [], []]
private var longPressGesture: UILongPressGestureRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
for j in 0...2 {
for i in 0...5 {
numbers[j].append(i)
}
}
longPressGesture = UILongPressGestureRecognizer(target: self, action: "handleLongGesture:")
self.collectionView.addGestureRecognizer(longPressGesture)
}
func handleLongGesture(gesture: UILongPressGestureRecognizer) {
switch(gesture.state) {
case .Began:
guard let selectedIndexPath = self.collectionView.indexPathForItemAtPoint(gesture.locationInView(self.collectionView)) else {
break
}
collectionView.beginInteractiveMovementForItemAtIndexPath(selectedIndexPath)
case .Changed:
collectionView.updateInteractiveMovementTargetPosition(gesture.locationInView(gesture.view!))
case .Ended:
collectionView.endInteractiveMovement()
default:
collectionView.cancelInteractiveMovement()
}
}
}
extension SecondViewController: UICollectionViewDataSource {
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return numbers.count
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
let number = numbers[section].count
print("numberOfItemsInSection \(section): \(number)")
return number
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
print("cellForItemAtIndexPath: {\(indexPath.section)-\(indexPath.item)}")
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! TextCollectionViewCell
cell.textLabel.text = "\(numbers[indexPath.section][indexPath.item])"
return cell
}
func collectionView(collectionView: UICollectionView, moveItemAtIndexPath sourceIndexPath: NSIndexPath, toIndexPath destinationIndexPath: NSIndexPath) {
let temp = numbers[sourceIndexPath.section].removeAtIndex(sourceIndexPath.item)
numbers[destinationIndexPath.section].insert(temp, atIndex: destinationIndexPath.item)
}
}
It works fine until I try to drag an item from section 0 to section 2 (which is off screen). When I drag the item to the bottom of collection view, it slowly starts scrolling down. At some point (when it scrolls past section 1) application crashes with fatal error: Index out of range because it attempts to request cell at index 6 in section 1 while there are only 6 items in this section. If I try to drag an item from section 1 to section 2, everything works fine.
Here's an example project reproducing this problem (credit to NSHint):
https://github.com/deville/uicollectionview-reordering
Is this a bug in framework or am I missing something? If it is the former, what would be the workaround?
I actually ran into this issue today. Ultimately the problem was in itemAtIndexPath - I was referencing the datasource to grab some properties for my cell : thus the source of the out of bounds crash. The fix for me was to keep a reference to the currently dragging cell and in itemAtIndexPath; check the section's datasource length versus the passed NSIndexPath and if out of bounds; refer to the dragging cell's property.
Likesuchas (in Obj-C ... haven't moved to Swift yet) :
NSArray* sectionData = [collectionViewData objectAtIndex:indexPath.section];
MyObject* object;
if (indexPath.row < [sectionData count]) {
object = [dataObject objectAtIndex:indexPath.row];
} else {
object = draggingCell.object;
}
[cell configureCell:object];
Not sure if this is similar to your exact issue; but it was mine as everything is working as expected now. :)

Cells insight UICollectionView do not load until there are visible?

I have a UITableView with UICollectionView insight every table view cell. I use the UICollectionView view as a gallery (collection view with paging). My logic is like this:
Insight the method
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// This is a dictionary with an index (for the table view row),
// and an array with the url's of the images
self.allImagesSlideshow[indexPath.row] = allImages
// Calling reloadData so all the collection view cells insight
// this table view cell start downloading there images
myCell.collectionView.reloadData()
}
I call collectionView.reloadData() and in the
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
// This method is called from the cellForRowAtIndexPath of the Table
// view but only once for the visible cell, not for the all cells,
// so I cannot start downloading the images
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! PhotoCollectionCell
if self.allImagesSlideshow[collectionView.tag] != nil {
var arr:[String]? = self.allImagesSlideshow[collectionView.tag]!
if let arr = arr {
if indexPath.item < arr.count {
var imageName:String? = arr[indexPath.item]
if let imageName = imageName {
var escapedAddress:String? = imageName.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())
if let escapedAddress = escapedAddress {
var url:NSURL? = NSURL(string: escapedAddress)
if let url = url {
cell.imageOutlet.contentMode = UIViewContentMode.ScaleAspectFill
cell.imageOutlet.hnk_setImageFromURL(url, placeholder: UIImage(named: "placeholderImage.png"), format: nil, failure: nil, success: nil)
}
}
}
}
}
}
return cell
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if self.allImagesSlideshow[collectionView.tag] != nil {
var arr:[String]? = self.allImagesSlideshow[collectionView.tag]!
if let arr = arr {
println("collection row: \(collectionView.tag), items:\(arr.count)")
return arr.count
}
}
return 0
}
I set the right image for the cell. The problem is that the above method is called only for the first collection view cell. So when the user swipe to the next collection view cell the above method is called again but and there is a delay while the image is downloaded. I would like all the collection view cells to be loaded insight every visible table view cell, not only the first one.
Using the image I have posted, "Collection View Cell (number 0)" is loaded every time but "Collection View Cell (number 1)" is loaded only when the user swipe to it. How I can force calling the above method for every cell of the collection view, not only for the visible one? I would like to start the downloading process before swiping of the user.
Thank you!
you're right. the function func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell will be called only when cell start to appear. that's a solution of apple called "Lazy loading". imagine your table / collection view have thousand of row, and all of those init at the same time, that's very terrible with both memory and processor. so apple decide to init only view need to be displayed.
and for loading image, you can use some asynchronous loader like
https://github.com/rs/SDWebImage
it's powerful and useful too :D

Resources