Collectionview Moving slider in row does not select the row

Hello,


I have a pretty simple setup... each row in my Collectionview has a slider and "percent" label below it. When the user adjusts the slider I want to update the percent label. The problem is that adjusting the slider doesn't select the row so I can't get the indexPath. Without the index path I can't get the item so I can't access the label.


I have seen two suggestions repeatedly... one tries to use "superview", but that won't work because the type casting doesn't line up. The other has multiple iterations of trying to use the coordinates of the tap, but I can't get any of them to work.


Thank you!

Accepted Reply

I'm confused about what you are asking then. Isn't that "slider position changed" method in your collection view cell class? If so, you have direct access to anything in the cell within that method. If you need the index path, so you can know which data it came from and do other things, do that part in the refreshField function using the collection view's indexPath(for:) method as I explained above.


You should not set the object in the addObserver statement, only in the post statement. If you set object to something in the addObserver statement, you will only get notifications from that one object.


As Cluade31 suggested, posting the code you have so far might help.

Replies

I want to update the percent label


What is this percent label ? What does it measures ? The position of the slider ?


If so, why do you want to change the selection ? you have to update directly the label in the IBAction in response to the sent event of the slider :

- value changed event, to update when dragging the thumb

- touch up inside when tapping inside

- touch up outside when tapping outside (may be the same IBAction as for inside)


That would look like this

    @IBAction func sliderChanged(_ sender: UISlider) {

        let max = sender.maximumValue
        let min = sender.minimumValue
        let percent = Int(100.0 * (sender.value - sender.minimumValue) / (sender.maximumValue - sender.minimumValue))
        label.text = String(percent) + " %"

        sender.setNeedsDisplay()     // Not always needed
    }

Thank you for your response...


Yes, the percent label shows the position of the slider... the problem is that I can't access it. It is a member of the collection view row, not the view controller. It really doesn't matter if the row gets selected, I just need access to the index path so I can get the item and use


item.percentLabel.text

Exact, the slider is hidden and is not in general manipulated directly.


So, I tried along this way (but did not fully test):

- in IB, add a swipeGestureRecogniqer to collection

- in the viewController where the collectionView is, add:

    @IBAction func swipeDetected(_ sender: UISwipeGestureRecognizer) {
        print("Swipe detected")
          // here compute the first visible row, taht will let you compute the %
    }


It does work like this.

But I had to add the gesture programmatically :

