Update CollectionView supplementaryView header when off screen - swift

I have a view similar to Instagram's main stories feed. I have a horizontal collection view at the top where I have a supplementaryView header cell that looks the same but acts differently to the rest of the cells.
I am trying to update the objects in the supplementaryView which I have managed to do with the following code...
#objc func uploadCompleted() {
DispatchQueue.main.async {
self.collectionView.performBatchUpdates({
if let myCell = self.collectionView.supplementaryView(forElementKind: UICollectionElementKindSectionHeader, at: IndexPath(item: 0, section: 0)) as? MyCollectionReusableView {
// Do your stuff here
myCell.nameLbl.textColor = .black
...
}
}, completion: nil)
}
}
This works fine when the cell is in view, however when the cell is swiped off screen during this call it doesn't get updated.
Any ideas on how to get around this? Or a better approach to updating just the first cell in a collection view and not the rest.
Thanks

The header is not nil when it's visible you need
var color = UIColor.red
And inside viewForSupplementaryElementOfKind
func collectionView(_ collectionView: UICollectionView,
viewForSupplementaryElementOfKind kind: String,
at indexPath: IndexPath) -> UICollectionReusableView
let myCell = //
myCell.nameLbl.textColor = color
return myCell
}
Then alter color where and when you want after the header appears again it'll be changed to the latest color
#objc func uploadCompleted() {
DispatchQueue.main.async {
self.color = UIColor.black
self.collectionView.performBatchUpdates({
if let myCell = self.collectionView.supplementaryView(forElementKind: UICollectionElementKindSectionHeader, at: IndexPath(item: 0, section: 0)) as? MyCollectionReusableView {
// Do your stuff here
myCell.nameLbl.textColor = self.color
...
}
}, completion: nil)
}
}

Related

Collection view cell scrollToItem cast fails [Swift]

In my swift app I've a collection view with a lot of items, I want to scroll to an item and then set a propriety of the collectionview cell custom class, but the cast fails, why?
Here's my code, I've omitted some obviously steps, but the result is printing of "Cast fails":
class ViewController: UIViewController {
lazy var collectionView: UICollectionView = {
let cv = UICollectionView(frame: self.view.bounds, collectionViewLayout: UICollectionViewFlowLayout())
cv.backgroundColor = .gray
cv.register(Cell.self, forCellWithReuseIdentifier: Cell.id)
cv.delegate = self
cv.dataSource = self
return cv
}()
}
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.id, for: indexPath) as! Cell
cell.backgroundColor = .purple
cell.label.text = "\(indexPath)"
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if indexPath.item == 14 {
collectionView.scrollToItem(at: IndexPath(item: 1, section: 0), at: .top, animated: true)
guard let cell = collectionView.cellForItem(at: IndexPath(item: 1, section: 0)) as? Cell else {
print("Cast fails")
return
}
cell.label.text = "Something"
}
}
}
Here's the solution I found:
extension UICollectionView {
func scrollToItem(at indexPath: IndexPath, at position: UICollectionView.ScrollPosition, completion: #escaping () -> ()) {
scrollToItem(at: indexPath, at: .top, animated: true)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
completion()
}
}
}
The cast fails as the cell ( of that specific index ) is not visible at the moment you query for it
guard let cell = collectionView.cellForItem(at: IndexPath(item: 1, section: 0)) as? Cell else {
print("Cast fails")
return
}
instead you can try to set , at: .top, animated: false) or wrap the above code snippet inside a Dispatch after block
Btw you should better change the dataSource array and reload that index and that way you don't need a wait
// change source array at that index
// scroll to that row with/without animation
// make sure you set value for the cell label in cellForRowAt table's datasource method

change collection view cell background colour dynamically on button action not working

