In our app we have an infinite (vertical) scroll where we need pagination (snapping to cells - of different heights), as this seemed to be very tedious to implement with SwiftUI, we used this gist that wraps a UICollectionView with a UIViewControllerRepresentable. Since the scroll is infite, further data is appended to the initial data once the user reaches almost the end.
This all works well, except: when the data array gets updated, the scrolling animation (either the snapping or normal scroll) stops at once, without finishing the animation. This also happens when updating the data using withAnimation:
withAnimation {
// update data
}
My suspicion is that this is due to the way the UICollectionView is wrapped since when using the normal ScrollView of SwiftUI it all works fine.
How can I insert data to the UICollectionView without breaking the scrolling animation? Or is there a better way to wrap it than with the gist linked?
P.S. using some complex SwiftUI solution is also not ideal since we need a custom Scroll to refresh functionality, which would make things even more complex in SwiftUI but has been implemented very easily with the Representable.
MRE (source of wrapper: this gist):
import SwiftUI
import UIKit
struct Model: Identifiable {
let id = UUID().uuidString
}
class PostService: ObservableObject {
#Published var posts: [Model] = []
init() {
self.posts = [Model(), Model(), Model()]
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
print("adding data")
self.addData()
}
}
func addData() {
Task { // this is because in the real example there are async calls in here...
let newData = Model()
DispatchQueue.main.async {
withAnimation {
self.pooledPosts.append(newData)
}
}
}
}
}
struct ContentView: View {
#StateObject var postService: PostService
var body: some View {
CollectionView(
collection: postService.posts,
scrollDirection: .vertical,
contentSize: .crossAxisFilled(mainAxisLength: UIScreen.main.bounds.height - 100),
itemSpacing: .init(mainAxisSpacing: 0, crossAxisSpacing: 0),
rawCustomize: { collectionView in
collectionView.showsVerticalScrollIndicator = false
//collectionView.isPagingEnabled = true
// no matter what this is set to the issue is present
},
contentForData: MyCustomCell.init)
}
}
struct MyCustomCell : View {
let data: Model
var body: some View {
ZStack(alignment: .center) {
Text(self.data.id)
.font(.system(size: 24))
.foregroundColor(Color(hex: 0x000000))
.fontWeight(.black)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.blue.cornerRadius(14))
}
}
struct CollectionView
<Collections, CellContent>
: UIViewControllerRepresentable
where
Collections : RandomAccessCollection,
Collections.Index == Int,
Collections.Element : RandomAccessCollection,
Collections.Element.Index == Int,
Collections.Element.Element : Identifiable,
CellContent : View
{
typealias Row = Collections.Element
typealias Data = Row.Element
typealias ContentForData = (Data) -> CellContent
typealias ScrollDirection = UICollectionView.ScrollDirection
typealias SizeForData = (Data) -> CGSize
typealias CustomSizeForData = (UICollectionView, UICollectionViewLayout, Data) -> CGSize
typealias RawCustomize = (UICollectionView) -> Void
enum ContentSize {
case fixed(CGSize)
case variable(SizeForData)
case crossAxisFilled(mainAxisLength: CGFloat)
case custom(CustomSizeForData)
}
struct ItemSpacing : Hashable {
var mainAxisSpacing: CGFloat
var crossAxisSpacing: CGFloat
}
fileprivate let collections: Collections
fileprivate let contentForData: ContentForData
fileprivate let scrollDirection: ScrollDirection
fileprivate let contentSize: ContentSize
fileprivate let itemSpacing: ItemSpacing
fileprivate let rawCustomize: RawCustomize?
init(
collections: Collections,
scrollDirection: ScrollDirection = .vertical,
contentSize: ContentSize,
itemSpacing: ItemSpacing = ItemSpacing(mainAxisSpacing: 0, crossAxisSpacing: 0),
rawCustomize: RawCustomize? = nil,
contentForData: #escaping ContentForData)
{
self.collections = collections
self.scrollDirection = scrollDirection
self.contentSize = contentSize
self.itemSpacing = itemSpacing
self.rawCustomize = rawCustomize
self.contentForData = contentForData
}
func makeCoordinator() -> Coordinator {
return Coordinator(view: self)
}
func makeUIViewController(context: Context) -> ViewController {
let coordinator = context.coordinator
let viewController = ViewController(coordinator: coordinator, scrollDirection: self.scrollDirection)
coordinator.viewController = viewController
self.rawCustomize?(viewController.collectionView)
return viewController
}
func updateUIViewController(_ uiViewController: ViewController, context: Context) {
// TODO: Obviously we can be efficient about what needs to be updated here
context.coordinator.view = self
uiViewController.layout.scrollDirection = self.scrollDirection
self.rawCustomize?(uiViewController.collectionView)
uiViewController.collectionView.reloadData()
}
}
extension CollectionView {
/*
Convenience init for a single-section CollectionView
*/
init<Collection>(
collection: Collection,
scrollDirection: ScrollDirection = .vertical,
contentSize: ContentSize,
itemSpacing: ItemSpacing = ItemSpacing(mainAxisSpacing: 0, crossAxisSpacing: 0),
rawCustomize: RawCustomize? = nil,
contentForData: #escaping ContentForData) where Collections == [Collection]
{
self.init(
collections: [collection],
scrollDirection: scrollDirection,
contentSize: contentSize,
itemSpacing: itemSpacing,
rawCustomize: rawCustomize,
contentForData: contentForData)
}
}
extension CollectionView {
fileprivate static var cellReuseIdentifier: String {
return "HostedCollectionViewCell"
}
}
extension CollectionView {
final class ViewController : UIViewController {
fileprivate let layout: UICollectionViewFlowLayout
fileprivate let collectionView: UICollectionView
init(coordinator: Coordinator, scrollDirection: ScrollDirection) {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = scrollDirection
self.layout = layout
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.backgroundColor = nil
collectionView.register(HostedCollectionViewCell.self, forCellWithReuseIdentifier: cellReuseIdentifier)
collectionView.dataSource = coordinator
collectionView.delegate = coordinator
self.collectionView = collectionView
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("In no way is this class related to an interface builder file.")
}
override func loadView() {
self.view = self.collectionView
}
}
}
extension CollectionView {
final class Coordinator : NSObject, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
fileprivate var view: CollectionView
fileprivate var viewController: ViewController?
init(view: CollectionView) {
self.view = view
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return self.view.collections.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.view.collections[section].count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellReuseIdentifier, for: indexPath) as! HostedCollectionViewCell
let data = self.view.collections[indexPath.section][indexPath.item]
let content = self.view.contentForData(data)
cell.provide(content)
return cell
}
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
let cell = cell as! HostedCollectionViewCell
cell.attach(to: self.viewController!)
}
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
let cell = cell as! HostedCollectionViewCell
cell.detach()
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
switch self.view.contentSize {
case .fixed(let size):
return size
case .variable(let sizeForData):
let data = self.view.collections[indexPath.section][indexPath.item]
return sizeForData(data)
case .crossAxisFilled(let mainAxisLength):
switch self.view.scrollDirection {
case .horizontal:
return CGSize(width: mainAxisLength, height: collectionView.bounds.height)
case .vertical:
fallthrough
#unknown default:
return CGSize(width: collectionView.bounds.width, height: mainAxisLength)
}
case .custom(let customSizeForData):
let data = self.view.collections[indexPath.section][indexPath.item]
return customSizeForData(collectionView, collectionViewLayout, data)
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return self.view.itemSpacing.mainAxisSpacing
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return self.view.itemSpacing.crossAxisSpacing
}
}
}
private extension CollectionView {
final class HostedCollectionViewCell : UICollectionViewCell {
var viewController: UIHostingController<CellContent>?
func provide(_ content: CellContent) {
if let viewController = self.viewController {
viewController.rootView = content
} else {
let hostingController = UIHostingController(rootView: content)
hostingController.view.backgroundColor = nil
self.viewController = hostingController
}
}
func attach(to parentController: UIViewController) {
let hostedController = self.viewController!
let hostedView = hostedController.view!
let contentView = self.contentView
parentController.addChild(hostedController)
hostedView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(hostedView)
hostedView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
hostedView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
hostedView.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
hostedView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
hostedController.didMove(toParent: parentController)
}
func detach() {
let hostedController = self.viewController!
guard hostedController.parent != nil else { return }
let hostedView = hostedController.view!
hostedController.willMove(toParent: nil)
hostedView.removeFromSuperview()
hostedController.removeFromParent()
}
}
}
Related
I am trying to create a custom transition, similar to the one that is on the App Store. I used these tutorials:
https://eric-dockery283.medium.com/custom-view-transitions-like-the-new-app-store-a2a1181229b6
https://www.raywenderlich.com/2925473-ios-animation-tutorial-custom-view-controller-presentation-transitions
I have also been experimenting with using the load view to have the root view to create the view, similar to what is used in the answer used here: does loadView get called even if we don't override it in viewcontroller like the other ViewController lifecycle methods?
now what I am doing is that I am using a collectionview, which I want to keep, and when an item is pressed it animate transitions to a new view controller with a larger image, simple.
the problem is that when I combine it with using the loadView with view = rootView() it did not work in some scenarios.
For example:
when I use the modalPresentationStyle = .fullScreen, when I press an item in the collection view the screen just goes black, and then when I press the view hierarchy it shows this:
when I remove the modalPresentationStyle = .fullScreen it does animate but it is the popover transition, which I do not want.
What I want is to have a full screen transition that is animated.
here is the view controller:
class ViewController: UIViewController {
let data = createData()
var transition = PopAnimator()
var rootView = MainView()
private let collectionView: UICollectionView = {
let viewLayout = UICollectionViewFlowLayout()
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: viewLayout)
collectionView.backgroundColor = .white
return collectionView
}()
private enum LayoutConstant {
static let spacing: CGFloat = 16.0
static let itemHeight: CGFloat = 200.0
}
override func loadView() {
view = rootView
rootView.collectionView.delegate = self
rootView.collectionView.dataSource = self
}
}
class MainView: UIView {
let collectionView: UICollectionView = {
let viewLayout = UICollectionViewFlowLayout()
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: viewLayout)
collectionView.backgroundColor = .white
return collectionView
}()
init() {
super.init(frame: .zero)
setupLayouts()
}
private func setupLayouts() {
backgroundColor = .white
addSubview(collectionView)
collectionView.register(CollectionViewCell.self, forCellWithReuseIdentifier: CollectionViewCell.identifier)
collectionView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor),
collectionView.leftAnchor.constraint(equalTo: safeAreaLayoutGuide.leftAnchor),
collectionView.rightAnchor.constraint(equalTo: safeAreaLayoutGuide.rightAnchor)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return data.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CollectionViewCell.identifier, for: indexPath) as! CollectionViewCell
cell.dogImg.image = UIImage(named: data[indexPath.row].image)
return cell
}
}
extension ViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let selected = data[indexPath.row]
let vc = OtherViewController()
vc.transitioningDelegate = self
// vc.modalPresentationStyle = .fullScreen
vc.data = selected
self.present(vc, animated: true)
print(selected)
}
}
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = itemWidth(for: view.frame.width, spacing: LayoutConstant.spacing)
return CGSize(width: width, height: LayoutConstant.itemHeight)
}
func itemWidth(for width: CGFloat, spacing: CGFloat) -> CGFloat {
let itemsInRow: CGFloat = 2
let totalSpacing: CGFloat = 2 * spacing + (itemsInRow - 1) * spacing
let finalWidth = (width - totalSpacing) / itemsInRow
return floor(finalWidth)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: LayoutConstant.spacing, left: LayoutConstant.spacing, bottom: LayoutConstant.spacing, right: LayoutConstant.spacing)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return LayoutConstant.spacing
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return LayoutConstant.spacing
}
}
extension ViewController: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
guard let selectedIndexPathCell = self.collectionView.indexPathsForSelectedItems?.first,
let selectedItem = self.collectionView.cellForItem(at: selectedIndexPathCell) as? CollectionViewCell
else { return nil }
guard let originFrame = selectedItem.superview?.convert(selectedItem.frame, to: nil) else {
return transition
}
transition.originFrame = originFrame
transition.presenting = true
return transition
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transition.presenting = false
return transition
}
}
this is the other view that is presented to.
class OtherViewController: UIViewController, UIViewControllerTransitioningDelegate {
var data: DogArr?
var rootView = testView1()
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .systemBackground
}
override func loadView() {
view = rootView
//rootView.inputViewController?.transitioningDelegate = self
rootView.carImg.image = UIImage(named: data?.image ?? "")
rootView.dismissBtn.addTarget(self, action: #selector(dismissingBtn), for: .touchUpInside)
}
#objc func dismissingBtn() {
self.dismiss(animated: true)
}
}
class testView1: UIView {
var carImg: UIImageView = {
let img = UIImageView()
img.contentMode = .scaleAspectFit
img.translatesAutoresizingMaskIntoConstraints = false
return img
}()
var dismissBtn: UIButton = {
let btn = UIButton()
btn.translatesAutoresizingMaskIntoConstraints = false
btn.setTitle("Done", for: .normal)
btn.titleLabel?.font = UIFont.systemFont(ofSize: 25)
btn.titleLabel?.textAlignment = .center
btn.setTitleColor(.label, for: .normal)
btn.contentMode = .scaleAspectFill
return btn
}()
init() {
super.init(frame: .zero)
self.addSubview(carImg)
self.addSubview(dismissBtn)
carImg.translatesAutoresizingMaskIntoConstraints = false
self.backgroundColor = .systemBackground
NSLayoutConstraint.activate([
carImg.topAnchor.constraint(equalTo: self.topAnchor),
carImg.heightAnchor.constraint(equalTo: self.heightAnchor, multiplier: 0.5),
carImg.widthAnchor.constraint(equalTo: carImg.heightAnchor),
dismissBtn.topAnchor.constraint(equalTo: self.carImg.bottomAnchor, constant: 20),
dismissBtn.centerXAnchor.constraint(equalTo: self.dismissBtn.centerXAnchor),
dismissBtn.widthAnchor.constraint(equalToConstant: 150),
dismissBtn.heightAnchor.constraint(equalToConstant: 75)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I noticed that when I remove the root view and just created everything in the view controller, it seems to work, the thing is that this is just a test for now. The root view in a future project may be very large so I think keeping to a rootview may be a good idea. If there are any other methods similar, please share.
If there is anything I can answer please ask,
Thank you
I am trying to create UICollectionView with cells that adjust the width based on the content of the cell using layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize but it doesn't update. Bellow is two images of what am trying to achieve and the next is what I have achieved.
Here is what am trying to achieve.
Correct display
Here is what I have been able to achieve
What I have now
And here is my code
import UIKit
struct Model {
let name: String
let color: String
}
class DisplayTagsView: UIView {
// Collection View
var projectTagsCollectionView: UICollectionView!
var projectTagsCollectionViewHeightConstraint: NSLayoutConstraint!
private var tagsObjects = [Any]() // Object to allow add button
private var tags: [Model] = [] // Tags
// MARK: - Init
init() {
super.init(frame: .zero)
initViews()
initTags()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
func initTags() {
self.tags.append(Model(name: "Some tag", color: "color"))
self.tags.append(Model(name: "Twotwo", color: "color"))
self.tags.append(Model(name: "Smartparking", color: "color"))
self.tags.append(Model(name: "Development", color: "color"))
self.tagsObjects = self.tags
self.tagsObjects.append(AddButtonCollectionViewCell.self)
}
// MARK: - Private
private func initViews() {
initTagCollectionView()
}
private func initTagCollectionView() {
let layout = UICollectionViewFlowLayout()
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
layout.minimumLineSpacing = 5
layout.minimumInteritemSpacing = 5
layout.scrollDirection = .vertical
projectTagsCollectionView = UICollectionView(frame: self.frame, collectionViewLayout: layout)
projectTagsCollectionView.delegate = self
projectTagsCollectionView.dataSource = self
projectTagsCollectionView.register(cell: ProjectCollectionViewCell.self)
projectTagsCollectionView.register(cell: AddButtonCollectionViewCell.self)
projectTagsCollectionView.isScrollEnabled = false
projectTagsCollectionView.backgroundColor = .clear
}
// set up views
func setupView() {
addSubview(projectTagsCollectionView)
setCollectionHeight()
}
// update height
func setCollectionHeight() {
projectTagsCollectionViewHeightConstraint = projectTagsCollectionView.heightAnchor.constraint(equalToConstant: 200)
projectTagsCollectionViewHeightConstraint.isActive = true
}
// Button button
func addButton() -> UIButton {
let addButton = UIButton()
addButton.setImage(#imageLiteral(resourceName: "settings"), for: .normal)
return addButton
}
}
// MARK: - UICollectionViewDelegateFlowLayout
extension DisplayTagsView : UICollectionViewDelegateFlowLayout {
// Auto resizing of the cell tags
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout
var newEdgeInsets = UIEdgeInsets(top: flowLayout!.sectionInset.top, left: flowLayout!.sectionInset.left, bottom: flowLayout!.sectionInset.bottom, right: flowLayout!.sectionInset.right)
if collectionView.numberOfItems(inSection: section) == 1 {
let edgeInset = newEdgeInsets.right + (collectionView.frame.width - flowLayout!.itemSize.width)
newEdgeInsets.right = edgeInset
}
return newEdgeInsets
}
}
// MARK: - UICollectionViewDelegate
extension DisplayTagsView: UICollectionViewDelegate {
}
// MARK: - UICollectionViewDataSource
extension DisplayTagsView: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return tagsObjects.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let tagObject = self.tagsObjects[indexPath.item]
if let tag = tagObject as? Model {
let cell: ProjectCollectionViewCell = projectTagsCollectionView.dequeueTypedCell(forIndexPath: indexPath)
cell.configureCell(tagName: tag.name, tagBackground: tag.color)
return cell
} else {
// Adding button to the UICollection
let cell: AddButtonCollectionViewCell = membersCollectionView.dequeueTypedCell(forIndexPath: indexPath)
let addButto = addButton()
cell.contentView.subviews.forEach({$0.removeFromSuperview()})
cell.contentView.addSubview(addButton)
settingButton.snp.makeConstraints { make in
make.size.equalTo(cell.contentView)
}
return cell
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print(indexPath)
}
}
Try Following library. TagList view.
https://github.com/ElaWorkshop/TagListView. It surely working ur conditions
I have made a collection view with cells arranged in rows in columns using a great tutorial. I have added a button to a toolbar in the main view controller that calls collectionView.reloadData() as I want a user to be able to edit values which will in turn update the datasource and then reload the collection view to show the updates.
Running this on a simulator it works, but if any scrolling takes place it causes this crash *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayM objectAtIndex:]: index 0 beyond bounds for empty array'. If no scrolling has taken place then calling collectionView.reloadData() works. I can't find where this empty array is which is causing the crash. I have tried printing all the arrays that are used in the code in the console but none appear to be empty. Have tried commenting out various lines of code to try and narrow down where the problem is, it seems to be something in the override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? block. I have also tried reseting the collection view frame co-ordinates to 0 before reload data is called but that also didn't work. Have been stuck going round in circles for a few days which no luck. Any suggestions as to where I am going wrong would be hugely appreciated! My code so far is below (please excuse the long winded explanation and code);
View Controller
import UIKit
class ViewController: UIViewController {
// variable to contain cell indexpaths sent from collectionViewFlowLayout.
var cellIndexPaths = [IndexPath] ()
#IBOutlet weak var collectionView: UICollectionView!
#IBOutlet weak var collectionViewFlowLayout: UICollectionViewFlowLayout! {
didSet {
collectionViewFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
}
}
#IBAction func editButtonPressed(_ sender: UIButton) {
collectionView.reloadData()
}
#IBAction func doneButtonPressed(_ sender: UIButton) {
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.delegate = self
collectionView.dataSource = self
}
}
extension ViewController:UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
2
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
5
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! collectionViewCell
cell.backgroundColor = .green
cell.textLabel.text = "\(indexPath) - AAAABBBBCCCC"
return cell
}
}
extension ViewController:UICollectionViewDelegate, IndexPathDelegate {
func getIndexPaths(indexPathArray: Array<IndexPath>) {
cellIndexPaths = indexPathArray.uniqueValues
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
var cellArray = [UICollectionViewCell] ()
print(cellIndexPaths)
for indexPathItem in cellIndexPaths {
if let cell = collectionView.cellForItem(at: indexPathItem) {
if indexPathItem.section == indexPath.section {
cellArray.append(cell)
}
}
for cells in cellArray {
cells.backgroundColor = .red
Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { (timer) in
cells.backgroundColor = .green
}
}
}
}
}
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let cell = collectionView.cellForItem(at: indexPath)
if let safeCell = cell {
let cellSize = CGSize(width: safeCell.frame.width, height: safeCell.frame.height)
return cellSize
} else {
return CGSize (width: 300, height: 100)
}
}
}
extension Array where Element: Hashable {
var uniqueValues: [Element] {
var allowed = Set(self)
return compactMap { allowed.remove($0) }
}
}
Flow Layout
import UIKit
protocol IndexPathDelegate {
func getIndexPaths(indexPathArray: Array<IndexPath>)
}
class collectionViewFlowLayout: UICollectionViewFlowLayout {
override var collectionViewContentSize: CGSize {
return CGSize(width: 10000000, height: 100000)
}
override func prepare() {
setupAttributes()
indexItemDelegate()
}
// MARK: - ATTRIBUTES FOR ALL CELLS
private var allCellAttributes: [[UICollectionViewLayoutAttributes]] = []
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
for rowAttrs in allCellAttributes {
for itemAttrs in rowAttrs where rect.intersects(itemAttrs.frame) {
layoutAttributes.append(itemAttrs)
}
}
return layoutAttributes
}
// MARK: - SETUP ATTRIBUTES
var cellIndexPaths = [IndexPath] ()
private func setupAttributes() {
allCellAttributes = []
var xOffset: CGFloat = 0
var yOffset: CGFloat = 0
for row in 0..<rowsCount {
var rowAttrs: [UICollectionViewLayoutAttributes] = []
xOffset = 0
for col in 0..<columnsCount(in: row) {
let itemSize = size(forRow: row, column: col)
let indexPath = IndexPath(row: row, column: col)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = CGRect(x: xOffset, y: yOffset, width: itemSize.width, height: itemSize.height).integral
rowAttrs.append(attributes)
xOffset += itemSize.width
cellIndexPaths.append(indexPath)
}
yOffset += rowAttrs.last?.frame.height ?? 0.0
allCellAttributes.append(rowAttrs)
}
}
// MARK: - CONVERT SECTIONS TO ROWS, ITEMS TO COLUMNS
private var rowsCount: Int {
return collectionView!.numberOfSections
}
private func columnsCount(in row: Int) -> Int {
return collectionView!.numberOfItems(inSection: row)
}
// MARK: - GET CELL SIZE
private func size(forRow row: Int, column: Int) -> CGSize {
guard let delegate = collectionView?.delegate as? UICollectionViewDelegateFlowLayout,
let size = delegate.collectionView?(collectionView!, layout: self, sizeForItemAt: IndexPath(row: row, column: column)) else {
assertionFailure("Implement collectionView(_,layout:,sizeForItemAt: in UICollectionViewDelegateFlowLayout")
return .zero
}
return size
}
private func indexItemDelegate () {
let delegate = collectionView?.delegate as? IndexPathDelegate
delegate?.getIndexPaths(indexPathArray: cellIndexPaths)
}
}
// MARK: - INDEX PATH EXTENSION
//creates index path with rows and columns instead of sections and items
private extension IndexPath {
init(row: Int, column: Int) {
self = IndexPath(item: column, section: row)
}
}
Collection Cell
import UIKit
class collectionViewCell: UICollectionViewCell {
#IBOutlet weak var textLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
contentView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentView.leftAnchor.constraint(equalTo: leftAnchor),
contentView.rightAnchor.constraint(equalTo: rightAnchor),
contentView.topAnchor.constraint(equalTo: topAnchor),
contentView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
}
I have managed to get around the issue by using the UICollectionView.reloadSections(sections: IndexSet) method. This doesn't cause any crashes. I loop through all sections and add each section to an IndexSet variable then use that in the reload sections method like this;
var indexSet = IndexSet()
let rowCount = collectionView.numberOfSections
for row in 0..<rowCount {
indexSet = [row]
collectionView.reloadSections(indexSet)
}
I have a SwiftUI View that displays a UIViewControllerRepresentable which is wrapped in a NavigationView. I've gotten it all working to what I want though now I want to be able to push from the collection view another View when the user taps in a cell. The issue I am facing is that the Coordinator does not have access to the navigation controller. Is there a better implementation to this?
This is the initial SwiftUI View
import SwiftUI
struct ExploreView: View {
#ObservedObject var exploreViewVM = ExploreViewViewModel()
var body: some View {
NavigationView {
CollectionView(products: exploreViewVM.products)
}
}
}
struct ExploreView_Previews: PreviewProvider {
static var previews: some View {
ExploreView()
}
}
This is the collectionView wrapper. As you can see in the didSelectItemAt function, I am trying to wrap the view within a HostingController. I know it does not make any sense to instantiate the navigation controller with the root of hosting controller and then push to it. Though I am not sure how to go about it and would appreciate any help.
import SwiftUI
import SDWebImage
struct CollectionView: UIViewControllerRepresentable {
var products: [Product]
let cellId = "cellId"
typealias UIViewControllerType = UICollectionViewController
func makeUIViewController(context: Context) -> UICollectionViewController {
let collectionView = UICollectionViewController(collectionViewLayout: createLayout())
collectionView.collectionView.backgroundColor = .white
collectionView.collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: cellId)
collectionView.collectionView.dataSource = context.coordinator
collectionView.collectionView.delegate = context.coordinator
return collectionView
}
func createLayout() -> UICollectionViewCompositionalLayout {
return UICollectionViewCompositionalLayout { sectionNumber, env in
let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(0.3333), heightDimension: .fractionalWidth(0.3333)))
item.contentInsets.trailing = 1
item.contentInsets.bottom = 1
let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .estimated(500)), subitems: [item])
group.contentInsets.leading = 1
let section = NSCollectionLayoutSection(group: group)
return section
}
}
func updateUIViewController(_ uiViewController: UICollectionViewController, context: Context) {
}
class Coordinator: NSObject, UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return parent.products.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: parent.cellId, for: indexPath)
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.layer.masksToBounds = true
imageView.sd_setImage(with: URL(string: parent.products[indexPath.item].imageUrl)) { (image, _, _, _) in
imageView.image = image
}
cell.backgroundView = imageView
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let controller = UIHostingController(rootView: ProductView(product: parent.products[indexPath.item]))
let navigation = UINavigationController(rootViewController: controller)
navigation.pushViewController(controller, animated: true)
}
let parent: CollectionView
init(_ parent: CollectionView) {
self.parent = parent
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
}
I am creating an app with Swift 4, where I make a request to the API and I want to return a result on a CollectionView.
But I get the following error, which I think is from constraints:
This block is repeated 100 times.
And the result is that he does not paint any cells. Showing an image like this:
Unless I press the top button "CHANGE AUTOLAYOUT" twice. Which is when you paint the cells of the two display modes you have, and it looks like this:
And this:
But the problem is, initially nothing is shown and should be shown. And the error that I show you in the beginning appears.
To help you a little, because I would say that the problem derives from the constrainst applied, I attach some images with the different constrainsts applied.
The initial xib, where the collectionView is, are:
The cell that is initially loaded is:
The cell once we have changed the layout is this:
I attached the code of the main class, the ViewVontroller that controls the CollectionView:
import UIKit
import RxSwift
import RxCocoa
final class SpeedRunListViewController: UIViewController {
#IBOutlet private var collectionView: UICollectionView!
#IBOutlet private var buttonChangeLayout: UIButton!
private let disposeBag = DisposeBag()
private var viewModelList: SpeedRunListViewModel?
private var numElementsByCol: CGFloat = 3
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.isNavigationBarHidden = true
setupCollectionView()
viewModelList = SpeedRunListViewModel(interactor: InteractorSpeedRunSearch())
setupRx(viewModel: viewModelList!)
viewModelList?.fetch()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.isNavigationBarHidden = true
}
private func setupCollectionView() {
registerCollectionCells()
if #available(iOS 10.0, *) {
collectionView.isPrefetchingEnabled = false
} else {
// Fallback on earlier versions
}
calculateLayoutCollectionItem()
}
private func registerCollectionCells() {
collectionView.register(UINib(nibName: SpeedRunRowCollectionViewCell.nibName, bundle: nil),
forCellWithReuseIdentifier: SpeedRunRowCollectionViewCell.reuseCellId)
collectionView.register(UINib(nibName: SpeedRunCollectionViewCell.nibName, bundle: nil),
forCellWithReuseIdentifier: SpeedRunCollectionViewCell.reuseCellId)
}
private func calculateLayoutCollectionItem() {
if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
layout.estimatedItemSize = CGSize.init(width: 2, height: 2)
}
}
private func setupRx(viewModel: SpeedRunListViewModel) {
viewModel.numElements.asObservable().subscribe(onNext: { e in
self.collectionView.reloadData()
}, onError: { error in
}, onCompleted: {
}, onDisposed: {
}).disposed(by: disposeBag)
buttonChangeLayout.rx.tap.subscribe(onNext: { void in
guard let value = self.viewModelList?.layoutRow else {
return
}
self.viewModelList?.layoutRow = !value
self.collectionView.collectionViewLayout.invalidateLayout()
self.collectionView.reloadData()
}, onError: { error in
}, onCompleted: {
}, onDisposed: {
}).disposed(by: disposeBag)
}
fileprivate func getCellId() -> String {
if let layoutRow = self.viewModelList?.layoutRow, layoutRow == true {
return SpeedRunRowCollectionViewCell.reuseCellId
}
return SpeedRunCollectionViewCell.reuseCellId
}
}
//MARK: - UICollectionViewDelegate, UICollectionViewDataSource
extension SpeedRunListViewController: UICollectionViewDelegate,
UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
guard let numElements = viewModelList?.numElements else {
return 0
}
return numElements.value
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: getCellId(), for: indexPath) as! SpeedRunCollectionViewCellBase
if let cellViewModel = viewModelList?.getCellViewModel(index: indexPath.row) {
cell.setupCell(viewModel: cellViewModel)
}
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let speedRun = viewModelList?.getSpeedRun(index: indexPath.row) else {
return
}
let interactorDetail: InteractorSpeedRunDetail = InteractorSpeedRunDetail(speedRun: speedRun)
let viewControllerDetail: SpeedRunDetailViewController = SpeedRunDetailViewController(interactor: interactorDetail)
viewControllerDetail.URISpeedRunDetail = (speedRun.links![1].uri)!
navigationController?.pushViewController(viewControllerDetail, animated: true)
}
}
And the truth is that I do not know why that conflict of layouts occurs. But it's driving me crazy ... I can not understand how the cells are not shown initially (because data is being received). What could it be?
Thank you very much, any question you attach it to me.
[CODE UPDATED]
These is the code solution:
//MARK: - UICollectionViewDelegateFlowLayout
extension SpeedRunListViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize{
if let value = self.viewModelList?.layoutRow {
if value {
return CGSize(width: 320, height: 144)
}
else{
return CGSize(width: 96, height: 162)
}
}
return CGSize(width: 320, height: 144)
}
}
You are not setting the UICollectionViewDelegateFlowLayout in the viewController. You need to set it and then use
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize
To set the sizes of your cells.
You are having the error because when you load the cells the very first time you are basically telling them that they have a size of 0 0.