Collection View is reloaded then why wrong item disappears - swift

I was seeing a code where a collection view is made and we can mark some items as favorite. Also there is a filter button on top if we select that only favorite items will be visible.
Now is favorite filter is applied, then any random item is unfavourated then from the collection view, the last item disappears (which is wrong, but the bussiness logic going on is right I have tested multiple times using breakpoints) and now if I go back and come to favorites page again then the wrong item which disappeared is in the list and the correct item which was unfavorited is not present because the businesses logic was right and the data source has correct elements so the collection view made is correct (what i think).
Now I guess after tapping favorite button something wrong is hapenning, that the collection view reloading has some problem.
Also second question is is always mandatory to call
collectionView.reloadData()
and
collectionView.reloadItems(at: [IndexPath(item: index, section: 0)])
in main thread using DispatchQueue.main.async
I am new to swift, can you help out what could be the case that the reload is happening is wrong but in data source correct element is removed.
Problem gif: View Here
I was not able to add in this post
UPDATE: added code snippets:
On tapping heart symbol this function is invoked:
func editFavouriteChannelList(with viewModel: MyChannelViewModel?, index: Int) {
guard var viewModel = viewModel,
viewModel.isFavouriteEnabled,
var channel = viewModel.channels?[safe: index],
!channel.state.isChangingStateInProgress() else {
return
}
// Some working
// Calls updateCell fn with same viewModel but updated channel state
self.updateCell(with: viewModel, channelCellModel: channel)
// This makes API call then again we call on it's success updateCell fn with viewModel have removed that updated channel
interactor.editFavouriteChannelList(with: channel, isMarkingFavourite: isMarkingFavourite)
}
This presenter's updateCell is called twice,
first when only channelCellModel is updated
and second time after making an API call that time viewModel will have that channel removed from it.
In presenter:
private func updateCell(with viewModel: MyChannelViewModel, channelCellModel: MyChannelCellModel) {
DispatchQueue.main.async { [weak self] in
self.interface?.updateCell(with: viewModel, channelCellModel: channelCellModel)
}
}
In ViewController:
func updateCell(with viewModel: MyChannelViewModel, channelCellModel: MyChannelCellModel) {
if myChannelVM?.channels?.count != viewModel.channels?.count {
self.myChannelVM = viewModel
collectionView.reloadData()
} else {
guard let index = myChannelVM?.channels?.firstIndex(of: channelCellModel) else {
return
}
myChannelVM?.channels?[index] = channelCellModel
print(" Like: updateCell local : \(myChannelVM?.channels?[index])")
collectionView.reloadItems(at: [indexPath])
}
}
In View Controller logic going on is like this first call to updateCell is made where viewModel is same as myChannelVM but channelCellModel is a channel with updated state.
Then second time call to updateCell is made where viewModel is same as myChannelVM but not having that channel which was earlier marked unfavorite.
Now if I remove collectionView.reloadItems(at: [indexPath]) from else block and use collectionView.reloadData() in place of it then collection view updates correctly otherwise not.
So updateCell(at:) is interfereing with reloadData() of when made in next call.
So there is issue like Why is a call to reloadItems(at:) stopping/breaking call to reloadData() in Collection View?
What is the reason for this in my case?

Related

UICollectionView inside UITableViewCell slow srolling

I know that this question was asked a couple of times, but other solutions didn't work for me.
I have this model:
var notifications = [Notification]()
Notification:
let user: User (name, profileimage, etc)
let items: [Movies] (movie has image, name, etc)
So i display my notifications inside a tableview, each cell has profile info at the top and collectionview with items bellow.
Inside my tableviewcell, i have to reloadData of my collectionview to display correct movies. I know that probably reloadData method causes this lagging, but are there any solutions to avoid it?
TableViewcell:
var notification: Notification! {
didSet {
collectionView.reloadData();
}
}
I also tried this thing inside tablecell and call this method in willDisplayTableViewcell, but it doesn't help at all:
func setCollectionViewDataSourceDelegate(forRow row: Int) {
collectionView.delegate = self
collectionView.dataSource = self
collectionView.reloadData()
}
Images are loading using kingfisher, so it's fine in other places in my project.
userProfileImage.kf.setImage(
with: URL(string: profileImage),
options: [
.processor(DownsamplingImageProcessor(size: CGSize(width: 175, height: 175))),
.scaleFactor(UIScreen.main.scale),
.transition(.fade(0.2)),
]
)
I would comment out the code you have shown above as it will only get in the way and give false readings. My gut feeling is somehow you have managed to directly wire your notifications to your tableview / collection view and they are running on a background thread. What I would do to test this theory is wrap code anywhere where the UI is going to be updated with a perform on main thread like this
DispatchQueue.main.async { [unowned self] in
//Your UI update code within cellforRow / cellForItem (both table and collection view) Here
}
Try doing this one by one until you find the culprit and for a better solution try to refactor so you don't have to force this Main thread directive.

UICollectionView's prefetchItemsAt not being called