I try to change the background colour of specific cell on button action. Color of cell is changing but but I scrolling this collection view the color of cell misplace from there original position
when I scrolling this collection view which contain question number, color position in this control misplaced like in this image
How I can handle this problem that collection view cell color never change their position automatically.
This is my code on button click to change the color :
let indexs = IndexPath(row: currentQuestion, section: 0)
let celi = questionNumberCollection.cellForItem(at: indexs)
celi?.backgroundColor = .blue
Problems is
You are changing the background color of the cell but you're not maintaining the state of the cell anywhere in your model which is important in the case where the cell is reused while scrolling.
Solution:
A simple and standard solution might be maintaining a state variable in a model or as a separate array, and change the background color in the cellforRowatIndexPath method.
Example:
struct Question {
var desc:String
var mark:Int
var status:AnsweringStatus
}
enum AnsweringStatus {
case notAttented,correct,inCorrect
}
class ViewController:UIViewController,UICollectionViewDataSource {
var dataSource:[Question]!
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataSource.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ShowCell", for: indexPath) as! ShowCell
switch dataSource[indexPath.row].status {
case .notAttented:
cell.backgroundColor = .gray
case .correct:
cell.backgroundColor = .red
case .inCorrect:
cell.backgroundColor = .green
}
return cell
}
}
Have showcased only the parts necessary to solve the issue. So on click of the button just changing the state in the respective model object using the index path and reloading the collection view will do the job.
Provide more insights about the issue, if this doesn't work for you.
I think your problem was that CollectionViews and TableView reuse the cells.
In your CollectionViewCell class use this method to reset the reused Cell to default values or Colors.
#IBAction func onButtonTappet(sender: UIButton) {
let indexs = IndexPath(row: currentQuestion, section: 0)
let cell = questionNumberCollection.cellForItem(at: indexs) as? MyCell
cell?.onButtonTapped = true
}
class MyCell: UICollectionViewCell {
var onButtonTapped: Bool = false {
didSet { checkBackgroundColor() }
}
override func prepareForReuse() {
checkBackgroundColor()
}
override func awakeFromNib() {
checkBackgroundColor()
}
private func checkBackgroundColor() {
self.backgroundColor = onButtonTapped ? myTappedColor : myDefaultColor
}
}

Cell of Collection does not let to be unchecked after making it selected

I've been stuggling for a while with saving which cell was marked and which not, the problem was not that hard. After few tries it is all about saving indePath somewhere and after return to the view with collection we are able to do some stuffs on the array of the choosen indexPaths inside willDisplay of the collectionView method. Basically that how it looks :
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
switch collectionView {
case myCollection:
var count: Int = 0
if !SessionMenager.Instance.indexPaths.isEmpty {
for index in SessionMenager.Instance.indexPaths {
if index == indexPath {
print(SessionMenager.Instance.indexPaths.count)
myCollection.selectItem(at: SessionMenager.Instance.indexPaths[count], animated: true, scrollPosition: .bottom)
var image = cell.viewWithTag(1) as! UIImageView
UIView.animate(withDuration: 0.25, animations: {
image.transform = CGAffineTransform(scaleX: 1.3, y: 1.3)
image = image.drawRingForCircle(imageView: image, color: .white)
})
cell.isSelected = true
count += 1
}
}
}
default:
break
}
}
Which works perfect but only for a one way. Now I am not able to deselect any of selected cells except the first selected before going to another view - why is that ? Does any one could help me ?
Thanks in advance!
EDIT
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath {
switch collectionView {
case xxx:
break
case yyy:
break
case myCollection:
let image = myCollection.cellForItem(at: indexPath)?.viewWithTag(1) as! UIImageView
UIView.animate(withDuration: 0.25, animations: {
image.transform = CGAffineTransform(scaleX: 1, y: 1)
image.layer.sublayers?.removeLast()
})
for (key, _) in tmpDict {
if key == image {
if let index = self.choosenData.index(of: tmpDict[key]! ) {
self.choosenData.remove(at: index)
SessionMenager.Instance.indexPaths = SessionMenager.Instance.indexPaths.filter { $0 != indexPath }
}
}
}
default:
break
}
}
How about changing your collectionView(_:willDisplay:forItemAt:) method to this:
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
var count: Int = 0
for index in SessionMenager.Instance.indexPaths {
if index == indexPath {
myCollection.selectItem(at: indexPath, animated: true, scrollPosition: .bottom)
var image = cell.viewWithTag(1) as! UIImageView
UIView.animate(withDuration: 0.25, animations: {
image.transform = CGAffineTransform(scaleX: 1.3, y: 1.3)
image = image.drawRingForCircle(imageView: image, color: .white)
})
}
}
}
It might not solve your issue, but it will definitely be simpler to debug. Also, you don't need to set isSelected on your cell directly. UICollectionView.selectItem does that already.
You should also check if you're getting all the correct IndexPaths, and call UICollectionView.reloadData on your collection view after you're done loading data.
ANSWER
I fugure out what must be done to make it works!
Basically whenever someone will have similar problems I do not recomend willDisplay cell method - it will not works in that case well.
While you invoke a .selectItem on a collection inside this method, the cell yes it will be selected but.. there is one problem, even while u set these on selected the delegate after u click inside a cell will jump into didSelectItemAt instead of didDeselectItemAt so to uncheck the cell you need click twice which is not good.
The second thing we might add to willDisplay cell .isSelected = true but after that we solved ~50% of our problem becouse we can not deselect them any more, the delegate does not react any more for other selected cells instead of first selected.
I ask my self, what if I do the same stuff after everything is loaded ? Then I've tryid it - it works!
For that I used viewDidAppear(_ animated: Bool).
The working example looks like :
if !SessionMenager.Instance.indexPaths.isEmpty {
for index in SessionMenager.Instance.colorsIndexPaths {
let cell = myCollection.cellForItem(at: index)!
myCollection.selectItem(at: index, animated: true, scrollPosition: .bottom)
var something = cell.viewWithTag(**YOUR TAG**) as! SMTH
// And the body what would you like to do with `something`, ex. increase scale of item, change a background, tint color etc.
})
}
}
Hope it will helps anyone who needs that! Cheers!

