UICollectionView destroyed in UITableViewCell - ios

how to clean UITableViewCell before reuse ? In cellForRow table view I set up viewModel and in didSet this View Model i change collectionView height constraint but when I scroll very fast then collection is totaly destroyed.
func setup(collectionView: UICollectionView) {
if self.collectionView != collectionView || self.collectionView == nil {
collectionView.register(UINib(nibName: "HomeRecommendationCollectionViewCell", bundle: nil),
forCellWithReuseIdentifier: HomeStoryboardConsts.Identifier.homeRecommendationCollectionViewCell.rawValue)
collectionView.contentInset = UIEdgeInsetsMake(0, 10, 0, 10)
if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
layout.minimumInteritemSpacing = 10
}
disposeBag = DisposeBag()
items.asObservable()
.subscribe(onNext: { [weak collectionView] _ in
collectionView?.reloadData()
})
.addDisposableTo(disposeBag)
self.collectionView = collectionView
}
}
collection View setup
viewModel Setup:
var viewModel: TitleAccessoryButtonCollectionViewModel? {
didSet {
guard let viewModel = viewModel else {
return
}
titleLabel.text = viewModel.title
if let buttonTitle = viewModel.accessoryButtonModel?.title {
setAccessoryButtonTitle(buttonTitle)
}else{
accessoryButton.hideTitleLabel()
}
if let buttonImage = viewModel.accessoryButtonModel?.image {
accessoryButton.buttonImageView.image = buttonImage
}
else {
accessoryButton.hideImageView()
}
sectionContentImage.image = viewModel.sectionContentImage
titleLabelLeadingConstraint.constant = viewModel.titleLabelLeadingSpacing
accessoryButton.isHidden = viewModel.hideAccessoryButton
sectionContentView.isHidden = viewModel.hidePremiumContentView
let collectionViewModel = viewModel.collectionViewModel
collectionViewHeight.constant = CGFloat(collectionViewModel.height)
collectionViewModel.setup(collectionView: collectionView)
collectionView.delegate = collectionViewModel.delegate
collectionView.dataSource = collectionViewModel.dataSource
collectionView.reloadData()
}
}
i have also in console
2018-09-19 14:27:52.335285+0200 App[76102:8433084] The behavior of the UICollectionViewFlowLayout is not defined because:
2018-09-19 14:27:52.335409+0200 App[76102:8433084] the item height must be less than the height of the UICollectionView minus the section insets top and bottom values, minus the content insets top and bottom values.

If you have a custom tableviewcell class for cells of the tableview. Then there is a method called prepareForReuse(). Use that method to cleanup a cell and prepare it for the new layout.

Related

UICollectionView How to make a cell size itself dynamically based on its UIHostingConfiguration?

