UICollectionView section border in Swift - ios

I'm trying to use UICollectionView to bind different cells with an XIB file and set a design for it.
I know how to bind different cells and it's works fine in my application.
Here the code to bind:
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
var cell : UICollectionViewCell
mCurrentIndexPath = indexPath
// HEADER
switch indexPath.section {
case 0:
cell = configureModuleHeaderCell(indexPath)
default:
// Local
let theme = getThemeFromIndex(indexPath.section - 1)
mCurrentDocuments = getDocumentsFromTheme(theme)
let cours : DownloadableDocument? = (mCurrentDocuments != nil) ? getCoursForTheme() : nil
mCurrentDocuments = deleteCoursFromDocAnnexe()
mCurrentDocuments = sortDocumentDoublePDF()
if indexPath.row == 0 {
cell = configureThemeHeaderCell(theme, cours: cours)
}
// NORMAL
else {
cell = configureThemeDocCell()
cell.layer.borderWidth = 1.0
cell.layer.borderColor = UIColor.grayColor().CGColor
}
break
}
return cell
}
override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
if mFetchedResultController != nil {
mCurrentIndexPath = NSIndexPath(forRow: 0, inSection: 0)
return mFetchedResultController!.fetchedObjects!.count + 1
}
return 0
}
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
var counter : Int = 1
switch section {
case 0:
counter = 1
break
default:
mCurrentDocuments = getDocumentsFromTheme(getThemeFromIndex(section - 1))
mCurrentDocuments = checkDoubleCours()
counter = (mCurrentDocuments != nil) ? mCurrentDocuments!.count : 0
counter += (!documentsContainsCours(mCurrentDocuments)) ? 1 : 0
break
}
return counter
}
Then I want to set a border for each section. Is that possible ?
I can set a border for cell by :
cell.layer.borderWidth = 1.0
cell.layer.borderColor = UIColor.grayColor().CGColor
But I want to do it for a section.

I've finally found a solution !
I just need to calculate the height of all cells of a section, set this height on the view and position it well.
EDIT:
Update for #Urmi
Add this function to instanciate a cardView with frame
func instanciateCardView(frame : CGRect) -> UIView {
// Declaration
let view = UIView(frame : frame)
// Basic UI
view.backgroundColor = UIColor.whiteColor()
view.layer.backgroundColor = UIColor.whiteColor().CGColor
view.layer.borderWidth = 1.0
view.layer.borderColor = ColorsUtil.UIColorFromRGB(0xAB9595).CGColor
// Shadow (cardview style)
view.layer.shadowPath = UIBezierPath(rect: view.bounds).CGPath
view.layer.shouldRasterize = true
view.layer.shadowColor = ColorsUtil.UIColorFromRGB(0xD5C6C6).CGColor
view.layer.shadowOpacity = 1
view.layer.shadowOffset = CGSizeMake(1, 1);
view.layer.shadowRadius = 1.0
if !mCardViewList.contains(view) {
// Add view to container - Just to save the instance
mCardViewList.append(view)
// Add view to main view
self.collectionView?.addSubview(view)
self.collectionView?.sendSubviewToBack(view)
}
return view
}
And in the cellForItemAtIndexPath :
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
var cell : UICollectionViewCell
mCurrentIndexPath = indexPath
switch indexPath.section {
case 0:
cell = configureModuleHeaderCell(indexPath)
// Basic UI
cell.backgroundColor = UIColor.whiteColor()
cell.layer.borderWidth = 1.0
cell.layer.borderColor = ColorsUtil.UIColorFromRGB(0xAB9595).CGColor
// Shadow (cardview style)
cell.layer.shadowPath = UIBezierPath(rect: view.bounds).CGPath
cell.layer.shouldRasterize = true
cell.layer.shadowColor = ColorsUtil.UIColorFromRGB(0xD5C6C6).CGColor
cell.layer.shadowOpacity = 1
cell.layer.shadowOffset = CGSizeMake(1, 1);
cell.layer.shadowRadius = 1.0
break
default:
let theme = getThemeFromIndex(indexPath.section - 1)
mCurrentDocuments = getDocumentsFromTheme(theme)
let cours : DownloadableDocument? = (mCurrentDocuments != nil) ? getCoursForTheme() : nil
mCurrentDocuments = deleteCoursFromDocAnnexe()
mCurrentDocuments = sortDocumentDoublePDF(mCurrentDocuments)
if indexPath.row == 0 {
cell = configureThemeHeaderCell(theme, cours: cours)
if mCurrentDocuments != nil {
if mCurrentDocuments!.count == 0 {
instanciateCardView(CGRectMake(cell.frame.origin.x - 30, cell.frame.origin.y - 10, cell.frame.width + 60, cell.frame.height + 20))
}
}
}
else {
cell = configureThemeDocCell()
if indexPath.row == mCollectionView.numberOfItemsInSection(indexPath.section) - 1 {
// Attribute
let index = NSIndexPath(forRow: 0, inSection: indexPath.section)
let headerAttribute : UICollectionViewLayoutAttributes = mCollectionView.layoutAttributesForItemAtIndexPath(index)!
let height = (cell.frame.origin.y + cell.frame.height) - (headerAttribute.frame.origin.y - 10)
instanciateCardView(CGRectMake(headerAttribute.frame.origin.x - 30, headerAttribute.frame.origin.y - 10, headerAttribute.frame.width + 60, height + 20))
}
}
break
}
return cell
}
I've found that is not the best solution, but it works. Now i've declare a UITableView and add a UICollectionView in each UITableViewCell
Don't forget to upvote if it helps you :)

Related

Matching Row Height Between UITableviews

