UICollectionView self sizing cells. Insert/reloadData breaks layout - swift

Writing this question as my last resort, being stuck on this for few days now. I am trying to implement self sizing cells for UICollectionView with FlowLayout. Everything seem to work fine until I reladData() or do insertItemsAtIndexPaths() while being at the bottom of the collectionView.
This is my Cell:
class FooFoo: UICollectionViewCell {
var label: UILabel!
var text: String! {
didSet {
label.text = text
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
setupConstraints()
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupView() {
self.contentView.backgroundColor = UIColor.yellowColor()
label = ({
let view = UILabel()
view.setTranslatesAutoresizingMaskIntoConstraints(false)
self.contentView.addSubview(view)
return view
})()
}
func setupConstraints() {
self.contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-100-[childView]-100-|", options: nil, metrics: nil, views: ["childView": label]))
self.contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-100-[childView]-100-|", options: nil, metrics: nil, views: ["childView": label]))
}
}
This is CollectionView definition:
collectionView = ({
let layout = UICollectionViewFlowLayout()
layout.minimumLineSpacing = 1
layout.estimatedItemSize = CGSize(width: 300, height: 300)
let view = UICollectionView(frame: self.view.frame, collectionViewLayout: layout)
view.dataSource = self
view.delegate = self
view.registerClass(FooFoo.self, forCellWithReuseIdentifier: "Cell")
view.backgroundColor = UIColor.clearColor()
self.view.addSubview(view)
return view
})()
This is dataSource:
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items.count
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! FooFoo
cell.text = "FooBar: \(indexPath.item)"
return cell
}
After I fetch additional items, i tried both inserting new array of indexes, or reloading data and the result is the same - broken layout.
I also tried replacing estimatedItemSize with itemSize and it did work just fine, so I am pretty certain its because self-sizing cells.
This is the result:

Related

Adding an Image to UICollection Header Swift

I'm looking to implement an image within the header of a collection view. This is the code that I have so far, but no header appears when I test. Am I missing something?
func collectionView(_ collectionView: UICollectionView, viewForHeaderInSection section: Int) -> UIView? {
let view = UIView()
let image = UIImageView()
image.frame = CGRect(x: collectionView.frame.width - 10 , y: 0, width: 20, height: 20)
image.image = UIImage.init(named: "trophyCase")
view.addSubview(image)
return view
}
UICollectionViewDelegate doesn't offer such a method as viewForHeaderInSection
Instead you can use viewForSupplementaryElementOfKind method of UICollectionViewDataSource
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
guard kind == UICollectionView.elementKindSectionHeader else {
fatalError("Unrecognized element of kind: \(kind)")
}
let view: ReusableHeaderView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: kind, for: indexPath) as! ReusableHeaderView
view.imageView.image = UIImage.init(named: "trophyCase")
view.imageView.frame = CGRect(x: collectionView.frame.width - 10 , y: 0, width: 20, height: 20)
return view
}
You are also required to register elementKindSectionHeader
collectionView.register(ReusableHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: UICollectionView.elementKindSectionHeader)
Following will be your ReusableHeaderView
class ReusableHeaderView: UICollectionReusableView {
let imageView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
self.setupUI()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setupUI()
}
private func setupUI() {
self.addSubview(imageView)
// Instead of settings imageView.frame, add following to use autolayout constraints
self.imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: self.topAnchor),
imageView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -10.0),
imageView.widthAnchor.constraint(equalToConstant: 20.0),
imageView.heightAnchor.constraint(equalToConstant: 20.0)
])
}
}
Create a view subclassing UICollectionReusableView:
class HeaderView: UICollectionReusableView {
let imageView: UIImageView = {
let iv = UIImageView(image: /*put your image here*/)
iv.clipsToBounds = true
iv.contentMode = .scaleAspectFill
return iv
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
addSubview(imageView)
imageView.fillSuperview() // Check extension below
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Then in your ViewController, first create a reuseIdentifier for your view:
fileprivate let headerId = "headerId"
After that register your custom view in collectionView(lets do it in viewDidLoad):
override func viewDidLoad() {
super.viewDidLoad()
collectionView.register(HeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: headerId)
}
Declare your custom view as optional in your vc:
var headerView: HeaderView?
Then override viewForSupplementaryElementOfKind method of the collectionView to initialize headerView:
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: headerId, for: indexPath) as? HeaderView
return headerView!
}
Then implement another collectionView method to give your headerView a size:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return .init(width: view.frame.width, height: 340) // edit width and height as you please.
}
Extension for fillSuperView used in initialization of custom view:
extension UIView {
func fillSuperview(withPadding padding: UIEdgeInsets = .zero) {
translatesAutoresizingMaskIntoConstraints = false
if let superview = superview {
topAnchor.constraint(equalTo: superview.topAnchor, constant: padding.top).isActive = true
leftAnchor.constraint(equalTo: superview.leftAnchor, constant: padding.left).isActive = true
rightAnchor.constraint(equalTo: superview.rightAnchor, constant: -padding.right).isActive = true
bottomAnchor.constraint(equalTo: superview.bottomAnchor, constant: -padding.bottom).isActive = true
}
}
}
Now it should work as header view for your collectionView.