I have made an UICollectionView in which you can double tap a cell to resize it.
I'm using a CompositionalLayout, a DiffableDataSource and the new UIHostingConfiguration hosting a SwiftUI View which depends on an ObservableObject.
The resizing is triggered by updating the height property of the ObservableObject. That causes the SwiftUI View to change its frame which leads to the collectionView automatically resizing the cell. The caveat is that it does so immediately without animation only jumping between the old and the new frame of the view.
The ideal end-goal would be to be able to add a .animation() modifier to the SwiftUI View that then determines animation for both view and cell. Doing so now without additional setup makes the SwiftUI View animate but not the cell.
Is there a way to make the cell (orange) follow the size of the view (green) dynamically?
The proper way to manipulate the cell animation (as far as I known) is to override initialLayoutAttributesForAppearingItem() and finalLayoutAttributesForDisappearingItem() but since the cell just changes and doesn't appear/disappear they don't have an effect.
One could also think of Auto Layout constraints to archive this but I don’t think they are usable with UIHostingConfiguration?
I've also tried:
subclassing UICollectionViewCell and overriding apply(_ layoutAttributes: UICollectionViewLayoutAttributes) but it only effects the orange cell-background on initial appearance.
to put layout.invalidateLayout() or collectionView.layoutIfNeeded() inside UIView.animate() but it does not seem to have an effect on the size change.
Any thoughts, hints, ideas are greatly appreciated ✌️ Cheers!
Here is the code I used for the first gif:
struct CellContentModel {
var height: CGFloat? = 100
}
class CellContentController: ObservableObject, Identifiable {
let id = UUID()
#Published var cellContentModel: CellContentModel
init(cellContentModel: CellContentModel) {
self.cellContentModel = cellContentModel
}
}
class DataStore {
var data: [CellContentController]
var dataById: [CellContentController.ID: CellContentController]
init(data: [CellContentController]) {
self.data = data
self.dataById = Dictionary(uniqueKeysWithValues: data.map { ($0.id, $0) } )
}
static let testData = [
CellContentController(cellContentModel: CellContentModel()),
CellContentController(cellContentModel: CellContentModel(height: 80)),
CellContentController(cellContentModel: CellContentModel())
]
}
class CollectionViewController: UIViewController {
enum Section {
case first
}
var dataStore = DataStore(data: DataStore.testData)
private var layout: UICollectionViewCompositionalLayout!
private var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, CellContentController.ID>!
override func loadView() {
createLayout()
createCollectionView()
createDataSource()
view = collectionView
}
}
// - MARK: Layout
extension CollectionViewController {
func createLayout() {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(50))
let Item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.8), heightDimension: .estimated(300))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [Item])
let section = NSCollectionLayoutSection(group: group)
layout = .init(section: section)
}
}
// - MARK: CollectionView
extension CollectionViewController {
func createCollectionView() {
collectionView = .init(frame: .zero, collectionViewLayout: layout)
let doubleTapGestureRecognizer = DoubleTapGestureRecognizer()
doubleTapGestureRecognizer.doubleTapAction = { [unowned self] touch, _ in
let touchLocation = touch.location(in: collectionView)
guard let touchedIndexPath = collectionView.indexPathForItem(at: touchLocation) else { return }
let touchedItemIdentifier = dataSource.itemIdentifier(for: touchedIndexPath)!
dataStore.dataById[touchedItemIdentifier]!.cellContentModel.height = dataStore.dataById[touchedItemIdentifier]!.cellContentModel.height == 100 ? nil : 100
}
collectionView.addGestureRecognizer(doubleTapGestureRecognizer)
}
}
// - MARK: DataSource
extension CollectionViewController {
func createDataSource() {
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewCell, CellContentController.ID>() { cell, indexPath, itemIdentifier in
let cellContentController = self.dataStore.dataById[itemIdentifier]!
cell.contentConfiguration = UIHostingConfiguration {
TextView(cellContentController: cellContentController)
}
.background(.orange)
}
dataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier)
}
var initialSnapshot = NSDiffableDataSourceSnapshot<Section, CellContentController.ID>()
initialSnapshot.appendSections([Section.first])
initialSnapshot.appendItems(dataStore.data.map{ $0.id }, toSection: Section.first)
dataSource.applySnapshotUsingReloadData(initialSnapshot)
}
}
class DoubleTapGestureRecognizer: UITapGestureRecognizer {
var doubleTapAction: ((UITouch, UIEvent) -> Void)?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
if touches.first!.tapCount == 2 {
doubleTapAction?(touches.first!, event)
}
}
}
struct TextView: View {
#StateObject var cellContentController: CellContentController
var body: some View {
Text(cellContentController.cellContentModel.height?.description ?? "nil")
.frame(height: cellContentController.cellContentModel.height, alignment: .top)
.background(.green)
}
}

Reload CollectionView Cell with custom UICollectionViewFlowLayout