I'm trying to implement data prefetching for my UICollectionView using the UICollectionViewDataSourcePrefetching protocol; however, the respective method is not being called.
The collection view has a custom layout. I have also tried with the regular flow layout, same results.
Upon data reload, I execute the following code to make the collection view have the size of its content to prevent scrolling within that collection view but in a scroll view outside the collection view:
func reloadData() {
viewDidLayoutSubviews()
collectionView.reloadData()
collectionView.layoutIfNeeded()
viewDidLayoutSubviews()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
collectionViewHeightConstraint.constant = collectionView.collectionViewLayout.collectionViewContentSize.height
collectionView.layoutIfNeeded()
view.layoutIfNeeded()
}
Maybe that has something to do with it?
What I have done:
my UIViewController does inherit from UICollectionViewDataSourcePrefetching
collectionView.prefetchDataSource = self (also tried using storyboard)
collectionView.isPrefetchingEnabled = true (also tried using storyboard)
I have implemented collectionView(_:prefetchItemsAt:)
Issue:
The prefetchItemsAt method is not being called. I determined that by placing a print statement in the method & setting up a breakpoint.
Like requested in the comments, I'll share my implementation for this issue here:
I created the tracker prefetchState which determines whether I'm prefetching at the moment, or not:
enum PrefetchState {
case fetching
case idle
}
var prefetchState: PrefetchState = .idle
Then, I hooked up the scroll view's delegate (the scroll view my collection view is in) to my view controller and implemented the scrollViewDidScroll method:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard scrollView == self.scrollView else { return }
let prefetchThreshold: CGFloat = 100 // prefetching will start 100pts above the bottom of the scroll view
if scrollView.contentOffset.y > scrollView.contentSize.height-screenBounds.height-prefetchThreshold {
if prefetchState == .idle {
prefetchItems()
}
}
}
In there, you can see that I check whether we're already prefetching. If not, I call prefetchItems(), as implemented here:
func prefetchItems() {
guard prefetchState == .idle else { return }
prefetchState = .fetching
someDownloadFuncWithCompletionBlock { (newItems) in
self.dataSource += newItems
self.collectionView.reloadData()
self.prefetchState = .idle
}
}
I execute the following code to make the collection view have the size of its content to prevent scrolling within that collection view but in a scroll view outside the collection view:
This sounds very broken.
Why are you doing this?
Prefetching on the collection view (from the docs) is triggered when the user scrolls the collection view. By making the collection view frame the same as the content size you are essentially disabling the scrolling of the collection view itself.
The collection view calls this method as the user scrolls
If you are forcing the collection view frame to be the same as the content size then you are entirely breaking UICollectionView.
The reason the prefetch isn't called is because every cell has been loaded already. Nothing is in prefetch any more. Because your collection view is displaying every cell at the same time.
If you want to scroll a collection view... let the collection view handle it. You shouldn't need to place the collection view inside another scroll view.

Refreshing a view controller on Firebase logout

I have a view controller that has a UICollectionView with information pulled from Firebase. When a user visits this view controller their info is placed in the collection view cells.
However, when the user logs out and logs in with a different account, the previous user's data stays in the collection view rather than re-populating with the new user's info.
Right now everything is called from viewDidLoad():
override func viewDidLoad() {
super.viewDidLoad()
loadData()
loadOngoingQuest()
collectionView.dataSource = self
collectionView.delegate = self
}
loadData() and loadOngoingQuest() pull info from Firebase into arrays used to populate the collection view.
I have tried adding everything in viewWillAppear() instead, but it causes some glitches as the collection view involves scrolling and other animations that I don't want to run every time the view controller appears.
Basically I just want to reload the entire view controller every time the user logs out. How can I do this?
You should do something like this, see comments for more info:
let firebaseAuth = FIRAuth.auth()
do {
try firebaseAuth?.signOut()
// clear the array that your collectionView is based on
// reload your collectionView (which will clear all the data since your array is empty)
} catch let signOutError as NSError {
print ("Error signing out: %#", signOutError)
}
Update:
Another way of handling this:
Create a Struct with the following property:
struct Global {
var isNewUser = false
}
Whenever you logout a user do this:
Global.isNewUser = true
And in your collectionView you check if Global.isNewUser == true then reload everything and most important do set Global.isNewUser = false. That way you´ll only load everything once.
After a lot of frustration I figured out the issue!
It was nothing to do with refreshing collection views or view controllers. I was using Firebase refs wrong. I had saved two refs in as global constants in a struct in hopes of not having to declare them on every VC. It was like this:
struct GlobalConstants {
struct Refs {
static let userRef = AppDatabaseReference.users(uid: (Auth.auth().currentUser?.uid)!).reference()
static let currentUser = Auth.auth().currentUser?.uid
}
}
I would then use them anywhere in my code like this:
var questsRef = var questsRef = GlobalConstants.Refs.userRef.reference().child("quests")
This would cause issues unless I terminated the app and opened it again, refreshing these variables.
I went back to declaring let userRef = AppDatabaseReference.users(uid: (Auth.auth().currentUser?.uid)!).reference() at the top in every VC and it solved the issue!

Reload regular view controller (swift)

My view controller display the view depends on an array. the object of the array may be removed according to user interaction. So i have to reload the view controller once the user delete data in the array. is there any way to reload the regular view controller?
You should design the view according to the array in one method and call it every time the array changes.
private func setupView() {
...
self.setNeedsLayout()
}
private func deleteObject() {
myArray.removeAtIndex(2)
self.setupView()
}

NSCollectionView selection handling in Swift

Learning with Swift and I've been at this all day with little progress:
Need to know when an item in NSCollectionView is selected. The end goal is to get the item to highlight and to be able to delete it from the collection with the delete key. My NSCollectionView is bound to an ArrayController to get content and send the selection indexes, so looks like I need to be watching the ArrayController for a selection change, but don't see any helpful delegate methods there. The prototype view has a single textfield.
I was following the obj-c examples here and elsewhere (found none in Swift), but a Swift NSCollectionViewItem doesn't have the setSelected method to override. It has a selected property.
How to get informed when an NSCollectionViewItem gets selected in Swift?
The most simple solution is to override the selected property and react for example whenever it is set:
class CollectionSonaViewItem: NSCollectionViewItem {
override var isSelected: Bool {
didSet {
// set background color to indicate selection
self.view.layer?.backgroundColor = (isSelected ? NSColor.blue.cgColor : NSColor.clear.cgColor)
// do more stuff
}
}
From there on you can send a notification or call a function in your collection view class, its delegate or whatever required.