Label, font programmatically

I need help in this view.
I have to solve these things:
- instead of the 2 "duck" labels, I need to have different label.text with different fonts. I used this method here, in which I assign all the fonts to my datasource and assign the label to it by changing the font. the final result must be, for example, "duck" "ciccio" and "another"..
At the moment the view in which to insert the text is not connected to the colors or the fonts, I have to be able to insert the text and then if I click on a color it automatically changes the color to the written text and so the same thing for the fonts ..
What do I have to do?
I am attaching part of the code, only the one in which I put the "duck" label and the controller of the whole view.
that I have now
class CollectionFont: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
weak var delegate: FontDelegate?
var collectionView: UICollectionView?
let cellSpacing:CGFloat = 1
let indexPath: IndexPath
var datasource: [UIFont] = []
var imageArray: Array<UIImageView> = []
var onceOnly = false
init(datasource: [UIFont], index: IndexPath = [0,0]) {
self.indexPath = index
self.datasource = Theme.CustomFontsFamily.customFonts
super.init(nibName: nil, bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.delegate = self
collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: UICollectionViewLayout())
collectionView!.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView!)
collectionView!.activate(constraint(edgesTo: self.view))
collectionView!.backgroundColor = Theme.Colors.sky
let collectionViewFlowLayout = UICollectionViewFlowLayout()
collectionView!.setCollectionViewLayout(collectionViewFlowLayout, animated: true)
collectionViewFlowLayout.scrollDirection = .horizontal
collectionViewFlowLayout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
collectionViewFlowLayout.minimumLineSpacing = 0
collectionViewFlowLayout.minimumInteritemSpacing = 0
collectionView!.register(FontCell.self, forCellWithReuseIdentifier: FontCell.reuseIdentifier)
collectionView!.delegate = self
collectionView!.dataSource = self
collectionView!.isPagingEnabled = true
collectionView!.bounces = false
self.collectionView?.reloadData()
}
class FontRound {
let font: UILabel
init(font: UILabel) {
self.font = font
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.datasource.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: FontCell.reuseIdentifier, for: indexPath) as! FontCell
cell.array.font = self.datasource[indexPath.row]
return cell
}
//UICollectionViewDelegateFlowLayout - constraint della collecion view da innestare
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = UIScreen.main.bounds.size.width/4
let height = UIScreen.main.bounds.size.width/4
return CGSize(width: width, height: height)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
debugPrint("sucaaaa")
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
var visibleRect = CGRect()
visibleRect.origin = collectionView!.contentOffset
visibleRect.size = collectionView!.bounds.size
let visiblePoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)
guard let indexPath = collectionView!.indexPathForItem(at: visiblePoint) else { return }
print(indexPath[1])
self.delegate?.sincronizeScroll(indexPath: indexPath)
}
internal func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if !onceOnly {
//set the row and section you need.
collectionView.scrollToItem(at: self.indexPath, at: UICollectionView.ScrollPosition.right, animated: false)
onceOnly = true
}
}
}
final class CollectionFontView: UICollectionView {
init(_ collectionViewLayout: UICollectionViewLayout) {
super.init(frame: .zero, collectionViewLayout: collectionViewLayout)
self.backgroundColor = UIColor.red
self.showsHorizontalScrollIndicator = false
register(FontCell.self, forCellWithReuseIdentifier: FontCell.reuseIdentifier)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class FontCell: UICollectionViewCell {
static let reuseIdentifier = "FontCell_RID"
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
var cellID: String?
var array = UILabel().then{
$0.text = "duck"
$0.font = UIFont(name:"Jellee-Roman",size:15)
$0.contentMode = UIView.ContentMode.scaleAspectFit
$0.layer.cornerRadius = (((UIScreen.main.bounds.size.width/8)*0.8)/2)
}
func setupViews(){
self.addSubview(self.array)
self.array.activate([
array.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 0.8),
array.heightAnchor.constraint(equalTo: self.array.widthAnchor),
array.centerXAnchor.constraint(equalTo: self.centerXAnchor),
array.centerYAnchor.constraint(equalTo: self.centerYAnchor),
])
}
func setContents(container: UILabel) {
for sv in self.contentView.subviews {
sv.removeFromSuperview()
}
self.contentView.addSubview(container)
container.activate(constraint(edgesTo: self.contentView))
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

Cells not appearing in UICollectionView

I don't understand why my cells aren't showing:
class MenuBar: UIView, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
let cellId = "pouet"
lazy var customCollectionView : UICollectionView = {
let layout = UICollectionViewLayout()
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = UIColor.gray
cv.dataSource = self
cv.delegate = self
return cv
}()
override init(frame: CGRect) {
super.init(frame: frame)
customCollectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: cellId)
addSubview(customCollectionView)
addConstraintsWithFormat(format: "H:|[v0]|", views: customCollectionView)
addConstraintsWithFormat(format: "V:|[v0]|", views: customCollectionView)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 4
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = customCollectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath)
print("test")
return cell
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Now my print("test") is not showing either, meaning that the method is actually not called, right? So what is wrong ?
Change this line of code:
let layout = UICollectionViewLayout()
To:
let layout = UICollectionViewFlowLayout()
UICollectionViewLayout is abstract class you can't use it directly.

collectionViewCells to collectionView position x:0, y:0

I am experiencing some layout issues with collectionViewCells. I am programmatically applying layout constraints and UICollectionViewCells are not being pinned to the 0,0 position in my collectionView. See attached screenshot for reference. Thanks in advance!
class CurrentUserPlaceDetailsVC: UIViewController, UICollectionViewDelegateFlowLayout {
override func viewDidLoad() {
super.viewDidLoad()
setupMenuBar()
}
let menuBar: MenuBar = {
let mb = MenuBar()
return mb
}()
private func setupMenuBar() {
view.addSubview(menuBar)
view.addConstraintsWithFormat("H:|[v0]|", views: menuBar)
view.addConstraintsWithFormat("V:|-64-[v0(150)]", views: menuBar)
}
var placesTableVC: PlacesTableVC?
}
class MenuBar: UIView, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = UIColor.green
cv.dataSource = self
cv.delegate = self
return cv
}()
let cellId = "cellId"
override init(frame: CGRect) {
super.init(frame: frame)
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: cellId)
addSubview(collectionView)
addConstraintsWithFormat("H:|[v0]|", views: collectionView)
addConstraintsWithFormat("V:|[v0]|", views: collectionView)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 4
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath)
cell.backgroundColor = .blue
return cell
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension UIView {
func addConstraintsWithFormat(_ format: String, views: UIView...) {
var viewsDictionary = [String: UIView]()
for (index, view) in views.enumerated() {
let key = "v\(index)"
view.translatesAutoresizingMaskIntoConstraints = false
viewsDictionary[key] = view
}
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: format, options: NSLayoutFormatOptions(), metrics: nil, views: viewsDictionary))
}
You need to set the minimumLineSpacing, headerReferenceSize, and/or sectionInset of the UICollectionViewFlowLayout instance you use to initialize your UICollectionView. You can also set the contentInset of the UICollectionView.
Try this:
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.minimumLineSpacing = 0
layout.headerReferenceSize = .zero
layout.sectionInset = .zero
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .green
cv.dataSource = self
cv.delegate = self
cv.contentInset = .zero
return cv
}()