I have a custom UICollectionViewFlowLayout which lays out items with a left-aligned format.
LAYOUT
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var newAttributesArray = [UICollectionViewLayoutAttributes]()
let superAttributesArray = super.layoutAttributesForElements(in: rect)!
guard let attributesToReturn = superAttributesArray.map( { $0.copy() }) as? [UICollectionViewLayoutAttributes] else {
return nil
}
for (index, attributes) in attributesToReturn.enumerated() {
if index == 0 || attributesToReturn[index - 1].frame.origin.y != attributes.frame.origin.y {
attributes.frame.origin.x = sectionInset.left
} else {
let previousAttributes = attributesToReturn[index - 1]
let previousFrameRight = previousAttributes.frame.origin.x + previousAttributes.frame.width
attributes.frame.origin.x = previousFrameRight + minimumInteritemSpacing
}
newAttributesArray.append(attributes)
}
return attributesToReturn
}
When I reload a cell that is not the first in the horizontal line, the cell tried performs peculiarly is in the below illustration. I believe this is a layout issue. Reloading a cell that's first does not act this way.
RELOAD METHOD
func selectedInterest(for cell: InterestCell) {
guard let indexPath = mainView.collectionView.indexPath(for: cell),
let documentID = cell.interest?.documentID,
let isSaved = cell.interest?.isSaved else { return }
var interest = self.interests[indexPath.item]
interest.isSaved = !isSaved
self.interests[indexPath.item] = interest
self.mainView.collectionView.collectionViewLayout.invalidateLayout()
self.mainView.collectionView.reloadItems(at: [indexPath])
}
I have tried to invalidate the layout before reloading however this has no effect.

UICollectionView jumps/scrolls when a nested UITextView begins editing

My first question on SO so bear with me. I have created a UICollectionViewController which has a header and 1 cell. Inside the cell is a tableview, inside the table view there are multiple static cells. One of those has a horizontal UICollectionView with cells which have UITextViews.
Problem: When tapping on a UITextView the collection view scrolls/jumps
Problem Illustration
On the right you can see the y offset values. On first tap it changes to 267 -- the header hight. On a consecutive tap it goes down to 400 -- the very bottom. This occurs no matter what I tried to do.
Note: Throughout my app I'am using IQKeyboardManager
What have I tried:
Disabling IQKeyboardManager completely and
Taping on text view
Replacing it with a custom keyboard management methods based on old SO answers
Set collectionView.shouldIgnoreScrollingAdjustment = true for:
all scrollable views in VC
Individuals scrollable views
Note: this property originates from the IQKeyboardManager Library and as far as I understand it is supposed to disable scroll adjustment offset.
Tried disabling scroll completely in viewDidLoad() as well as all other places within this VC. I used:
collectionView.isScrollEnabled = false
collectionView.alwaysBounceVertical = false
Notably, I have tried disabling scroll in text viewDidBeginEditing as well as the custom keyboard management methods.
My Code:
The main UICollectionView and its one cell are created in the storyboard. Everything else is done programatically. Here is the flow layout function that dictates the size of the one and only cell:
extension CardBuilderCollectionViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout:
UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let height = view.frame.size.height
let width = view.frame.size.width
return CGSize(width: width * cellWidthScale, height: height * cellHeigthScale)
}
}
Additionally, collectionView.contentInsetAdjustmentBehavior = .never
The TableView within the subclass of that one cell is created like so:
let tableView: UITableView = {
let table = UITableView()
table.estimatedRowHeight = 300
table.rowHeight = UITableView.automaticDimension
return table
}()
and:
override func awakeFromNib() {
super.awakeFromNib()
dataProvider = DataProvider(delegate: delegate)
addSubview(tableView)
tableView.fillSuperview() // Anchors to 4 corners of superview
registerCells()
tableView.delegate = dataProvider
tableView.dataSource = dataProvider
}
The cells inside the table view are all subclasses of class GeneralTableViewCell, which contains the following methods which determine the cells height:
var cellHeightScale: CGFloat = 0.2 {
didSet {
setContraints()
}
}
private func setContraints() {
let screen = UIScreen.main.bounds.height
let heightConstraint = heightAnchor.constraint(equalToConstant: screen*cellHeightScale)
heightConstraint.priority = UILayoutPriority(999)
heightConstraint.isActive = true
}
The height of the nested cells (with TextView) residing in the table view is determined using the same method as the one and only cell in the main View.
Lastly the header is created using a custom FlowLayout:
class StretchyHeaderLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let layoutAttributes = super.layoutAttributesForElements(in: rect)
layoutAttributes?.forEach({ (attribute) in
if attribute.representedElementKind == UICollectionView.elementKindSectionHeader && attribute.indexPath.section == 0 {
guard let collectionView = collectionView else { return }
attribute.zIndex = -1
let width = collectionView.frame.width
let contentOffsetY = collectionView.contentOffset.y
print(contentOffsetY)
if contentOffsetY > 0 { return }
let height = attribute.frame.height - contentOffsetY
attribute.frame = CGRect(x: 0, y: contentOffsetY, width: width, height: height)
}
})
return layoutAttributes
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
}
This is my first time designing a complex layout with mostly a programatic approach. Hence it is possible that I missed something obvious. However, despite browsing numerous old questions I was not able to find a solution. Any solutions or guidance is appreciated.
Edit:
As per request here are the custom keyboard methods:
In viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
Then:
var scrollOffset : CGFloat = 0
var distance : CGFloat = 0
var activeTextFeild: UITextView?
var safeArea: CGRect?
#objc func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
var safeArea = self.view.frame
safeArea.size.height += collectionView.contentOffset.y
safeArea.size.height -= keyboardSize.height + (UIScreen.main.bounds.height*0.04)
self.safeArea = safeArea
}
}
private func configureScrollView() {
if let activeField = activeTextFeild {
if safeArea!.contains(CGPoint(x: 0, y: activeField.frame.maxY)) {
print("No need to Scroll")
return
} else {
distance = activeField.frame.maxY - safeArea!.size.height
scrollOffset = collectionView.contentOffset.y
self.collectionView.setContentOffset(CGPoint(x: 0, y: scrollOffset + distance), animated: true)
}
}
// prevent scrolling while typing
collectionView.isScrollEnabled = false
collectionView.alwaysBounceVertical = false
}
#objc func keyboardWillHide(notification: NSNotification) {
if distance == 0 {
return
}
// return to origin scrollOffset
self.collectionView.setContentOffset(CGPoint(x: 0, y: scrollOffset), animated: true)
scrollOffset = 0
distance = 0
collectionView.isScrollEnabled = true
}
Finaly:
//MARK: - TextViewDelegate
extension CardBuilderCollectionViewController: UITextViewDelegate {
func textViewDidBeginEditing(_ textView: UITextView) {
self.activeTextFeild = textView
configureScrollView()
}
}
The problem that I can see is you calling configureScrollView() when your textView is focused in textViewDidBeginEditing .
distance = activeField.frame.maxY - safeArea!.size.height
scrollOffset = collectionView.contentOffset.y
self.collectionView.setContentOffset(CGPoint(x: 0, y: scrollOffset + distance), animated: true)
You're calling collectionView.setContentOffset --> so that's why your collection view jumping.
Please check your distance calculated correctly or not. Also, your safeArea was modified when keyboardWillShow.
Try to disable setCOntentOffset?