class Item1ViewController: UIViewController,UICollectionViewDataSource,UICollectionViewDelegate, UIGestureRecognizerDelegate {
    @IBOutlet weak var collectionView: UICollectionView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let swipeGestureUp = UISwipeGestureRecognizer(target: self, action: #selector(swipeDetected))
        swipeGestureUp.numberOfTouchesRequired = 1
        swipeGestureUp.direction = .up
        swipeGestureUp.delegate = self
        collectionView.addGestureRecognizer(swipeGestureUp)

        let swipeGestureDown = UISwipeGestureRecognizer(target: self, action: #selector(swipeDetected))
        swipeGestureDown.numberOfTouchesRequired = 1
        swipeGestureDown.direction = .down
        swipeGestureDown.delegate = self
        collectionView.addGestureRecognizer(swipeGestureDown)

        let swipeGestureLeft = UISwipeGestureRecognizer(target: self, action: #selector(swipeDetected))
        swipeGestureLeft.numberOfTouchesRequired = 1
        swipeGestureLeft.direction = .left
        swipeGestureLeft.delegate = self
        collectionView.addGestureRecognizer(swipeGestureLeft)

        let swipeGestureRight = UISwipeGestureRecognizer(target: self, action: #selector(swipeDetected))
        swipeGestureRight.numberOfTouchesRequired = 1
        swipeGestureRight.direction = .right
        swipeGestureRight.delegate = self
        collectionView.addGestureRecognizer(swipeGestureRight)
       
        collectionView.isScrollEnabled = false // Needed for vertical swipe to be detected ; but clearly, not good !
    }

The problem in my test app is that I have to desable collection view scrolling for vertical swipe to be detected !

So you should evaluate the swipe, or decide to move by one row for each swipe, and scroll to the new row (may be select it) in the swipeDetected func.


    @objc func swipeDetected(sender: UISwipeGestureRecognizer) {
        print("Swipe detected", sender.direction)
     // Here, for up and down, scroll by 1 row and redraw accordingly
    }

Thank you very much, but there has to be a more mainstream solution for this scenario. I mean the sender is the slider control. There has to be a way to get through the hierarchy or something. I can't believe this is an uncommon setup.

OK, let's forget gestures.


I have found this delegate func in UIScrollView, that let you know the position of the slider (in fact the offset position of the content of Collection view (ScrollView) with respect to the top of the Collectionj view itself.


    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let y = collectionView.contentOffset
        print("end scroll", y)
    }


Scrolling will give this type of log

end scroll (0.0, 6.33333333333333)

end scroll (0.0, 21.6666666666667)

end scroll (0.0, 41.3333333333333)

end scroll (0.0, 55.3333333333333)

end scroll (0.0, 63.6666666666667)

end scroll (0.0, 70.0)

end scroll (0.0, 78.0)

end scroll (0.0, 83.6666666666667)

end scroll (0.0, 87.3333333333333)

end scroll (0.0, 89.6666666666667)

end scroll (0.0, 90.0)

With this, you can easily compute the percent label.

You could send a notification to the view controller (with the cell as the object), or make the view controller a delegate of the cell.


EDIT: You would send the notification or call a delegate function when the position changes, which can be detected as indicated by Claude31. I'm assuming you want to get the position information to do other things in your view controller. If not, and all you want to do is update the label, then all you need is what Claude31 mentioned (assuming your collection view cells are instances of a class you created with the slider and the label as outlets).

Thank you... sorry for the late reply.


Yes, there are other things in the CollectionView item that I want to change. Which is the better solution; sending the notification or calling a delegate method? Which ever one it is, can you show me some code? Thanks again!

I find notification very flexible and easier to implement than delegates.


Examples are very simple:

- declare the notification name

extension Notification.Name { 
    public static let kRefresh = Notification.Name("refresh")
}

- the receiver subscribes to the notification

        NotificationCenter.default.addObserver(self, selector: #selector(refreshFields), name: .kRefresh, object: nil) 

- It implements the action on reception of notification

    @objc func refreshFields() {  
    //  your action
}


- the sender posts the notification.

        NotificationCenter.default.post(name: .kRefresh, object: self)

To add a few more implemenation details to Claude31's post:


The extension statement is global, so put it ouside any class definition (probably at the top of your ViewController code file).

The addObserver statement would be early in your ViewController's life cycle, for example in its viewDidLoad.

The refreshFields function is in your ViewControlller.

The .post(...) call is in the scrollViewDidScroll function Claude31 demonstrated.

I'm with you on the notification solution. However, I still don't know how to do the updating. I'm in a method that responds to the slider position changing. The sender for this method is the slider control. I want to update other controls in the cell but I don't know how to get access to which one the slider control that changed is contained within.

In the .post call, you are passing the collection view item ("self") to the observer function. I think you need to change the selector to "refreshFields:" instead of "refreshFields", and make your refreshFields function take a Notification as a parameter. The .object property of the notification will be the collection view item that contains the slider. I don't remember the exact swift syntax for the selector, but hopefully that's enough info for you to figure it out 🙂.

Getting closer... how do I know which item in the collection view contains the slider that was moved? Moving the slider doesn't automatically select the item.

You could set programmatically a tag ineach cell with same value for both (slider and item) ; and use it to know which item is concerned when you move a slide.


Note: in my example, the selector function had no parameter, hence no colon at the end of name. If you define a parameter, need colon.

See my reply above that starts with "In the .post call...". Your notification can contain a reference to the cell that contains the slider.

Thanks! But, that reference is what I don't know how to acquire.