My application is to display employee schedules at a store.
My design is that on the left side I have a tableview with employee names in each cell, on the right side I have a header with store operation times and below that a colored bar that extends from an employee's start time to their end time.
The right side of the table must scroll horizontally to so that the user can scroll and see the schedule for any part of the day. I accomplished this effect by putting the times header and the right side table into a scrollview.
In the heightForRowAt function I have
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if tableView.accessibilityIdentifier == "SecondaryTable" {
var height = tblView.rowHeight
if let nameCell = tblView.cellForRow(at: indexPath) {
height = nameCell.frame.height
} else {
height = tblView.rowHeight
}
return height
}
else {
return UITableView.automaticDimension
}
}
Which for the name table returns UITableView.automaticDimension and for the hours table (accessibility identifier "SecondaryTable") should return the height for the corresponding cell on the name table to make sure they line up properly.
The issue is for some reason heightForRowAt is being called for the schedule table when the name table has not loaded the corresponding cell and so it returns tblView.rowHeight which is not the correct height. You can see this in the second to last row in the above image. I verified this by checking what indexPath the schedule table was loading and that the index was not in the list of visible cells for the name table.
The only time these tables are being reloaded is in the viewWillAppear right now:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
DispatchQueue.main.async {
self.tblView.reloadData()
self.secondTblView.reloadData()
}
}
This is only affecting the last row in the table when initially loaded but when the name table does load in the cell doesn't line up with the schedule. It looks like this corrects itself later on but doesn't reload the cell because the information in the cell doesn't change to fill the cell but the next cell starts at the correct spot and the separator lines line up properly.
In case it helps, here is my cellForRow:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "scheduleCell") as? ScheduleCell ?? ScheduleCell()
let cellSchedule = schedule[indexPath.row]
if tableView.accessibilityIdentifier == "SecondaryTable" {
let coloredLabel = UILabel()
coloredLabel.text = " " + cellSchedule.role
coloredLabel.backgroundColor = UIColor.green.darker(by: 35)
coloredLabel.textColor = UIColor.white
timesHeader.layoutSubviews()
var drawStartOnHalfHour = false
var drawEndOnHalfHour = false
var clockFormat = ClockFormat.TwentyFourHour
for hour in self.timesHeader.arrangedSubviews{
let lineLayer = CAShapeLayer()
let x = hour.center.x
switch indexPath.row % 2 {
case 0:
lineLayer.strokeColor = UIColor.lightGray.cgColor
break
default:
lineLayer.strokeColor = UIColor.white.cgColor
break
}
lineLayer.lineWidth = 2
let path = CGMutablePath()
if let header = hour as? UILabel, header.text != nil {
lineLayer.lineDashPattern = [1,5]
path.addLines(between: [CGPoint(x: x, y: 5),
CGPoint(x: x, y: cell.contentView.frame.maxY - 5)])
} else {
path.addLines(between: [CGPoint(x: x, y: cell.contentView.frame.maxY/2 - 2),
CGPoint(x: x, y: cell.contentView.frame.maxY/2 + 2)])
}
lineLayer.path = path
DispatchQueue.main.async {
cell.contentView.layer.addSublayer(lineLayer)
}
if let header = hour as? UILabel {
if header.text != nil {
var afterNoon = false
//On the hour logic
var formatCheck = ""
if header.text!.split(separator: ":").count == 1 {
clockFormat = .TwelveHour
formatCheck = String(describing: header.text!.split(separator: " ")[1] )
}
var headerTime = 0
if clockFormat == .TwentyFourHour {
headerTime = Int(String(describing: header.text!.split(separator: ":")[0])) ?? 0
} else {
headerTime = Int(String(describing: header.text!.split(separator: " ")[0])) ?? 0
}
var UTCCalendar = Calendar.current
UTCCalendar.timeZone = TimeZone(abbreviation: "UTC")!
var t = UTCCalendar.component(.hour, from: cellSchedule.StartTime)
if clockFormat == .TwelveHour && t >= 12{
if t > 12 {
t = t-12
}
afterNoon = true
}
if headerTime == t {
let m = UTCCalendar.component(.minute, from: cellSchedule.StartTime)
if clockFormat == .TwentyFourHour || ((afterNoon && formatCheck.contains("p")) || (!afterNoon && formatCheck.contains("a"))) {
if m == 0 {
//Logic for start times on the hour
coloredLabel.frame = CGRect(x: x, y: cell.contentView.frame.maxY/4,
width: 5, height: cell.contentView.frame.maxY/2)
} else {
//Logic for handling start times on half-hour
drawStartOnHalfHour = true
}
}
}
var e = UTCCalendar.component(.hour, from: cellSchedule.EndTime)
if clockFormat == .TwelveHour && e >= 12{
if e > 12 {
e = e - 12
}
afterNoon = true
}
if headerTime == e {
let m = UTCCalendar.component(.minute, from: cellSchedule.EndTime)
if clockFormat == .TwentyFourHour || ((afterNoon && formatCheck.contains("p")) || (!afterNoon && formatCheck.contains("a"))) {
if m == 0 {
//Logic for end time on the hour
let width = x - coloredLabel.frame.minX
coloredLabel.frame = CGRect(x: coloredLabel.frame.minX,
y: coloredLabel.frame.minY,
width: width, height: coloredLabel.frame.height)
} else {
//Logic for end time on the half-hour
drawEndOnHalfHour = true
}
}
}
} else {
//Half-hour logic
if drawStartOnHalfHour {
drawStartOnHalfHour = false
coloredLabel.frame = CGRect(x: x, y: cell.contentView.frame.maxY/4,
width: 5, height: cell.contentView.frame.maxY/2)
} else if drawEndOnHalfHour {
drawEndOnHalfHour = false
let width = x - coloredLabel.frame.minX
coloredLabel.frame = CGRect(x: coloredLabel.frame.minX,
y: coloredLabel.frame.minY,
width: width, height: coloredLabel.frame.height)
}
}
}
}
DispatchQueue.main.async {
cell.contentView.addSubview(coloredLabel)
}
switch indexPath.row % 2 {
case 0:
let backGround = CALayer()
backGround.frame = cell.contentView.frame
backGround.backgroundColor = UIColor.white.cgColor
cell.contentView.layer.addSublayer(backGround)
break
default:
let backGround = CALayer()
backGround.frame = CGRect(x: 0,
y: 0,
width: self.timesHeader.frame.width,
height: cell.contentView.frame.height)
backGround.backgroundColor = UIColor.lightGray.cgColor
cell.contentView.layer.addSublayer(backGround)
break
}
} else {
cell.textLabel?.numberOfLines = 2
let firstName = String(describing: cellSchedule.FirstName!.prefix(35))
let lastName = String(describing: cellSchedule.LastName!.prefix(35))
cell.textLabel?.text = firstName.trimmingCharacters(in: CharacterSet(charactersIn: " ")) + "\n" + lastName.trimmingCharacters(in: CharacterSet(charactersIn: " "))
cell.textLabel?.sizeToFit()
switch indexPath.row % 2 {
case 0:
cell.textLabel?.textColor = UIColor.black
cell.contentView.backgroundColor = UIColor.white
break
default:
cell.textLabel?.textColor = UIColor.white
cell.contentView.backgroundColor = UIColor.lightGray
break
}
}
return cell
}
Any idea on a clean way to accomplish this?