Self-sizing cells with UICollectionViewCompositionalLayout

I have a view controller that displays a collection view with self-sizing cells. The collection view has one section that scrolls horizontally. It looks like this:
Problem
The collection view behaves unexpectedly when the view controller is presented using the default pageSheet style on iOS 13+.
When pulling upward on the sheet, cells may appear to resize like the cell labeled "Rectify" below:
When pulling upward on the sheet, the content may shift horizontally. Sometimes, cells may disappear too:
Question
Is there a way to fix this behavior while still using UICollectionViewCompositionalLayout and the pageSheet presentation style?
Code Summary
The code is pretty straightforward. Just 3 classes, which can be dropped into the ViewController.swift file using the Single View App project template in Xcode.
A UICollectionViewCell class called Cell. The cell has a UILabel and overrides sizeThatFits(_:).
A UIViewController called ViewController used only to present BugViewController.
BugViewController, which configures the data source and presents the collection view. This is where the problem occurs.
Code
import UIKit
// MARK: - Cell -
final class Cell: UICollectionViewCell {
static let reuseIdentifier = "Cell"
lazy var label: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.frame.size = contentView.bounds.size
label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(label)
contentView.backgroundColor = .tertiarySystemFill
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
.init(width: label.sizeThatFits(size).width + 32, height: 32)
}
}
// MARK: - ViewController -
final class ViewController: UIViewController {
private let button: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Tap Me!".uppercased(), for: .normal)
button.addTarget(self, action: #selector(presentBugViewController), for: .touchUpInside)
button.sizeToFit()
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(button)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
button.center = view.center
}
#objc func presentBugViewController() {
present(BugViewController(), animated: true)
}
}
// MARK: - BugViewController -
final class BugViewController: UIViewController {
private let models = [
"Better Call Saul",
"Mad Men",
"Rectify",
"Tiger King: Murder, Mayhem, and Madness",
"Master of None",
"BoJack Horseman"
]
private lazy var collectionView: UICollectionView = {
let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createCollectionViewLayout())
collectionView.register(Cell.self, forCellWithReuseIdentifier: Cell.reuseIdentifier)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.contentInset.top = 44
collectionView.backgroundColor = .white
return collectionView
}()
private lazy var dataSource = UICollectionViewDiffableDataSource<Int, String>(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.reuseIdentifier, for: indexPath) as? Cell else { fatalError() }
cell.label.text = itemIdentifier
return cell
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(collectionView)
var snapshot = NSDiffableDataSourceSnapshot<Int, String>()
snapshot.appendSections([0])
snapshot.appendItems(models)
dataSource.apply(snapshot)
}
private func createCollectionViewLayout() -> UICollectionViewCompositionalLayout {
let layoutSize = NSCollectionLayoutSize.init(
widthDimension: .estimated(200),
heightDimension: .absolute(32)
)
let section = NSCollectionLayoutSection(group:
.horizontal(
layoutSize: layoutSize,
subitems: [.init(layoutSize: layoutSize)]
)
)
section.interGroupSpacing = 8
section.orthogonalScrollingBehavior = .continuous
return .init(section: section)
}
}
Notes
The collection view in my app actually has many sections and scrolls vertically. That is why I'm using a vertically scrolling collection view and a section with orthogonalScrollingBehavior in the example code.
Failed Attempts
I've tried using Auto Layout constraints instead of sizeThatFits(_:).
I've tried not using UICollectionViewDiffableDataSource.
Workarounds
Modifying the cell with a child scroll view and passing in an array of strings (as opposed to one at a time) does avoid this problem. But, it's a dirty hack that I'd like to avoid if possible.