Collectionview interactive cell swapping

I have implemented a collection view as described here. As you can see, it uses the interactive re-ordering of cells of collectionview provided in iOS 9. But the problem is, I cannot control the reordering of cells.
Say the cells are like this -
1 2 3 4
5 6 7 8
9 10 11 12
I want to swap only cell 6 and 4. So after re-ordering the cells will be
1 2 3 6
5 4 7 8
9 10 11 12
Here, In the tutorial, at the beginning of the program, the collection view is like this -
If I put Starbuck on top of Rose Tyler this happens -
Notice that Sarah Connor has Place of Starbuck.
I want to control the reordering of cells so that here Rose Tyler and Starbuck positions will be swapped.
How do I do that?
The simple solution will be to implement your own interactive cell ordering behaviour by simply moving the cell along the pan in screen and swap when gesture ends on another cell's location (with your own animation and data source update ). Here is a working sample:
class ViewController: UIViewController, UICollectionViewDataSource {
var arr: [String] = (0...100).map { return "\($0)" }
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let cv: UICollectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: layout)
cv.register(Cell.self, forCellWithReuseIdentifier: Cell.id)
layout.itemSize = CGSize(width: view.bounds.width/3.5, height: 100)
cv.dataSource = self
cv.addGestureRecognizer(longPressGesture)
return cv
}()
lazy var longPressGesture: UILongPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongGesture(gesture:)))
private var movingCell: MovingCell?
override func viewDidLoad() {
super.viewDidLoad()
view = collectionView
}
#objc func handleLongGesture(gesture: UILongPressGestureRecognizer) {
var cell: (UICollectionViewCell?, IndexPath?) {
guard let indexPath = collectionView.indexPathForItem(at: gesture.location(in: collectionView)),
let cell = collectionView.cellForItem(at: indexPath) else { return (nil, nil) }
return (cell, indexPath)
}
switch(gesture.state) {
case .began:
movingCell = MovingCell(cell: cell.0, originalLocation: cell.0?.center, indexPath: cell.1)
break
case .changed:
/// Make sure moving cell floats above its siblings.
movingCell?.cell.layer.zPosition = 100
movingCell?.cell.center = gesture.location(in: gesture.view!)
break
case .ended:
swapMovingCellWith(cell: cell.0, at: cell.1)
movingCell = nil
default:
movingCell?.reset()
movingCell = nil
}
}
func swapMovingCellWith(cell: UICollectionViewCell?, at indexPath: IndexPath?) {
guard let cell = cell, let moving = movingCell else {
movingCell?.reset()
return
}
// update data source
arr.swapAt(moving.indexPath.row, indexPath!.row)
// swap cells
animate(moving: moving.cell, to: cell)
}
func animate(moving movingCell: UICollectionViewCell, to cell: UICollectionViewCell) {
longPressGesture.isEnabled = false
UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.1, initialSpringVelocity: 0.7, options: UIViewAnimationOptions.allowUserInteraction, animations: {
movingCell.center = cell.center
cell.center = movingCell.center
}) { _ in
self.collectionView.reloadData()
self.longPressGesture.isEnabled = true
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return arr.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: Cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.id, for: indexPath) as! Cell
cell.titleLable.text = arr[indexPath.row]
return cell
}
private struct MovingCell {
let cell: UICollectionViewCell
let originalLocation: CGPoint
let indexPath: IndexPath
init?(cell: UICollectionViewCell?, originalLocation: CGPoint?, indexPath: IndexPath?) {
guard cell != nil, originalLocation != nil, indexPath != nil else { return nil }
self.cell = cell!
self.originalLocation = originalLocation!
self.indexPath = indexPath!
}
func reset() {
cell.center = originalLocation
}
}
final class Cell: UICollectionViewCell {
static let id: String = "CellId"
lazy var titleLable: UILabel = UILabel(frame: CGRect(x: 0, y: 20, width: self.bounds.width, height: 30))
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(titleLable)
titleLable.backgroundColor = .green
backgroundColor = .white
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
}
For reordering of collection view item we need to add Long press gesture on collection view . and for reordering the collection View item with a particular indexPath, UICollectionViewDelegate provide the "targetIndexPathForMoveFromItemAt" method.
And for getting the complete tutorial you follow this one Reordering CollectionView Item
I would do in the following steps...
1-st Step Create a model like this
class CrewMembersModel {
var name: String?
var position: Int?
fileprivate init (name: String, position: Int) {
self.name = name
self.position = position
}
static var crewMembersDataList: [CrewMembersModel]? = nil
class func getMembersList() -> [CrewMembersModel] {
if (self.crewMembersDataList == nil) {
self.crewMembersDataList = [CrewMembersModel]()
self.crewMembersDataList?.append(CrewMembersModel(name: "Sam Carter", position: 0))
self.crewMembersDataList?.append(CrewMembersModel(name: "Dana Scully", position: 1))
self.crewMembersDataList?.append(CrewMembersModel(name: "Rose Taylor", position: 2))
self.crewMembersDataList?.append(CrewMembersModel(name: "Sarah Conor", position: 3))
self.crewMembersDataList?.append(CrewMembersModel(name: "Starbuck", position: 4))
self.crewMembersDataList?.append(CrewMembersModel(name: "River Tam", position: 5))
self.crewMembersDataList?.append(CrewMembersModel(name: "Sarah Jane", position: 6))
}
return crewMembersDataList!.sorted(by: { $0.position! < $1.position!})
}
}
Please Note
Here you can add other properties of your model like image, etc...
2-nd Step Get model list in ViewController viewDidLoad method
self.crewMembersDataList = CrewMembersModel.getMembersList()
3-rd Step In func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell parse model data
let crewMembersData: CrewMembersModel = self.crewMembersDataList[indexPath.row - 1]
cell.name = crewMembersData.name!
And finally in 4-th step whenever you made changes change the positions of selected cells and call collectionView.reloadData()
func changePositions(selectedModelOne: CrewMembersModel, selectedModelTwo: CrewMembersModel) {
let tmpPosition = selectedModelOne.position!
selectedModelOne.position = selectedModelTwo.position!
selectedModelTwo.position = tmpPosition
self.crewMembersDataList.sorted(by: { $0.position! < $1.position!})
self.collectionView.reloadData()
}
Please Note I didn't check entire code, might be some syntax issues
It sounds as if you're only moving one object, or in your case Starbuck.
When you do that the list is going to reorder itself; Based on that Rose Tyler will logically be in the position that you've shown.
If you think of it as a queue for a ride in a theme park then the behaviour you've shown makes perfect sense. Sam, Dana and Rose are first second and third in the queue and Sarah, Starbuck and River are fourth, fifth and sixth. If Rose then lets Starbuck go in front of her then Rose is then fourth in line and Sarah is now fifth.
To get what you want you'll need to keep a reference of where Rose is in the list and where Starbuck is in the list. You'll then need to move each person that needs to move one by one rather than just moving Starbuck as you're doing currently.
UICollectionView *collection;
MSMutableArray *datasource;
NSIndexPath *from = [NSIndexPath indexPathForItem:0 inSection:0];
NSIndexPath *to = [NSIndexPath indexPathForItem:1 inSection:0];
// swap animated
[collection performBatchUpdates:^{
// To avoid problems, you should update your data model inside the updates block
[datasource exchangeObjectAtIndex:from.item withObjectAtIndex:to.item];
// for swap need two move operations
[collection moveItemAtIndexPath:from toIndexPath:to];
[collection moveItemAtIndexPath:to toIndexPath:from];
// no need to update the moved cells because the content has not changed
//[collection reloadSections:[NSIndexSet indexSetWithIndex:0]];
} completion:^(BOOL finished) {
}];

