reloadData() on UICollectionView change contenteOffset with dynamics cells - swift

I just updated my app for iOS 12 and I found this problem with my collectionView. I'm using self sizing cells this way on my collectionView subclass :
if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
layout.estimatedItemSize = CGSize(width: collectionView.frame.width, height: 44)
layout.itemSize = UICollectionViewFlowLayout.automaticSize
}
When I'm calling reloadData(), the contentOffset is changing where it doesn't have to. I've tried a workaround that worked perfectly well on on iOS 11 by is overriding reloadData() :
override func reloadData() {
let oldOffset = contentOffset
super.reloadData()
layoutSubviews() // This line do the job with iOS 11.
self.setContentOffset(oldOffset, animated: false)
}
But on iOS 12 it doesn't work anymore and the contentOffset is completely broken when calling reloadData()
I've also tried to invalidateLayout() instead of layoutSubviews() but doesn't work too.
Can someone explain me why and give me a potential solution ?
Thanks

Related

how to adjust layout of tableview cells having collectionView on screen rotation on all screen sizes?

I have CollectionView embedded in TableView cells to show images posted by a user. Im targeting this app on every device from iPhone 8 to Mac and I do have screen rotation on for iPhones and iPads. This is my first time developing app for all platforms & dealing with screen rotation & after searching online I came up with many solutions for handling layout on screen rotation but none of them worked for TableView cells that contain CollectionView and are repeated.
Here I would like to know the best solutions among these for different situations and how to use them to handle CollectionView inside TableView cells.
The first solution I implemented is to handle everything by overriding viewWillLayoutSubviews() in my main View Controller and wrote something like this:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
let index = IndexPath(row: 0, section: 0)
guard let cell = tblHome.cellForRow(at: index) as? PriorityTaskTableViewCell else {
return
}
guard let flowLayout = cell.cvPriorityTask.collectionViewLayout as? UICollectionViewFlowLayout else {
return
}
flowLayout.invalidateLayout()
cell.layoutIfNeeded()
cell.heightCvPriorityTask.constant = cell.height + 40
}
I had to add cell.layoutIfNeeded() & cell.heightCvPriorityTask.constant = cell.height + 40 to update cell height after rotation. The above approach worked fine for me on every device but only for one TableView cell as you can see I can access CollectionView present in 1st row of TableView only. This approach did not help me deal with situation where I have multiple rows with CollectionView like in case of a social media feed.
Then I decided to deal with rotation in TableView Cell Subclass and came up with following code inside PriorityTaskTableViewCell:
weak var weakParent: HomeViewController?
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
cvPriorityTask.contentInset = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
cvPriorityTask.delegate = self
cvPriorityTask.dataSource = self
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
guard
let previousTraitCollection = previousTraitCollection,
self.traitCollection.verticalSizeClass != previousTraitCollection.verticalSizeClass ||
self.traitCollection.horizontalSizeClass != previousTraitCollection.horizontalSizeClass
else {
return
}
self.cvPriorityTask?.collectionViewLayout.invalidateLayout()
weakParent?.tblHome.updateConstraints()
DispatchQueue.main.async {
self.cvPriorityTask?.reloadData()
self.weakParent?.tblHome.updateConstraints()
}
}
func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
viewWillTransition(to: size, with: coordinator)
self.cvPriorityTask?.collectionViewLayout.invalidateLayout()
weakParent?.tblHome.updateConstraints()
coordinator.animate(alongsideTransition: { context in
}, completion: { context in
self.cvPriorityTask?.collectionViewLayout.invalidateLayout()
self.weakParent?.tblHome.updateConstraints()
})
}
And this is how I'm setting up CollectionView Layout:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
var width = 0.0
if traitCollection.horizontalSizeClass == .compact {
width = (weakParent?.view.frame.size.width ?? 200) * 0.88
}
else {
width = (weakParent?.view.frame.size.width ?? 200) * 0.425
}
if traitCollection.userInterfaceIdiom == .mac {
height = width * 0.75
}
else {
height = width * 1.13
}
return CGSize(width: width, height: height)
}
This is again another modified code and it seems to work perfectly for small devices like iPhone 8. But does not have any impact on larger displays on rotation. Maybe it works only for traitCollection.horizontalSizeClass == .compact If I could get this working I would have solved my issue for dealing my repeating CollectionView inside multiple rows but I have no idea why it doesn't work on iPhone 13 Pro Max or iPads.
After trying for hours I removed code and just called cvPriorityTask.collectionViewLayout.invalidateLayout() in layoutSubviews() function and it worked too for all cells so I removed all above code from Cell subclass and left with this only:
override func layoutSubviews() {
super.layoutSubviews()
cvPriorityTask.collectionViewLayout.invalidateLayout()
}
This alone made sure collectionView embedded in TableView cells were changing layout as expected but they started cutting TableView cell as main TableView wasn't being updated so I modified my code to somehow refresh TableView too and came up with something like this:
override func layoutSubviews() {
super.layoutSubviews()
cvPriorityTask.collectionViewLayout.invalidateLayout()
layoutIfNeeded()
heightCvPriorityTask.constant = height + 40
weakParent?.tblHome.rowHeight = UITableView.automaticDimension
weakParent?.tblHome.updateConstraints()
}
It did not work at all. If somehow I could update layout of the main table too from the cell subclass.
So im confused which should be right approach. The first one seemed to be most common one I find on internet but I don't know how to handle multiple cells containing CollectionViews with that approach.
Second one works only for iPhone 8 and I have no idea why. And I don't think reloading collectionView is the right approach but it works for small screens so I wouldn't mind if somehow works for large screens.
The third one seems totally wrong to me but actually works on perfectly for every device. However only cell layout is adjusted and I tried to update layout of whole tableView from Cell Subclass but no luck.
So how to deal with layout issues for CollectionView which is embedded in TableView cells? Am I on right track or there is another better way to deal with this situtaion?