View not removed from its superview in iOS

I have a UIView and a collectionView. If there is an internet connection I want to hide the collectionView and show the UIView, if not otherwise.
class MyClass{
#IBOutlet weak var collectionView: UICollectionView!
var myView : CustomView?
....
func internetStatusChanegd(){
if(isOnline){
collectionView.isHidden = true
if let viewNib = UIView.loadFromNibNamed("CustomView", bundle: Bundle.main) as? CustomView {
myView = viewNib
myView!.frame = self.view.bounds
self.view.addSubview(myView!)
}
}else{
if let customView = myView{
customView.removeFromSuperview()
}
collectionView.isHidden = false
}
}
}
removeFromSuperview() Is called but the view is not removed from the view. Do you have an idea about the problem?
While adding a sub view in my view give a tag to that view.
Iterate the for loop for subviews in view.
While removing just check if it's the view with same tag then call-
self.removeFromSuperview()
Please remove already available view before you are adding new view.
func internetStatusChanegd() {
if(isOnline) {
collectionView.isHidden = true
for subView in (self.view.subviews)! {
if (subView.tag == 100) {
subView.removeFromSuperview() //this will remove already available object form self.view
}
}
if let viewNib = UIView.loadFromNibNamed("CustomView", bundle: Bundle.main) as? CustomView {
myView = viewNib
myView!.frame = self.view.bounds
myView.tag = 100 //add tag when you create object
self.view.addSubview(myView!)
}
}else{
if let customView = myView{
customView.removeFromSuperview()
}
collectionView.isHidden = false}
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if isOnline{
collectionView.backgroundView = myView //your custom view whatever you want to show here like : button..
return 0
}
collectionView.backgroundView = nil
return array.count
}
func internetStatusChanegd(){ collectionView.reloadData() } //it'll handle automatically that view .
Try this
Remove like this
for subview in self.view.subviews{
if subview is CustomView
{
subview.removeFromSuperview()
}
}

Resources