How do I programmatically select an object (and have that object show as selected) in the new NSCollectionView?

I have sucessfully implemented a 10.11 version of NSCollectionView in my Mac app. It displays the 10 items that I want, but I want the first item to be automatically selected when the app starts.
I have tried the following in the viewDidLoad and alternatively in the viewDidAppear functions;
let indexPath = NSIndexPath(forItem: 0, inSection: 0)
var set = Set<NSIndexPath>()
set.insert(indexPath)
collectionView.animator().selectItemsAtIndexPaths(set, scrollPosition: NSCollectionViewScrollPosition.Top)
I have tried line 4 above with and without the animator
I have also tried the following in place of line 4
collectionView.animator().selectionIndexPaths = set
with and without the animator()
While they both include the index path in the selected index paths, neither actually displays the item as selected.
Any clues where I am going wrong?
I propose not to use the scroll position. In Swift 3 the following code in viewDidLoad is working for me
// select first item of collection view
collectionView(collectionView, didSelectItemsAt: [IndexPath(item: 0, section: 0)])
collectionView.selectionIndexPaths.insert(IndexPath(item: 0, section: 0))
The second code line is necessary, otherwise the item is never deselected.
The following is working, too
collectionView.selectItems(at: [IndexPath(item: 0, section: 0)], scrollPosition: NSCollectionViewScrollPosition.top)
For both code snippets it is essential to have a NSCollectionViewDelegate with the function
func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set<IndexPath>) {
// if you are using more than one selected item, code has to be changed
guard let indexPath = indexPaths.first
else { return }
guard let item = collectionView.item(at: indexPath) as? CollectionViewItem
else { return }
item.setHighlight(true)
}
According to Apple's docs, programmingly selecting items using NSCollectionView's methods won't call NSCollectionViewDelegate's didSelect method. So you have to add the highlight yourself.
override func viewDidAppear() {
super.viewDidAppear()
retainSelection()
}
private func retainSelection() {
let indexPath = IndexPath(item: 0, section: 0)
collectionView.selectItems(at: [indexPath], scrollPosition: .nearestVerticalEdge)
highlightItems(true, atIndexPaths: [indexPath])
}
private func highlightItems(_ selected: Bool, atIndexPaths: Set<IndexPath>) {
for indexPath in atIndexPaths {
guard let item = collectionView.item(at: indexPath) else {continue}
item.view.layer?.backgroundColor = (selected ? NSColor(named: "ItemSelectedColor")?.cgColor : NSColor(named: "ItemColor")?.cgColor)
}
}
I think you use view.layer for display of selection state. And it looks like NSCollectionViewItem hasn't layer at moment. If you subclass NSCollectionViewItem try to enable wantsLayer property of it's view in viewDidLoad method.
override func viewDidAppear() {
super.viewDidLoad()
self.view.wantsLayer = true
}
Also you can enable wantsLayer in func collectionView(NSCollectionView, itemForRepresentedObjectAt: IndexPath) -> NSCollectionViewItem.
func collectionView(collectionView: NSCollectionView, itemForRepresentedObjectAtIndexPath
indexPath: NSIndexPath) -> NSCollectionViewItem {
let item = self.collectionView.makeItemWithIdentifier("dataSourceItem", forIndexPath: indexPath)
// Configure the item ...
item.view.wantsLayer = true
return item
}
Then you can call
collectionView.selectionIndexPaths = set
and be sure it works.