White Space at the bottom of a tableview

I have a viewcontroller with a tableview inside it. At the bottom of the tableview there is some white space that I want to get rid of and be replaced by my background colour.
I have added and customised the tableview programatically so I am looking for a programatic answer, as I did not use storyboard much for this. (I had only used it to setup my TabBarController and link it to navigation and view-controllers.)
Below is the some of the code I used to configure the tableview.
private let tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .plain)
tableView.register(ProfileTableViewCell.self, forCellReuseIdentifier: ProfileTableViewCell.identifier)
tableView.backgroundColor = Constants.backgroundColor
return tableView
}()
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
tableView.frame = view.bounds
}
Im sure the answer is probably just a single line of code, but I couldn't find one that worked. Thanks in advance!
***EDIT
I have found a solution, but I still think there is a better way to solve this problem. The code below sets a UIView as the background of the tableview, then changes the colour of the UIView.
private let tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .plain)
tableView.register(ProfileTableViewCell.self, forCellReuseIdentifier: ProfileTableViewCell.identifier)
let backgroundView = UIView(frame: CGRect(x: 0,
y: 0,
width: tableView.bounds.size.width,
height: tableView.bounds.size.height))
backgroundView.backgroundColor = Constants.backgroundColor
tableView.backgroundView = backgroundView
return tableView
}()
The default color of the containing view controller is showing, since the table is short. Try setting the view controller's view's background color in viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad();
view.backgroundColor = Constants.backgroundColor
}

UITableViewCell width not expanding on rotation to landscape

I'm having trouble resizing the content of a UITableViewCell when the device rotates to landscape (and therefore view width increases).
For context, this is part of a universal split view app and is only occurring on iPhone 8 in the simulator (which doesn't support split view). Later devices which do support the split view have no issue.
In my UITableViewController, translatesAutoresizingMaskIntoConstraints = false, and 'Follow Readable Width' is unchecked in IB. I have also added:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
self.tableView.reloadData()
}
The tableView also has a custom UITableViewCell to which I've added:
override func layoutSubviews() {
uniqueIDLeading.constant = (self.contentView.frame.width * 0.4)
layoutIfNeeded()
}
No errors or warnings in the console. Any other ideas?
Eventually fixed with the following in custom UITableViewCell class.
override func layoutSubviews() {
super.layoutSubviews()
autoresizingMask = .flexibleWidth
layoutIfNeeded()
}
I have the similar issue today, but I am not using a custom cell. I solved it with the following code.
Swift:
self.tableView.autoresizingMask = .flexibleWidth
Objective-C:
self.tableView.autoresizingMask=UIViewAutoresizingFlexibleWidth;