How to align cell to top by Flow Layout in CollectionView

In this code I am trying to change the size of the first cell of the UICollectionView and the others with the same size but in the first row only one cell is coming out when I want two to come out:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout:
if indexPath.row == 0 {
return CGSize(width: collectionView.frame.width/1.5-2, height: collectionView.frame.width/1.5-2)
}
else {
return CGSize(width: collectionView.frame.width/3.0-3, height: collectionView.frame.width/3.0-3)
}
}
What I really want is this:
You need to implement an UICollectionViewLayout, I had called it FillingLayout, Note that you can adjust the number of columns and the size of your big cells with the delegate methods
Explanation
You need to add an Array to track your columns heigths and see what is the shortest column that is private var columsHeights : [CGFloat] = [] and you need also an array of (Int,Float) tuple to keep which spaces are available to be filled, I also added a method in the delegate to get the number of columns we want in the collection View and a method to know if a cell can be added or not in a position according their size.
Then if we want to add a cell we check if can be added if not, because the first column is filled we add the space corresponding to column2 in the avaiableSpaces array and when we add the next cell first we check if can be added in any available space if can be added we add and remove the available space.
here is the full code
import UIKit
protocol FillingLayoutDelegate: class {
func collectionView(_ collectionView:UICollectionView, sizeForViewAtIndexPath indexPath:IndexPath) -> Int
// Returns the amount of columns that have to display at that moment
func numberOfColumnsInCollectionView(collectionView:UICollectionView) ->Int
}
class FillingLayout: UICollectionViewLayout {
weak var delegate: FillingLayoutDelegate!
fileprivate var cellPadding: CGFloat = 10
fileprivate var cache = [UICollectionViewLayoutAttributes]()
fileprivate var contentHeight: CGFloat = 0
private var columsHeights : [CGFloat] = []
private var avaiableSpaces : [(Int,CGFloat)] = []
fileprivate var contentWidth: CGFloat {
guard let collectionView = collectionView else {
return 0
}
let insets = collectionView.contentInset
return collectionView.bounds.width - (insets.left + insets.right)
}
var columnsQuantity : Int{
get{
if(self.delegate != nil)
{
return (self.delegate?.numberOfColumnsInCollectionView(collectionView: self.collectionView!))!
}
return 0
}
}
//MARK: PRIVATE METHODS
private func shortestColumnIndex() -> Int{
var retVal : Int = 0
var shortestValue = MAXFLOAT
var i = 0
for columnHeight in columsHeights {
//debugPrint("Column Height: \(columnHeight) index: \(i)")
if(Float(columnHeight) < shortestValue)
{
shortestValue = Float(columnHeight)
retVal = i
}
i += 1
}
//debugPrint("shortest Column index: \(retVal)")
return retVal
}
//MARK: PRIVATE METHODS
private func largestColumnIndex() -> Int{
var retVal : Int = 0
var largestValue : Float = 0.0
var i = 0
for columnHeight in columsHeights {
//debugPrint("Column Height: \(columnHeight) index: \(i)")
if(Float(columnHeight) > largestValue)
{
largestValue = Float(columnHeight)
retVal = i
}
i += 1
}
//debugPrint("shortest Column index: \(retVal)")
return retVal
}
private func canUseBigColumnOnIndex(columnIndex:Int,size:Int) ->Bool
{
if(columnIndex < self.columnsQuantity - (size-1))
{
let firstColumnHeight = columsHeights[columnIndex]
for i in columnIndex..<columnIndex + size{
if(firstColumnHeight != columsHeights[i]) {
return false
}
}
return true
}
return false
}
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}
override func prepare() {
// Check if cache is empty
guard cache.isEmpty == true, let collectionView = collectionView else {
return
}
// Set all column heights to 0
self.columsHeights = []
for _ in 0..<self.columnsQuantity {
self.columsHeights.append(0)
}
for item in 0 ..< collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
let viewSize: Int = delegate.collectionView(collectionView, sizeForViewAtIndexPath: indexPath)
let blockWidth = (contentWidth/CGFloat(columnsQuantity))
let width = blockWidth * CGFloat(viewSize)
let height = width
var columIndex = self.shortestColumnIndex()
var xOffset = (contentWidth/CGFloat(columnsQuantity)) * CGFloat(columIndex)
var yOffset = self.columsHeights[columIndex]
if(viewSize > 1){//Big Cell
if(!self.canUseBigColumnOnIndex(columnIndex: columIndex,size: viewSize)){
// Set column height
for i in columIndex..<columIndex + viewSize{
if(i < columnsQuantity){
self.avaiableSpaces.append((i,yOffset))
self.columsHeights[i] += blockWidth
}
}
// Set column height
yOffset = columsHeights[largestColumnIndex()]
xOffset = 0
columIndex = 0
}
for i in columIndex..<columIndex + viewSize{
if(i < columnsQuantity){
//current height
let currValue = self.columsHeights[i]
//new column height with the update
let newValue = yOffset + height
//space that will remaing in blank, this must be 0 if its ok
let remainder = (newValue - currValue) - CGFloat(viewSize) * blockWidth
if(remainder > 0) {
debugPrint("Its bigger remainder is \(remainder)")
//number of spaces to fill
let spacesTofillInColumn = Int(remainder/blockWidth)
//we need to add those spaces as avaiableSpaces
for j in 0..<spacesTofillInColumn {
self.avaiableSpaces.append((i,currValue + (CGFloat(j)*blockWidth)))
}
}
self.columsHeights[i] = yOffset + height
}
}
}else{
//if there is not avaiable space
if(self.avaiableSpaces.count == 0)
{
// Set column height
self.columsHeights[columIndex] += height
}else{//if there is some avaiable space
yOffset = self.avaiableSpaces.first!.1
xOffset = CGFloat(self.avaiableSpaces.first!.0) * width
self.avaiableSpaces.remove(at: 0)
}
}
print(width)
let frame = CGRect(x: xOffset, y: yOffset, width: width, height: height)
let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = insetFrame
cache.append(attributes)
contentHeight = max(contentHeight, frame.maxY)
}
}
func getNextCellSize(currentCell: Int, collectionView: UICollectionView) -> Int {
var nextViewSize = 0
if currentCell < (collectionView.numberOfItems(inSection: 0) - 1) {
nextViewSize = delegate.collectionView(collectionView, sizeForViewAtIndexPath: IndexPath(item: currentCell + 1, section: 0))
}
return nextViewSize
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var visibleLayoutAttributes = [UICollectionViewLayoutAttributes]()
// Loop through the cache and look for items in the rect
for attributes in cache {
if attributes.frame.intersects(rect) {
visibleLayoutAttributes.append(attributes)
}
}
return visibleLayoutAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cache[indexPath.item]
}
}
UPDATED
You need to setup your viewController as FillingLayoutDelegate
override func viewDidLoad() {
super.viewDidLoad()
collectionView.delegate = self
collectionView.dataSource = self
// Do any additional setup after loading the view.
if let layout = self.collectionView.collectionViewLayout as? FillingLayout
{
layout.delegate = self
}
}
FillingLayoutDelegate implementation in your ViewController
extension ViewController: FillingLayoutDelegate{
func collectionView(_ collectionView:UICollectionView,sizeForViewAtIndexPath indexPath:IndexPath) ->Int{
if(indexPath.row == 0 || indexPath.row == 4)
{
return 2
}
if(indexPath.row == 5)
{
return 3
}
return 1
}
func numberOfColumnsInCollectionView(collectionView:UICollectionView) ->Int{
return 3
}
}
ScreenShot working
This is not an exact answer to your question, but maybe it will help you.
Here is a very good tutorial:
https://www.raywenderlich.com/164608/uicollectionview-custom-layout-tutorial-pinterest-2
Item # 6 I handed over for myself:
// 6. Updates the collection view content height
contentHeight = max(contentHeight, frame.maxY)
yOffset[column] = yOffset[column] + height
//Only for 2 columns
if yOffset[1] >= yOffset[0] {
column = 0
} else {
column = 1
}