Swift 3: Be able to tap view behind a collectionViewCell?

I am working on a new feature in my app where I want a user to have an image and then be able to swipe through a collectionView on top of that image, adding "filters". The issue that I have is that I want the user to still be able to interact with the image, but it is behind the collectionView. If I set the collectionViews inUsersInteractionEnabled to false, of course I then cannot use the collectionView at all. I am going for a similar functionality to Snapchats "scroll through filters" feature. Any help will be highly appreciated. Thank you guys. Code is below:
import UIKit
class FirstCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
func setupViews() {
backgroundColor = .clear
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class SecondCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
let cellView: UIView = {
let view = UIView()
view.backgroundColor = .red
view.alpha = 0.25
return view
}()
func setupViews() {
backgroundColor = .clear
addSubview(cellView)
cellView.frame = CGRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class SecondController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.minimumLineSpacing = 0
layout.minimumInteritemSpacing = 0
layout.scrollDirection = .horizontal
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .clear
cv.dataSource = self
cv.delegate = self
cv.translatesAutoresizingMaskIntoConstraints = false
cv.isPagingEnabled = true
return cv
}()
override func viewDidLoad() {
super.viewDidLoad()
collectionView.register(FirstCell.self, forCellWithReuseIdentifier: "firstCell")
collectionView.register(SecondCell.self, forCellWithReuseIdentifier: "secondCell")
view.backgroundColor = .white
setupViews()
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: view.frame.height)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 2
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if indexPath.item == 0 {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "firstCell", for: indexPath) as! FirstCell
return cell
} else {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "secondCell", for: indexPath) as! SecondCell
return cell
}
}
lazy var imageView: UIImageView = {
let iv = UIImageView()
let image = UIImage(named: "img")
iv.image = image
iv.contentMode = .scaleAspectFill
iv.translatesAutoresizingMaskIntoConstraints = false
iv.isUserInteractionEnabled = true
iv.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap)))
return iv
}()
func handleTap() {
print(123)
}
func setupViews() {
view.addSubview(imageView)
view.addSubview(collectionView)
imageView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
imageView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
imageView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
imageView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
collectionView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
collectionView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
collectionView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
}
}
There is a hitTest(_::) method available to override in any of the UIView subclass, which you can implement in your custom UICollectionView subclass and forward taps from it to any other view (the image behind collection view in your case).
override func hitTest(_ point: CGPoint,
with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
if self.parentViewController.forwardTapToImageIfNeeded(point) {
return nil
}
return hitView
}
Where parentViewController is a reference to your SecondController instance, which should have forwardTapToImageIfNeeded(_) function to check if user's tap is on top of the image frame, and if it is — automatically pass tap event to it.
func forwardTapToImageIfNeeded(_ p: CGPoint) -> Bool {
/* Convert point to local coordinates */
let pointInSuperView = self.convert(p, from: collectionView)
/* Check if tap is on top of the image frame */
if imageBehindCollection.frame.contains(pointInSuperView) {
// Forward tap to the image `imageBehindCollection`
return true
}
return false
}
Note: I didn't test this particular code by myself, so it might need a few fixes in Xcode.