I'm trying to create hints for new users in iOS application, like to open profile click here, etc. I have a collection view and inside it I have elements that I fetch from server.
I need to show hint near the first element. My code:
var tipView = EasyTipView(
text: "Выбирай предмет и начинай обучение",
preferences: EasyTipView.globalPreferences
)
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let section = sections[indexPath.section]
switch section {
case .fav_subjects:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "FavSubjectsCollectionViewCell", for: indexPath) as! FavSubjectsCollectionViewCell
let favSubjects = model[.fav_subjects] as! [Subjects.FavSubject]
let cellModel = favSubjects[indexPath.item]
cell.configure(with: cellModel)
//here I'm trying to render hint near first element of collectionView
if indexPath.section == 0 && indexPath.item == 0 {
let view = cell.contentView
tipView.show(forView: view)
}
return cell
case .all_subjects:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "AllSubjectsCollectionViewCell", for: indexPath) as! AllSubjectsCollectionViewCell
let allSubjects = model[.all_subjects] as! [Subjects.Subject]
let cellModel = allSubjects[indexPath.item]
cell.configure(with: cellModel)
return cell
}
}
}
My problem is when I open screen for the first time my hint displays on the top of the screen and not near the first element, but when I switch to another screen and then go back, hint displays exactly where expected.
I suggest that my problem is screen rendered for the first time before all items fetched from server so tried to use layoutIfNeeded() to rerender when the size is changed but it doesn't help:
tipView.layoutIfNeeded()
if indexPath.section == 0 && indexPath.item == 0 {
let view = cell.contentView
self.tipView.layoutIfNeeded()
self.tipView.show(forView: view)
}
Also I have tried to use viewDidAppear, because I thought that if screen is already rendered than I will have element rendered and I will show hint in the right place for the first render but it turns out that in viewDidAppear cell is nil:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let cell = collectionView.cellForItem(at: IndexPath(item: 0, section: 0))
}
Incorrect(when screen opened first time):
Correct:
I'm new to iOS so I think I missed something important, please help.
The method cellForItem(at:) gets called before the cells gets displayed on screen. In fact, at this point the final frame of the cell is not yet determined, it will happen later when the cell gets added to the view hierarchy and the layout pass happens.
In some cases even if cellForItem(at:) gets called, the cell may never appear on screen.
If you always display the hint near the first element, perhaps it will be easier to use the collection view's frame as reference instead of the first cell unless you need the hint to scroll along with the cell.
Alternatively, add the hint as a subview to the cell's contentView and use Auto Layout to define its position inside the cell. Don't forget to remove the hint from the view hierarchy when appropriate because the same cell may end up being dequeued for another index path during scroll.
Related
I have a collection view scrollable in 2-dimensions. Each row is a section with some cells.
Now in each row only one item is special (will have enum state = special, otherwise normal).
I want a behavior such that when we move up or down and we were on a special cell we should jump to special cell of next row.
View that behavior here
I did it like this, I will be storing previous focused cell's indexpath in a variable and when I move to next cell then in collectionView(:canFocusItemAt:) I will check if previous was special and we are changing the section then only next cell is focusable if it's special.
func collectionView(_ collectionView: UICollectionView, canFocusItemAt indexPath: IndexPath) -> Bool {
if let previousFocusIndex = focusIndex,
previousFocusIndex.section != indexPath.section,
let previousCell = collectionView.cellForItem(at: previousFocusIndex) as? EpisodeCell,
let nextCell = collectionView.cellForItem(at: indexPath) as? EpisodeCell,
previousCell.timeState == .special {
print("🟢#Searching_Episode: \(nextCell.description)")
return nextCell.timeState == .special
}
return true
}
But now I came to know about behavior of focus engine that its search area is based on current cell's width.
And is a cell in next row does not lies in that search area it will not focus there.
Problem I am facing
I tried googling but didn't find anything better and can't think what else I can try.
If you have an idea it will be really helpful. Thanks.
There are 2 parts to solve this problem.
Identify the user is seeing the special content shown in collection view[Special Item]
This can be achieved by checking the Visible Items and cross check with data source that if it is special
var indexPathsForVisibleItems: [IndexPath] { get }
Scrolling to Special Item in a particular section
Based on the out come of step 1 we should decide calling scrollToItem
func scrollToItem(at: IndexPath, at: UICollectionView.ScrollPosition, animated: Bool)
You may put this logic when you identify that user is scrolling the collection view by confirming to UIScrollViewDelegate, which is inherited by UICollectionViewDelegate.
protocol UIScrollViewDelegate //Protocol to use
func scrollViewDidScroll(UIScrollView)
//Tells the delegate when the user scrolls the content view within the scroll view.
We need to have optimised logic here such that scrollToItem is only called when there is a new row is showed.
I was making a demo app for instagram and implemented story view at the top of Home Screen and decided to keep add story button in one section and stories coming from server in other section. However sometimes the add story button shows image coming from the server while in my code I clearly set its image = add story image. Here's my code:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "StoryCollectionViewCell", for: indexPath) as! StoryCollectionViewCell
if indexPath.section == 0 {
//code to set profile picture
cell.ivProfilePic.image = nil
cell.ivProfilePic.layer.borderWidth = 0
cell.ivRoundImage.isHidden = true // outside circle if unwatched story
cell.ivProfilePic.image = UIImage(named: "Create-Story-icon")
}
else {
let data = stories[indexPath.item]
//code to set profile picture
cell.ivProfilePic.image = nil
cell.ivProfilePic.kf.setImage(with: data.logo, options: [.processor(DownsamplingImageProcessor(size: cell.ivProfilePic.frame.size))])
}
}
I even tried to set nil so in case cell was retaining image after scrolling it would be set nil first, still no luck. The "create story icon" gets replaced by some image coming from server sometimes.
Or is there any other better way to show stories? and implement add story button different way?
What I'm trying to do is when I tap the button in a cell, that button in that cell becomes invisible. The problem is when I tap the button, it becomes invisible, but when I scroll the collection view the hidden button goes from one to the other. For example, I tap the second one it hides but when I scroll I see that the 7th becomes hidden. Every time I scroll the hidden button change.
This is the code I wrote:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell : CollectionViewCellKharid3 = collectionView.dequeueReusableCell(withReuseIdentifier: "customcell3", for: indexPath) as! CollectionViewCellKharid3
cell.lblEsmeMahsul.text = mainCats[indexPath.row]
cell.imgMahsul.af_setImage(withURL: URL(string : (mainadress + "/Opitures/" + mainPicNumbers[indexPath.row]))!, placeholderImage: UIImage(named: "loadings" ))
cell.btnKharid.addTarget(self, action: #selector(btnColectionviewCellTapped), for : UIControlEvents.touchUpInside)
cell.btnKharid.tag = indexPath.row
cell.btnMosbat.addTarget(self, action: #selector(btnMosbatTapped), for : UIControlEvents.touchUpInside)
cell.btnMosbat.tag = indexPath.row
cell.configureCell()
return cell
}
#objc func btnColectionviewCellTapped(_ sender:UIButton){
// let indexPath : IndexPath = self.collectionview1.ind
print(sender.tag)
sender.isHidden = true
}
#objc func btnMosbatTapped(_ sender:UIButton){
let index = IndexPath(item: sender.tag , section: 0)
let cell = self.collectionviewForushVije.cellForItem(at: index) as? CollectionViewCellKharid3
cell?.lblTedad.text = "22"
print(sender.tag)
}
Cells get reused. You need to keep track of which cells have been tapped so you can set the proper button state in your cellForItemAt method.
Declare a property in your class:
var beenTapped: Set<Int> = []
Then in btnColectionviewCellTapped add:
beenTapped.insert(sender.tag)
And in cellForItemAt you need:
cell.btnKharid.isHidden = beenTapped.contains(indexPath.item)
You should also replace the use of indexPath.row with indexPath.item. row is for table views. item is for collection views.
It's a very common mis-use of UICollectionView(or UITableView). When deal with them, you should alway keep one thing in mind, re-use. The collection/tableview cell will be highly reuse by os when on need. The problem cause in your code is, you assume the one time set of one property in a cell will be persistence, which is wrong. The cell come from dequeue method, can always be a new cell or an existing cell, therefore, any configuration should be apply to a cell should be config again. Think in that way, all view in a cell is "dirty" when it get it from collection view, you should set the property you want before return it back(or have a mechanism to set it later). Therefore, in your case, just set the isHidden property every time you prepare the cell in cellForRow delegate.
In the scenario mentioned in the question title, on changing the segment, ideally the UITableView should reload and hence the UITableViewCell should also reload. The issue is, all the content gets updated like label texts. But if I have expanded a subview of cell in one segment, it remains still expanded after segment is changed.
Segment index change function :
#IBAction func segmentOnChange(sender: UISegmentControl)
{
// Few lines
// self.tableMenu.reloadData()
}
Screenshot 1 :
Screenshot 2 :
So ideally, in screenshot 2, the cart view should have been collapsed.
Update :
Show/Hide view :
func showHideCartView(sender: UIButton)
{
let cell = self.tableMenu.cellForRow(at: IndexPath(row: 0, section: Int(sender.accessibilityHint!)!)) as! RestaurantMenuItemCell
if sender.tag == 1
{
// Show cart view
cell.buttonArrow.tag = 2
cell.viewAddToCart.isHidden = false
cell.constraint_Height_viewAddToCart.constant = 50
cell.buttonArrow.setImage(UIImage(named: "arrowUp.png"), for: .normal)
}
else
{
// Show cart view
cell.buttonArrow.tag = 1
cell.viewAddToCart.isHidden = true
cell.constraint_Height_viewAddToCart.constant = 0
cell.buttonArrow.setImage(UIImage(named: "arrowDown.png"), for: .normal)
}
self.tableMenu.reloadData()
}
cellForRowAtIndexPath :
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "RestaurantMenuItemCell", for: indexPath) as! RestaurantMenuItemCell
cell.selectionStyle = .none
let menuItem = self.menuItems[indexPath.section]
cell.imageViewMenuItem.image = UIImage(named: "recommend0#2x.png")
cell.labelName.text = menuItem.name
cell.labelDescription.text = menuItem.description
cell.labelPrice.text = String(format: "$%i", menuItem.price!)
cell.buttonArrow.accessibilityHint = String(format: "%i", indexPath.section)
cell.buttonArrow.addTarget(self, action: #selector(self.showHideCartView(sender:)), for: .touchUpInside)
return cell
}
Since you rely on the sender button's tag to determine whether the cell should be shown in expanded or collapsed state, you need to make sure that when the segment changes, the tags for all the cells' buttonArrow also change to 1.
Unless that happens, the cells will be reused and since the buttonArrow's tag is set to 2, it will be shown as expanded.
You are reusing cells, see first line of your cellForRowAt implementation:
let cell = tableView.dequeueReusableCell(withIdentifier: "RestaurantMenuItemCell", for: indexPath) as! RestaurantMenuItemCell
That means that a tableView does not create a new cell, nor it redraws it unless necessary for the given indexPath. However, in the code you expand a cell by setting some height constraints and isHidden flags on subviews of the cell. Now once you reload table at a new segment, the tableView will use the already rendered cells to make the reload as efficient as possible. However, that means, that although you will change the data in the cell, unless you explicitly collapse it, it will stay expanded (because previously you have set the constraints to expanded height).
You need to reset the expanded state when you change the segment. Now let's elaborate, because I believe your current solution has a hidden bug in it:
First of all, since as I said you are dequeueing the cells, in case there are a lot of items in the table and you are scrolling through them, there is a high chance that the expanded cell will get reused even somewhere later in the table view. So it will seem like there is some random item expanded too.
Therefore I suggest you to provide some model that would remember which cells are supposed to be expanded and then use the information in this model to update state of each cell before you return them in your cellForRowAt. The model can be for example a simple array of integer values that would represent indexes of cells that are expanded - so for example if there is an index 3 in the array, that would mean that cell at row 3 should be expanded. Then when you dequeue a cell in cellForRowAt for indexPath with row = 3 you should set constraints on that cell before returning it. This way you can remove the bug I mentioned. Moreover, then when you change segments, you can just remove all the indexes from the array to signify that no cell should be expanded anymore (do it before calling tableView.reloadData()).
What happened here, When you click any row then It will be expanded based on make true first condition in showHideCartView method and after that when you are changing segment index that keep height of previous row as it is because you are reusing cell So you must set normal hight of each row when you change segment index.
For do that take object of indexPath like
var selectedIndexPath: NSIndexPath?
Then set selectedIndexPath in showHideCartView method
func showHideCartView(sender: UIButton) {
selectedIndexPath = NSIndexPath(row: 0, section: Int(sender.accessibilityHint!)!)) // you can change row and section as per your requirement.
//// Your other code
}
Then apply below logic whenever you are changing segment index.
#IBAction func segmentOnChange(sender: UISegmentControl)
{
if indexPath = selectedIndexPath { // check selectedIndexPath is not nil
let cell = self.tableMenu.cellForRow(at: indexPath) as! RestaurantMenuItemCell
cell.buttonArrow.tag = 1
cell.viewAddToCart.isHidden = true
cell.constraint_Height_viewAddToCart.constant = 0
cell.buttonArrow.setImage(UIImage(named: "arrowDown.png"), for: .normal)
}
selectedIndexPath = nil // assign nil to selectedIndexPath
// Few lines
self.tableMenu.reloadData() // Reload your tableview
}
I have a CollectionView with 3 Cells which extend across the whole device's screen and basically represent my 3 main views.
I have paging enabled, so it basically works exactly like the iOS home screen right now.
My problem is that I want the "default" position of this CollectionView to be equal to view.frame.width so that the second Cell is the "default" view and I can swipe left and right to get to my secondary views.
I have already tried via
collectionView.scrollToItem()
and
collectionView.scrollRectToVisible()
as well as
collectionView.setContentOffset()
but they all seem to work only after the view has loaded (I tried them via a button in my navigation bar).
Thank you in advance!
EDIT:
Now this works, but I also have another collection view in that one middle cell which holds a list of little 2-paged UICollectionViews which are actually objects from a subclass of UICollectionViewCell called PersonCell each holding a UICollectionView. I want these UICollectionViews to be scrolled to index 1 as well, this is my code:
for tabcell in (collectionView?.visibleCells)! {
if let maincell: MainCell = tabcell as? MainCell {
for cell in maincell.collectionView.visibleCells {
if let c = cell as? PersonCell {
c.collectionView.scrollToItem(at: (NSIndexPath(item: 1, section: 0) as IndexPath), at: [], animated: false)
}
}
}
}
This is executed in the viewDidLayoutSubviews of my 'root' CollectionViewController.
EDIT 2:
Now I tried using following code in the MainCell class (it's a UICollectionViewCell subclass):
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
let personCell = cell as! PersonCell
personCell.collectionView.scrollToItem(at: (NSIndexPath(item: 1, section: 0) as IndexPath), at: [], animated: false)
}
EDIT 3:
Long story short, I basically need a delegate method that is called after a cell has been added to the UICollectionView.
Did you try [self.collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
Try calling the code in viewDidLayoutSubviews
Okay, I got it!
This SO answer gave me the final hint:
https://stackoverflow.com/a/16071589/3338129
Basically, now I just do this after initialization of the Cell:
self.collectionView.alpha = 1
self.data = data // array is filled with data
self.collectionView.reloadData()
DispatchQueue.main.async {
for cell in self.collectionView.visibleCells {
(cell as! PersonCell).collectionView.scrollToItem(at: (NSIndexPath(item: 1, section: 0) as IndexPath), at: [], animated: false)
}
self.collectionView.alpha = 1
}
Basically, the code in the DispatchQueue.main.async{...} is called on the next UI refresh cycle which means at a point where the cells are already there. To prevent the user from seeing the wrong page for a fraction of a second, I set the whole collectionView's alpha to 0 and toggle it on as soon as the data is there, the cell is there and the page has scrolled.