How to set different height and width of cells in horizontal collectionview

I have a horizontal collection view and it shows 5 cells at a time and i have adjusted that through sizeForItemAt :
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: (self.numberCollectionView?.bounds.size.width)!/5 - 3, height: (self.numberCollectionView?.bounds.size.width)!/5 - 3 )
}
But these 5 cells would have different width and height and there is a ratio to it. So the center item would take 40% of the whole size, the 2nd and 3rd item would take 20% of the size and the 1st and 5th would take 10% of the size. According to this ratio items size would get change on scroll. These items are all Label.
Edit:
unlike the suggested question and other questions in stackoverflow in my case there is only one row and i have custom UICollectionViewFlowLayout to handle the scrolling by cell
UICollectionViewFlowLayout to handle the scrolling by cell:
class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
var offsetAdjustment = CGFloat.greatestFiniteMagnitude
let horizontalOffset = proposedContentOffset.x
let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: self.collectionView!.bounds.size.width, height: self.collectionView!.bounds.size.height)
for layoutAttributes in super.layoutAttributesForElements(in: targetRect)! {
let itemOffset = layoutAttributes.frame.origin.x
if (abs(itemOffset - horizontalOffset) < abs(offsetAdjustment)) {
offsetAdjustment = itemOffset - horizontalOffset
}
}
return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
}
}
Then to find the center cell of the CollectionView:
private func findCenterIndex() -> Int {
let center = self.view.convert(numberCollectionView.center, to: self.numberCollectionView)
let row = numberCollectionView!.indexPathForItem(at: center)?.row
guard let index = row else {
return 0
}
self.rating = index
return index
}
under scrollViewDidScrolltransformed cell:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
transformCell()
}
private func transformCell() {
let row = findCenterIndex()
var offset : Float = 0.0
for cell in numberCollectionView.visibleCells as! [VotingCell] {
if row == numberCollectionView.indexPath(for: cell)?.row {
offset = 1.1
cell.label.font = UIFont.boldSystemFont(ofSize: 28.0)
cell.label.textColor = UIColor.black
cell.alpha = 1.0
cell.label.textAlignment = .center
}
else if row == (numberCollectionView.indexPath(for: cell)?.row)! + 1 || row == (numberCollectionView.indexPath(for: cell)?.row)! - 1
{
offset = 0.8
cell.label.font = UIFont(name: cell.label.font.fontName, size: 26.0)
cell.label.textColor = UIColor.gray
cell.alpha = 0.8
cell.label.textAlignment = .center
}
else if row == (numberCollectionView.indexPath(for: cell)?.row)! + 2 || row == (numberCollectionView.indexPath(for: cell)?.row)! - 2 {
offset = 0.7
cell.label.font = UIFont(name: cell.label.font.fontName, size: 20.0)
cell.label.textColor = UIColor.gray
cell.alpha = 0.8
cell.label.textAlignment = .center
}
else
{
offset = 0.00
}
cell.transform = CGAffineTransform.identity
cell.transform = CGAffineTransform(scaleX: CGFloat(offset), y: CGFloat(offset))
}
}
also called transformCell() from viewDidLayoutSubviews for initial load.