scrollViewDidScroll called with strange contentOffsetY

I have a UIViewController that holds a NSFetchedResultsController. After the insertion of rows to the top, I want to keep the rows visible as they where before the insertions. This means I need to make some calculations to keep the contentOffSetY right after the update. The calculation is correct, but I noticed that scrollViewDidScroll gets called after it scrolled to my specified contentOffsetY, this results in a corrupted state. This is the logging:
Will apply an corrected Y value of: 207.27359771728516
Scrolled to: 207.5
Corrected to: 207.27359771728516
Scrolled to: 79.5 <-- Why is this logline here?
You can directly clone an example project: https://github.com/Jasperav/FetchResultControllerGlitch (commit https://github.com/Jasperav/FetchResultControllerGlitch/commit/d46054040139afeeb648e1e0b5b113bd98685b4a, the newest version of the project only glitches, the weird call to the scrollViewDidScroll method is now gone. If you fix the glitch I award the bounty. Just clone the newest version, run it and scroll a little bit. You will see strange content offset's (glitches)). Run the project and you will see the strange output. This is the controllerDidChangeContent method:
public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
let currentSize = tableView.contentSize.height
UIView.performWithoutAnimation {
tableView.endUpdates()
let newSize = tableView.contentSize.height
let correctedY = tableView.contentOffset.y + newSize - currentSize
print("Will apply an corrected Y value of: \(correctedY)")
tableView.setContentOffset(CGPoint(x: 0,
y: correctedY),
animated: false)
print("Corrected to: \(correctedY)")
}
}
If I call tableView.layoutIfNeeded right after the tableView.endUpdates(), the delegate is already called. What does it cause to call the delegate method? Is there any way it does not scroll?
I downloaded your code and made some tweaks to fix the glitching issue. Here is what I have done.
set estimatedRowHeight property of table view to some number
init() {
super.init(frame: .zero, style: .plain)
estimatedRowHeight = 50.0
register(MyTableViewCell.self, forCellReuseIdentifier: "cell")
}
created a function to handle the UITableView reload action without modifying the current contentOffset
func reloadDataWithoutScroll() {
let lastScrollOffset = contentOffset
beginUpdates()
reloadData()
layoutIfNeeded()
endUpdates()
layer.removeAllAnimations()
setContentOffset(lastScrollOffset, animated: false)
}
Updated controllerDidChangeContent function to make use of the reloadDataWithoutScroll function
UIView.performWithoutAnimation {
tableView.performBatchUpdates({
tableView.insertRows(at: inserts, with: .automatic)
})
inserts.removeAll()
tableView.reloadDataWithoutScroll()
}
When I execute with these changes, it doesn't scroll when a new row is added. However, it does scroll to show the new added row when the current contentOffset is 0. And I don't think that would be a problem, logically speaking.

Autolayout CollectionView

I am trying to use autolayout for a CollectionVIew, but when I run it in iPhone plus, a space is created between the cells.
iPhone 8 Plus
iPhone 8
I guess you already set the cell size from collection view's delegate method. One thing you need to do is to call invalidateLayout() whenever the collection view's frame changes, so it will re-render the cells.
IE:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
collectionView.collectionViewLayout.invalidateLayout()
}
I solved it by setting in ViewDidLoad the size of the cell (I just set the width, but you could set the height too)
let layout = YourCollectionView.collectionViewLayout as? UICollectionViewFlowLayout
layout?.estimatedItemSize = CGSize(width: view.bounds.width / 2 , height: 114)