Problems reordering Collection View Cell with custom dimensions

I want to reorder cells in a Collection View with custom size for every cell.
In every cell of the Collection View there is a label with a word.
I set the dimension of every cell with this code:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let word = textArray[indexPath.row]
let font = UIFont.systemFont(ofSize: 17)
let fontAttributes = [NSFontAttributeName: font]
var size = (word as NSString).size(attributes: fontAttributes)
size.width = size.width + 2
return size
}
I reorder the Collection View with this code:
override func viewDidLoad() {
super.viewDidLoad()
self.installsStandardGestureForInteractiveMovement = false
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(gesture:)))
self.collectionView?.addGestureRecognizer(panGesture)
}
func handlePanGesture(gesture: UIPanGestureRecognizer) {
switch gesture.state {
case UIGestureRecognizerState.began :
guard let selectedIndexPath = self.collectionView?.indexPathForItem(at: gesture.location(in: self.collectionView)) else {
break
}
collectionView?.beginInteractiveMovementForItem(at: selectedIndexPath)
print("Interactive movement began")
case UIGestureRecognizerState.changed :
collectionView?.updateInteractiveMovementTargetPosition(gesture.location(in: gesture.view!))
print("Interactive movement changed")
case UIGestureRecognizerState.ended :
collectionView?.endInteractiveMovement()
print("Interactive movement ended")
default:
collectionView?.cancelInteractiveMovement()
print("Interactive movement canceled")
}
}
override func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
// Swap values if sorce and destination
let change = textArray[sourceIndexPath.row]
textArray.remove(at: sourceIndexPath.row)
textArray.insert(change, at: destinationIndexPath.row)
// Reload data to recalculate dimensions for the cells
collectionView.reloadData()
}
The view looks like this:
The problem is that during the reordering, the cells maintain the dimensions of the original cell at a indexPath, so during the the reordering, the view looks like this:
At the moment I've fixed the problem reloading data at the end of the reordering, to recalculate the right dimensions.
How can I mantain the right dimensions for the cells also during the interactive movement and reorder custom size cells?
This has been bugging me all week so I sat down this evening to try and find a solution. I think what you need is a custom layout manager for your collection view, which can dynamically adjust the layout for each cell as the order is changed.
The following code obviously produces something a lot cruder than your layout above, but fundamentally achieves the behaviour you want: crucially moving to the new layout when the cells are reordered occurs "instantaneously" without any interim adjustments required.
The key to it all is the didSet function in the sourceData variable of the view controller. When this array's value is changed (via pressing the sort button - my crude approximation to your gesture recogniser), this automatically triggers a recalculation of the required cell dimensions which then also triggers the layout to clear itself down and recalculate and the collection view to reload the data.
If you have any questions on any of this, let me know. Hope it helps!
UPDATE: OK, I understand what you are trying to do now, and I think the attached updated code gets you there. Instead of using the in-built interaction methods, I think it is easier given the way I have implemented a custom layout manager to use delegation: when the pan gesture recognizer selects a cell, we create a subview based on that word which moves with the gesture. At the same time in the background we remove the word from the data source and refresh the layout. When the user selects a location to place the word, we reverse that process, telling the delegate to insert a word into the data source and refresh the layout. If the user drags the word outside the collection view or to a non-valid location, the word is simply put back where it began (use the cunning technique of storing the original index as the label's tag).
Hope that helps you out!
[Text courtesy of Wikipedia]
import UIKit
class ViewController: UIViewController, bespokeCollectionViewControllerDelegate {
let sourceText : String = "So Midas, king of Lydia, swelled at first with pride when he found he could transform everything he touched to gold; but when he beheld his food grow rigid and his drink harden into golden ice then he understood that this gift was a bane and in his loathing for gold, cursed his prayer"
var sourceData : [String]! {
didSet {
refresh()
}
}
var sortedCVController : UICollectionViewController!
var sortedLayout : bespokeCollectionViewLayout!
var sortButton : UIButton!
var sortDirection : Int = 0
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
sortedLayout = bespokeCollectionViewLayout(contentWidth: view.frame.width - 200)
sourceData = {
let components = sourceText.components(separatedBy: " ")
return components
}()
sortedCVController = bespokeCollectionViewController(sourceData: sourceData, collectionViewLayout: sortedLayout, frame: CGRect(origin: CGPoint(x: 100, y: 100), size: CGSize(width: view.frame.width - 200, height: view.frame.height - 200)))
(sortedCVController as! bespokeCollectionViewController).delegate = self
sortedCVController.collectionView!.frame = CGRect(origin: CGPoint(x: 100, y: 100), size: CGSize(width: view.frame.width - 200, height: view.frame.height - 200))
sortButton = {
let sB : UIButton = UIButton(frame: CGRect(origin: CGPoint(x: 25, y: 100), size: CGSize(width: 50, height: 50)))
sB.setTitle("Sort", for: .normal)
sB.setTitleColor(UIColor.black, for: .normal)
sB.addTarget(self, action: #selector(sort), for: .touchUpInside)
sB.layer.borderColor = UIColor.black.cgColor
sB.layer.borderWidth = 1.0
return sB
}()
view.addSubview(sortedCVController.collectionView!)
view.addSubview(sortButton)
}
func refresh() -> Void {
let dimensions : [CGSize] = {
var d : [CGSize] = [CGSize]()
let font = UIFont.systemFont(ofSize: 17)
let fontAttributes = [NSFontAttributeName : font]
for item in sourceData {
let stringSize = ((item + " ") as NSString).size(attributes: fontAttributes)
d.append(CGSize(width: stringSize.width, height: stringSize.height))
}
return d
}()
if self.sortedLayout != nil {
sortedLayout.dimensions = dimensions
if let _ = sortedCVController {
(sortedCVController as! bespokeCollectionViewController).sourceData = sourceData
}
self.sortedLayout.cache.removeAll()
self.sortedLayout.prepare()
if let _ = self.sortedCVController {
self.sortedCVController.collectionView?.reloadData()
}
}
}
func sort() -> Void {
sourceData = sortDirection > 0 ? sourceData.sorted(by: { $0 > $1 }) : sourceData.sorted(by: { $0 < $1 })
sortDirection = sortDirection + 1 > 1 ? 0 : 1
}
func didMoveWord(atIndex: Int) {
sourceData.remove(at: atIndex)
}
func didPlaceWord(word: String, atIndex: Int) {
print(atIndex)
if atIndex >= sourceData.count {
sourceData.append(word)
}
else
{
sourceData.insert(word, at: atIndex)
}
}
func pleaseRefresh() {
refresh()
}
}
protocol bespokeCollectionViewControllerDelegate {
func didMoveWord(atIndex: Int) -> Void
func didPlaceWord(word: String, atIndex: Int) -> Void
func pleaseRefresh() -> Void
}
class bespokeCollectionViewController : UICollectionViewController {
var sourceData : [String]
var movingLabel : UILabel!
var initialOffset : CGPoint!
var delegate : bespokeCollectionViewControllerDelegate!
init(sourceData: [String], collectionViewLayout: bespokeCollectionViewLayout, frame: CGRect) {
self.sourceData = sourceData
super.init(collectionViewLayout: collectionViewLayout)
self.collectionView = UICollectionView(frame: frame, collectionViewLayout: collectionViewLayout)
self.collectionView?.backgroundColor = UIColor.white
self.collectionView?.layer.borderColor = UIColor.black.cgColor
self.collectionView?.layer.borderWidth = 1.0
self.installsStandardGestureForInteractiveMovement = false
let pangesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(gesture:)))
self.collectionView?.addGestureRecognizer(pangesture)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func handlePanGesture(gesture: UIPanGestureRecognizer) {
guard let _ = delegate else { return }
switch gesture.state {
case UIGestureRecognizerState.began:
guard let selectedIndexPath = self.collectionView?.indexPathForItem(at: gesture.location(in: self.collectionView)) else { break }
guard let selectedCell : UICollectionViewCell = self.collectionView?.cellForItem(at: selectedIndexPath) else { break }
initialOffset = gesture.location(in: selectedCell)
let index : Int = {
var i : Int = 0
for sectionCount in 0..<selectedIndexPath.section {
i += (self.collectionView?.numberOfItems(inSection: sectionCount))!
}
i += selectedIndexPath.row
return i
}()
movingLabel = {
let mL : UILabel = UILabel()
mL.font = UIFont.systemFont(ofSize: 17)
mL.frame = selectedCell.frame
mL.textColor = UIColor.black
mL.text = sourceData[index]
mL.layer.borderColor = UIColor.black.cgColor
mL.layer.borderWidth = 1.0
mL.backgroundColor = UIColor.white
mL.tag = index
return mL
}()
self.collectionView?.addSubview(movingLabel)
delegate.didMoveWord(atIndex: index)
case UIGestureRecognizerState.changed:
if let _ = movingLabel {
movingLabel.frame.origin = CGPoint(x: gesture.location(in: self.collectionView).x - initialOffset.x, y: gesture.location(in: self.collectionView).y - initialOffset.y)
}
case UIGestureRecognizerState.ended:
print("Interactive movement ended")
if let selectedIndexPath = self.collectionView?.indexPathForItem(at: gesture.location(in: self.collectionView)) {
guard let _ = movingLabel else { return }
let index : Int = {
var i : Int = 0
for sectionCount in 0..<selectedIndexPath.section {
i += (self.collectionView?.numberOfItems(inSection: sectionCount))!
}
i += selectedIndexPath.row
return i
}()
delegate.didPlaceWord(word: movingLabel.text!, atIndex: index)
UIView.animate(withDuration: 0.25, animations: {
self.movingLabel.alpha = 0
self.movingLabel.removeFromSuperview()
}, completion: { _ in
self.movingLabel = nil })
}
else
{
if let _ = movingLabel {
delegate.didPlaceWord(word: movingLabel.text!, atIndex: movingLabel.tag)
UIView.animate(withDuration: 0.25, animations: {
self.movingLabel.alpha = 0
self.movingLabel.removeFromSuperview()
}, completion: { _ in
self.movingLabel = nil })
}
}
default:
collectionView?.cancelInteractiveMovement()
print("Interactive movement canceled")
}
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
guard !(self.collectionViewLayout as! bespokeCollectionViewLayout).cache.isEmpty else { return 0 }
return (self.collectionViewLayout as! bespokeCollectionViewLayout).cache.last!.indexPath.section + 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
guard !(self.collectionViewLayout as! bespokeCollectionViewLayout).cache.isEmpty else { return 0 }
var n : Int = 0
for element in (self.collectionViewLayout as! bespokeCollectionViewLayout).cache {
if element.indexPath.section == section {
if element.indexPath.row > n {
n = element.indexPath.row
}
}
}
print("Section \(section) has \(n) elements")
return n + 1
}
override func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let change = sourceData[sourceIndexPath.row]
sourceData.remove(at: sourceIndexPath.row)
sourceData.insert(change, at: destinationIndexPath.row)
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
// Clean
for subview in cell.subviews {
subview.removeFromSuperview()
}
let label : UILabel = {
let l : UILabel = UILabel()
l.font = UIFont.systemFont(ofSize: 17)
l.frame = CGRect(origin: CGPoint.zero, size: cell.frame.size)
l.textColor = UIColor.black
let index : Int = {
var i : Int = 0
for sectionCount in 0..<indexPath.section {
i += (self.collectionView?.numberOfItems(inSection: sectionCount))!
}
i += indexPath.row
return i
}()
l.text = sourceData[index]
return l
}()
cell.addSubview(label)
return cell
}
}
class bespokeCollectionViewLayout : UICollectionViewLayout {
var cache : [UICollectionViewLayoutAttributes] = [UICollectionViewLayoutAttributes]()
let contentWidth: CGFloat
var dimensions : [CGSize]!
init(contentWidth: CGFloat) {
self.contentWidth = contentWidth
super.init()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepare() -> Void {
guard self.dimensions != nil else { return }
if cache.isEmpty {
var xOffset : CGFloat = 0
var yOffset : CGFloat = 0
var rowCount = 0
var wordCount : Int = 0
while wordCount < dimensions.count {
let nextRowCount : Int = {
var totalWidth : CGFloat = 0
var numberOfWordsInRow : Int = 0
while totalWidth < contentWidth && wordCount < dimensions.count {
if totalWidth + dimensions[wordCount].width >= contentWidth {
break
}
else
{
totalWidth += dimensions[wordCount].width
wordCount += 1
numberOfWordsInRow += 1
}
}
return numberOfWordsInRow
}()
var columnCount : Int = 0
for count in (wordCount - nextRowCount)..<wordCount {
let index : IndexPath = IndexPath(row: columnCount, section: rowCount)
let newAttribute : UICollectionViewLayoutAttributes = UICollectionViewLayoutAttributes(forCellWith: index)
let cellFrame : CGRect = CGRect(origin: CGPoint(x: xOffset, y: yOffset), size: dimensions[count])
newAttribute.frame = cellFrame
cache.append(newAttribute)
xOffset += dimensions[count].width
columnCount += 1
}
xOffset = 0
yOffset += dimensions[0].height
rowCount += 1
}
}
}
override var collectionViewContentSize: CGSize {
guard !cache.isEmpty else { return CGSize(width: 100, height: 100) }
return CGSize(width: self.contentWidth, height: cache.last!.frame.maxY)
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
if cache.isEmpty {
self.prepare()
}
for attributes in cache {
if attributes.frame.intersects(rect) {
layoutAttributes.append(attributes)
}
}
return layoutAttributes
}
}

how to prevent cell reuse of uicollectionview

I use UICollectionView and refer to this link implement scroll table view Fixed single column and row like below image.
but when I want to add shadow property, it display messy.
I think the reason is that I reuse cell when render visual view, but I don't know how to fix it :(
provide my code below, thanks for your time!
import Foundation
import SwiftyJSON
#objc(RNCollection)
class RNCollection : UICollectionView, UICollectionViewDataSource, UICollectionViewDelegate {
var contentCellIdentifier = "CellIdentifier"
var collectionView: UICollectionView!
static var dataSource = []
static var sections = 0
static var rows = 0
override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
super.init(frame: CGRectZero, collectionViewLayout: RNCollectionLayout())
self.registerClass(RNCollectionCell.self, forCellWithReuseIdentifier: contentCellIdentifier)
self.directionalLockEnabled = false
self.backgroundColor = UIColor.whiteColor()
self.delegate = self
self.dataSource = self
self.frame = frame
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setConfig(config: String!) {
var json: JSON = nil;
if let data = config.dataUsingEncoding(NSUTF8StringEncoding) {
json = JSON(data: data);
};
RNCollection.dataSource = json["dataSource"].arrayObject!
RNCollection.sections = json["sections"].intValue
RNCollection.rows = json["rows"].intValue
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let contentCell: RNCollectionCell = collectionView.dequeueReusableCellWithReuseIdentifier(contentCellIdentifier, forIndexPath: indexPath) as! RNCollectionCell
contentCell.textLabel.textColor = UIColor.blackColor()
if indexPath.section == 0 {
contentCell.backgroundColor = UIColor(red: 232/255.0, green: 232/255.0, blue: 232/255.0, alpha: 1.0)
if indexPath.row == 0 {
contentCell.textLabel.font = UIFont.systemFontOfSize(16)
contentCell.textLabel.text = (RNCollection.dataSource[0] as! NSArray)[0] as? String
} else {
contentCell.textLabel.font = UIFont.systemFontOfSize(14)
contentCell.textLabel.text = (RNCollection.dataSource[0] as! NSArray)[indexPath.row] as? String
}
} else {
contentCell.textLabel.font = UIFont.systemFontOfSize(12)
contentCell.backgroundColor = UIColor.whiteColor()
if(indexPath.section % 2 == 0) {
contentCell.backgroundColor = UIColor(red: 234/255.0, green: 234/255.0, blue: 236/255.0, alpha: 1.0)
}
if indexPath.row == 0 {
contentCell.textLabel.text = (RNCollection.dataSource[indexPath.section] as! NSArray)[0] as? String
contentCell.layer.shadowOffset = CGSize(width: 3, height: 3)
contentCell.layer.shadowOpacity = 0.7
contentCell.layer.shadowRadius = 2
} else {
contentCell.textLabel.text = (RNCollection.dataSource[indexPath.section] as! NSArray)[indexPath.row] as? String
}
}
return contentCell
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return RNCollection.rows
}
// MARK - UICollectionViewDataSource
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return RNCollection.sections
}
}
You are specifying your customizations for a certain section/row and in your else condition you just set the text. You should specify your required/default customization in your else condition. Consider this part of your code
if indexPath.row == 0 {
contentCell.textLabel.text = (RNCollection.dataSource[indexPath.section] as! NSArray)[0] as? String
contentCell.layer.shadowOffset = CGSize(width: 3, height: 3)
contentCell.layer.shadowOpacity = 0.7
contentCell.layer.shadowRadius = 2
} else {
contentCell.textLabel.text = (RNCollection.dataSource[indexPath.section] as! NSArray)[indexPath.row] as? String
}
You have specified your shadowOffset,shadowOpacity and shadowRadius in your if condition, but you ignored them in your else condition. If you specify the appearance in the else condition you won't have the problem you are facing now. It should look something like this
if indexPath.row == 0 {
contentCell.textLabel.text = (RNCollection.dataSource[indexPath.section] as! NSArray)[0] as? String
contentCell.layer.shadowOffset = CGSize(width: 3, height: 3)
contentCell.layer.shadowOpacity = 0.7
contentCell.layer.shadowRadius = 2
} else {
contentCell.textLabel.text = (RNCollection.dataSource[indexPath.section] as! NSArray)[indexPath.row] as? String
contentCell.layer.shadowOffset = 0
contentCell.layer.shadowOpacity = 0
contentCell.layer.shadowRadius = 0
}
If you reset everything properly in all your condition checks, this problem won't occur.
You set up shadow when your row is equal to 0:
if indexPath.row == 0 {...}
but if the row is not equal to zero collection view can reuse the cell when the shadow was already set. The solution is reset the shadow when row is not 0, in your code you can do something like this (see todo):
if indexPath.row == 0 { // Set up shadow here
contentCell.textLabel.text = (RNCollection.dataSource[indexPath.section] as! NSArray)[0] as? String
contentCell.layer.shadowOffset = CGSize(width: 3, height: 3)
contentCell.layer.shadowOpacity = 0.7
contentCell.layer.shadowRadius = 2
} else {
contentCell.textLabel.text = (RNCollection.dataSource[indexPath.section] as! NSArray)[indexPath.row] as? String
//TODO: Remove the shadow here
}
If you wanna save the cell setting everytime when your cell appears,you should set every possible condition of the method "func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell".Do not prevent the reuse of collectionView only if your data or items is few.
In Objective-C, but in Swift, the logic should be the same and not that hard to translate.
In RNCollectionCell.
//These enums may have a better nam, I agree
typedef enum : NSUInteger {
RNCollectionCellStyleColumnTitle,
RNCollectionCellStyleColumnValue,
RNCollectionCellStyleRowTitle,
RNCollectionCellStyleUpperLeftCorner,
} RNCollectionCellStyle;
-(void)titleIt:(NSString *)title forStyle:(RNCollectionCellStyle)style forSection:(NSUInteger)section
{
self.textLabel.textColor = [UIColor blackColor]; //Since it's always the same could be done elsewhere once for all.
//Note I didn't check completely if the case were good, and I didn't add the background color logic, but it should be done here too.
switch (style)
{
case RNCollectionCellStyleColumnTitle:
{
self.textLabel.font = [UIFont systemFontOfSize:12];
self.layer.shadowOffset = CGSizeMake(3, 3);
self.layer.shadowOpacity = 0.7;
self.layer.shadowRadius = 2;
}
break;
case RNCollectionCellStyleColumnValue:
{
self.textLabel.font = [UIFont systemFontOfSize:12];
self.layer.shadowOffset = CGSizeZero;
self.layer.shadowOpacity = 0;
self.layer.shadowRadius = 0;
}
break;
case RNCollectionCellStyleRowTitle:
{
self.textLabel.font = [UIFont systemFontOfSize:14];
self.layer.shadowOffset = CGSizeZero;
self.layer.shadowOpacity = 0;
self.layer.shadowRadius = 0;
}
break;
case RNCollectionCellStyleUpperLeftCorner:
{
self.textLabel.font = [UIFont systemFontOfSize:16];
self.layer.shadowOffset = CGSizeZero;
self.layer.shadowOpacity = 0;
self.layer.shadowRadius = 0;
}
break;
default:
break;
}
self.textLabel.text = title;
}
-(void)prepareForReuse
{
[super prepareForReuse];
self.layer.shadowOffset = CGSizeZero;
self.layer.shadowOpacity = 0;
self.layer.shadowRadius = 0;
}
In RNCollection:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(nonnull NSIndexPath *)indexPath
{
RNCollectionCell *contentCell = [collectionView dequeueReusableCellWithReuseIdentifier: contentCellIdentifier forIndexPath:indexPath];
//Here check what style you need to do
//Also, you seems to always do dataSource[indexPath.section][indexPath.row], you just use the "if test values".
[contentCell titleIt:dataSource[indexPath.section][indexPath.row]
forStyle:RNCollectionCellStyleColumnTitle
forSection:[indexPath section]];
return contentCell;
}
As said, I didn't check if all the cases were correct according to your needs.
But, you don't "prevent reuse of cell", it's optimized.
You need to "reset" values before reused or when you set the values. In theory the thing done in prepareForReuse should be enough and you shouldn't have to "reset" the layer effect in the switch, but I honestly didn't check it.

Resources