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?
Related
I’m using Spreadsheetview library in order to show case Jobber functionality.
There is one critical issues which is blocking project release. This is mentioned below:
Problem: Not able to display multi block content in custom cell.
Scenario: Suppose userA has task1 from 10:00 AM to 12:00 AM, userB has task2 from 10:00 AM to 11:00 AM, userC has task3 from 10:00 AM to 11:30 AM so these three task should be displayed in merged cell with one after another.
Refer below screenshot.
Code:
func spreadsheetView(_ spreadsheetView: SpreadsheetView, cellForItemAt indexPath: IndexPath) -> Cell? {
if self.jobDetails == nil {
return nil
}
. . .
//other cases handled like displaying time, date, visit person name which is not having any issue
. . .
else if case (1...(calenderData.count + 1), 2...(Constants.timeIntervals.count + 1)) = (indexPath.column, indexPath.row) {
let cell = spreadsheetView.dequeueReusableCell(withReuseIdentifier: String(describing: ScheduleCell1.self), for: indexPath) as! ScheduleCell1
if(cell.firstBlockLabel.text != nil) || (cell.secondBlockLabel.text != nil) || (cell.thirdBlockLabel.text != nil) {
return nil
}
let visits = calenderData[indexPath.column - 1].calendarRows
for visit in visits {
let diff = findTimeDifference(firstTime: cellTime,secondTime: visit.startTime)
print("startTime: \(visit.startTime) endTime \(visit.endTime) title \(visit.title) totalBlocks \(visit.totalBlocks)")
if(diff >= -30 && diff <= 30 && diff != -1) {
switch visit.totalBlocks {
case 0,1:
cell.firstBlockLabel.isHidden = false
cell.secondBlockLabel.isHidden = true
cell.thirdBlockLabel.isHidden = true
cell.firstBlockLabel.text = "1 - case 1"
if visit.blockSerialNo == 1 {
if(visit.statusCode.caseInsensitiveCompare("completed") == .orderedSame){
cell.firstBlockLabel.attributedText = "\(visit.title)".strikeThrough()
} else {
cell.firstBlockLabel.text = "\(visit.title)"
}
cell.firstBlockLabel.backgroundColor = hexStringToUIColor(hex: visit.statusTagProp.background)
cell.firstBlockLabel.textColor = hexStringToUIColor(hex: visit.statusTagProp.text)
}
case 2:
cell.firstBlockLabel.isHidden = false
cell.secondBlockLabel.isHidden = false
cell.thirdBlockLabel.isHidden = true
cell.firstBlockLabel.text = "1 - case 2"
cell.secondBlockLabel.text = "2 - case 2"
if visit.blockSerialNo == 2 {
if(visit.statusCode.caseInsensitiveCompare("completed") == .orderedSame){
cell.secondBlockLabel.attributedText = "\(visit.title)".strikeThrough()
} else {
cell.secondBlockLabel.text = "\(visit.title)"
}
cell.secondBlockLabel.backgroundColor = hexStringToUIColor(hex: visit.statusTagProp.background)
cell.secondBlockLabel.textColor = hexStringToUIColor(hex: visit.statusTagProp.text)
}
case 3:
cell.firstBlockLabel.isHidden = false
cell.secondBlockLabel.isHidden = false
cell.thirdBlockLabel.isHidden = false
cell.firstBlockLabel.text = "1 - case 3"
cell.secondBlockLabel.text = "2 - case 3"
cell.thirdBlockLabel.text = "3 - case 3"
if visit.blockSerialNo == 3 {
if(visit.statusCode.caseInsensitiveCompare("completed") == .orderedSame){
cell.thirdBlockLabel.attributedText = "\(visit.title)".strikeThrough()
} else {
cell.thirdBlockLabel.text = "\(visit.title)"
}
cell.thirdBlockLabel.backgroundColor = hexStringToUIColor(hex: visit.statusTagProp.background)
cell.thirdBlockLabel.textColor = hexStringToUIColor(hex: visit.statusTagProp.text)
}
default:
break
}
break
}
}
return cell
}
return nil
}
class ScheduleCell1: Cell {
let firstBlockLabel = UILabel()
let secondBlockLabel = UILabel()
let thirdBlockLabel = UILabel()
let stackview = UIStackView()
let lineLabel = UILabel()
var lineYPosition: Int = 0
override var frame: CGRect {
didSet {
firstBlockLabel.frame = CGRect(x: 0, y: 0, width: 500, height: 500)
secondBlockLabel.frame = CGRect(x: 0, y: 0, width: 500, height: 500)
thirdBlockLabel.frame = CGRect(x: 0, y: 0, width: 500, height: 500)
lineLabel.frame = bounds.insetBy(dx: 0, dy: 0)
lineLabel.frame = CGRect(x: 0, y: lineYPosition, width: 300, height: 1)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
lineLabel.frame = bounds
lineLabel.backgroundColor = .red
firstBlockLabel.textAlignment = .center
//firstBlockLabel.text = "firstBlockLabel"
firstBlockLabel.numberOfLines = 0
firstBlockLabel.lineBreakMode = .byTruncatingTail
firstBlockLabel.translatesAutoresizingMaskIntoConstraints = false
secondBlockLabel.textAlignment = .center
//secondBlockLabel.text = "secondBlockLabel"
secondBlockLabel.numberOfLines = 0
secondBlockLabel.lineBreakMode = .byTruncatingTail
secondBlockLabel.translatesAutoresizingMaskIntoConstraints = false
thirdBlockLabel.textAlignment = .center
//thirdBlockLabel.text = "thirdBlockLabel"
thirdBlockLabel.numberOfLines = 0
thirdBlockLabel.lineBreakMode = .byTruncatingTail
thirdBlockLabel.translatesAutoresizingMaskIntoConstraints = false
stackview.frame = bounds
stackview.axis = .horizontal
stackview.spacing = .leastNonzeroMagnitude
stackview.contentMode = .scaleToFill
stackview.translatesAutoresizingMaskIntoConstraints = false
stackview.alignment = .fill
stackview.distribution = .fill
stackview.distribution = .fillProportionally
stackview.addArrangedSubview(firstBlockLabel)
stackview.addArrangedSubview(secondBlockLabel)
stackview.addArrangedSubview(thirdBlockLabel)
firstBlockLabel.backgroundColor = .yellow
secondBlockLabel.backgroundColor = .purple
thirdBlockLabel.backgroundColor = .green
stackview.backgroundColor = .magenta
//contentView.backgroundColor = .magenta
contentView.addSubview(lineLabel)
contentView.bringSubviewToFront(lineLabel)
contentView.addSubview(stackview)
stackview.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
stackview.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
So I’ve added two property totalBlocks (determines how may blocks to be displayed) and BlockSrNo (determines which serial number of block). Logic for this is mentioned below:
func determineMultiBlocks() {
for data in calenderData {
let visits = data.calendarRows
for var i in 0..<visits.count {
let visit = visits[i]
for var j in (i+1)..<visits.count {
let nextVisit = visits[j]
let timeOverlapExists = CheckIfTimeExistBetweenTwoTimeInterval(withStartTime: visit.startTime.timeInSeconds, withEndTime: visit.endTime.timeInSeconds, withTimeToCheck: nextVisit.startTime.timeInSeconds)
if timeOverlapExists {
visit.totalBlocks = visit.totalBlocks + 1
nextVisit.totalBlocks = 0 //nextVisit.totalBlocks - 1
nextVisit.blockSerialNo = visit.totalBlocks
j = j + 1
}
}
break
}
Can help me where am I going wrong? If there is any other solution instead of using totalBlocks/blockSerialNo then let me know.
Appreciate all solutions!
I mean initially there is dummy data then data gets loaded.
I am using collection view for the EPG. I want to load the data dynamically and at the same time I want to set the collection view cell width dynamically.
For example please see the JIOTV app. I am trying to do same thing in my application.
Below is my code of the custom layout.
import UIKit
class CustomCollectionViewLayout: UICollectionViewLayout {
var numberOfColumns = 8
var shouldPinFirstColumn = true
var shouldPinFirstRow = true
var sectionNumber = 0
var itemAttributes = [[UICollectionViewLayoutAttributes]]()
var itemsSize = [CGSize]()
var contentSize: CGSize = .zero
var arr = [String]()
var generalArr = [[String]]()
var durationArr = [String]()
override func prepare() {
guard let collectionView = collectionView else {
return
}
let appDelegate = UIApplication.shared.delegate as! AppDelegate
numberOfColumns = appDelegate.timeArr.count//appDelegate.eventNameArr.count //appDelegate.multipleColumns
// print(numberOfColumns)
if appDelegate.generalArr.count > 0 {
durationArr = appDelegate.generalArr[0]
generalArr = appDelegate.generalArr
if collectionView.numberOfSections == 0 {
return
}
if itemAttributes.count != collectionView.numberOfSections {
generateItemAttributes(collectionView: collectionView)
return
}
for section in 0..<collectionView.numberOfSections {
for item in 0..<collectionView.numberOfItems(inSection: section) {
if section != 0 && item != 0 {
continue
}
let attributes = layoutAttributesForItem(at: IndexPath(item: item, section: section))!
if section == 0 {
var frame = attributes.frame
frame.origin.y = collectionView.contentOffset.y
attributes.frame = frame
}
if item == 0 {
var frame = attributes.frame
frame.origin.x = collectionView.contentOffset.x
attributes.frame = frame
}
}
}
}
}
override var collectionViewContentSize: CGSize
{
return contentSize
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return itemAttributes[indexPath.section][indexPath.row]
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var attributes = [UICollectionViewLayoutAttributes]()
for section in itemAttributes {
let filteredArray = section.filter { obj -> Bool in
return rect.intersects(obj.frame)
}
attributes.append(contentsOf: filteredArray)
}
return attributes
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
func convertHourtoMin(strTime : String) -> Int {
var components: Array = strTime.components(separatedBy: ":")
let hours = Int(components[0]) ?? 0
let minutes = Int(components[1]) ?? 0
let seconds = Int(components[2]) ?? 0
return ((hours * 60) + (minutes) + (seconds / 60))
}
}
// MARK: - Helpers
extension CustomCollectionViewLayout {
func generateItemAttributes(collectionView: UICollectionView) {
if itemsSize.count != numberOfColumns {
calculateItemSizes()
}
var column = 0
var xOffset: CGFloat = 0
var yOffset: CGFloat = 0
var contentWidth: CGFloat = 0
itemAttributes = []
for section in 0..<collectionView.numberOfSections {
var sectionAttributes: [UICollectionViewLayoutAttributes] = []
arr = generalArr[section]
// print("General Array : \(arr)")
// print("General Array count : \(arr.count)")
numberOfColumns = arr.count
durationArr = arr
let appDelegate = UIApplication.shared.delegate as! AppDelegate
for index in 0..<numberOfColumns {
var itemSize = itemsSize[index]
if numberOfColumns == appDelegate.timeArr.count {
itemSize = itemsSize[index]
}
else {
calculateItemSizes()
itemSize = itemsSize[index]
}
let indexPath = IndexPath(item: index, section: section)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = CGRect(x: xOffset, y: yOffset, width: itemSize.width, height: itemSize.height).integral
if section == 0 && index == 0 {
// First cell should be on top
attributes.zIndex = 1024
} else if section == 0 || index == 0 {
// First row/column should be above other cells
attributes.zIndex = 1023
}
// Below code with section == 0 and index till end
/* if section == 0 && 0 < numberOfColumns {
attributes.frame = CGRect(x: xOffset, y: yOffset, width: 100, height: 54).integral
}
*/
if section == 0 {
var frame = attributes.frame
frame.origin.y = collectionView.contentOffset.y
attributes.frame = frame
}
if index == 0 {
var frame = attributes.frame
frame.origin.x = collectionView.contentOffset.x
attributes.frame = frame
}
sectionAttributes.append(attributes)
xOffset += itemSize.width
column += 1
if column == numberOfColumns {
if xOffset > contentWidth {
contentWidth = xOffset
}
column = 0
xOffset = 0
yOffset += itemSize.height
}
}
itemAttributes.append(sectionAttributes)
}
if let attributes = itemAttributes.last?.last {
contentSize = CGSize(width: contentWidth, height: attributes.frame.maxY)
}
}
func calculateItemSizes() {
itemsSize = []
let appDelegate = UIApplication.shared.delegate as! AppDelegate
if numberOfColumns == appDelegate.timeArr.count{
for index in 0..<numberOfColumns {
itemsSize.append(sizeForItemWithColumnIndexA(index))
}
}
else {
for index in 0..<numberOfColumns {
itemsSize.append(sizeForItemWithColumnIndex(index))
}
}
}
func sizeForItemWithColumnIndex(_ columnIndex: Int) -> CGSize {
var text: NSString
switch columnIndex {
case 0: return CGSize(width: 80, height: 40) //54
// case 0: return CGSize(width: 106, height: 54)
// case 1:
// text = "MMM-99"
default:
text = "Content"
//return CGSize(width: 100, height: 54)
// for Stringresult in durationArr[columnIndex]
// Below is code to make the cell dynamic
var width:Float = Float(convertHourtoMin(strTime: durationArr[columnIndex]))
var actualWidth:Float = Float((width / 60) * 200) // * 100
actualWidth = actualWidth + actualWidth
// print("Actual Width : \(actualWidth)")
return CGSize(width: Int(actualWidth), height: 40) // 54
//}
}
// let size: CGSize = text.size(withAttributes: [kCTFontAttributeName as NSAttributedStringKey: UIFont.systemFont(ofSize: 14.0)])
// let width: CGFloat = size.width + 16
return CGSize(width: 100, height: 54)
}
// Below method is for time row in EPG VIEW
func sizeForItemWithColumnIndexA(_ columnIndex: Int) -> CGSize {
var text: NSString
switch columnIndex {
case 0: return CGSize(width: 80, height: 25)
// case 0: return CGSize(width: 106, height: 54)
// case 1:
// text = "MMM-99"
default:
text = "Content"
return CGSize(width: 200, height: 25) // originally width : 100
// for Stringresult in durationArr[columnIndex]
// Below is code to make the cell dynamic
var width:Float = Float(convertHourtoMin(strTime: durationArr[columnIndex]))
var actualWidth:Float = Float((width / 60) * 100)
actualWidth = actualWidth + actualWidth
// print("Actual Width : \(actualWidth)")
return CGSize(width: Int(actualWidth), height: 54)
//}
}
// let size: CGSize = text.size(withAttributes: [kCTFontAttributeName as NSAttributedStringKey: UIFont.systemFont(ofSize: 14.0)])
// let width: CGFloat = size.width + 16
return CGSize(width: 100, height: 35)
}
}
Check this out
For an EPG you will basically need to calculate the size of the cell and also it's position for each program for each channel.
I've created a custom data grid. It's based on collection view with custom layout. The layout modifies the first section and row attributes making them sticky, so when the user scrolls other rows and sections should go under the sticky ones. The idea for this layout is not mine, I've just adopted it. (I can't give the credits for the real creator, in my research I found so many variations of the layout that I'm not sure which is the original one).
Unfortunately I'm facing a problem with it. I'm getting a crash when scrolling:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'no
UICollectionViewLayoutAttributes instance for
-layoutAttributesForItemAtIndexPath:
Despite the message I think that the real problem is in layoutAttributesForElements method. I've read some threads with a similar problem, but the only working solution is to return all cached attributes, no matter of the passed rectangle. I just don't like quick and dirty solutions like this. I would really appreciate any ideas/solutions you can give me.
The whole project is here. However the most important is the layout so for convenience here it is:
class GridViewLayout: UICollectionViewLayout {
//MARK: - Setup
private var isInitialized: Bool = false
//MARK: - Attributes
var attributesList: [[UICollectionViewLayoutAttributes]] = []
//MARK: - Size
private static let defaultGridViewItemHeight: CGFloat = 47
private static let defaultGridViewItemWidth: CGFloat = 160
static let defaultGridViewRowHeaderWidth: CGFloat = 200
static let defaultGridViewColumnHeaderHeight: CGFloat = 80
static let defaultGridViewItemSize: CGSize =
CGSize(width: defaultGridViewItemWidth, height: defaultGridViewItemHeight)
// This is regular cell size
var itemSize: CGSize = defaultGridViewItemSize
// Row Header Size
var rowHeaderSize: CGSize =
CGSize(width: defaultGridViewRowHeaderWidth, height: defaultGridViewItemHeight)
// Column Header Size
var columnHeaderSize: CGSize =
CGSize(width: defaultGridViewItemWidth, height: defaultGridViewColumnHeaderHeight)
var contentSize : CGSize!
//MARK: - Layout
private var columnsCount: Int = 0
private var rowsCount: Int = 0
private var includesRowHeader: Bool = false
private var includesColumnHeader: Bool = false
override func prepare() {
super.prepare()
rowsCount = collectionView!.numberOfSections
if rowsCount == 0 { return }
columnsCount = collectionView!.numberOfItems(inSection: 0)
// make header row and header column sticky if needed
if self.attributesList.count > 0 {
for section in 0..<rowsCount {
for index in 0..<columnsCount {
if section != 0 && index != 0 {
continue
}
let attributes : UICollectionViewLayoutAttributes =
layoutAttributesForItem(at: IndexPath(forRow: section, inColumn: index))!
if includesColumnHeader && section == 0 {
var frame = attributes.frame
frame.origin.y = collectionView!.contentOffset.y
attributes.frame = frame
}
if includesRowHeader && index == 0 {
var frame = attributes.frame
frame.origin.x = collectionView!.contentOffset.x
attributes.frame = frame
}
}
}
return // no need for futher calculations
}
// Read once from delegate
if !isInitialized {
if let delegate = collectionView!.delegate as? UICollectionViewDelegateGridLayout {
// Calculate Item Sizes
let indexPath = IndexPath(forRow: 0, inColumn: 0)
let _itemSize = delegate.collectionView(collectionView!,
layout: self,
sizeForItemAt: indexPath)
let width = delegate.rowHeaderWidth(in: collectionView!,
layout: self)
let _rowHeaderSize = CGSize(width: width, height: _itemSize.height)
let height = delegate.columnHeaderHeight(in: collectionView!,
layout: self)
let _columnHeaderSize = CGSize(width: _itemSize.width, height: height)
if !__CGSizeEqualToSize(_itemSize, itemSize) {
itemSize = _itemSize
}
if !__CGSizeEqualToSize(_rowHeaderSize, rowHeaderSize) {
rowHeaderSize = _rowHeaderSize
}
if !__CGSizeEqualToSize(_columnHeaderSize, columnHeaderSize) {
columnHeaderSize = _columnHeaderSize
}
// Should enable sticky row and column headers
includesRowHeader = delegate.shouldIncludeHeaderRow(in: collectionView!)
includesColumnHeader = delegate.shouldIncludeHeaderColumn(in: collectionView!)
}
isInitialized = true
}
var column = 0
var xOffset : CGFloat = 0
var yOffset : CGFloat = 0
var contentWidth : CGFloat = 0
var contentHeight : CGFloat = 0
for section in 0..<rowsCount {
var sectionAttributes: [UICollectionViewLayoutAttributes] = []
for index in 0..<columnsCount {
var _itemSize: CGSize = .zero
switch (section, index) {
case (0, 0):
switch (includesRowHeader, includesColumnHeader) {
case (true, true):
_itemSize = CGSize(width: rowHeaderSize.width, height: columnHeaderSize.height)
case (false, true): _itemSize = columnHeaderSize
case (true, false): _itemSize = rowHeaderSize
default: _itemSize = itemSize
}
case (0, _):
if includesColumnHeader {
_itemSize = columnHeaderSize
} else {
_itemSize = itemSize
}
case (_, 0):
if includesRowHeader {
_itemSize = rowHeaderSize
} else {
_itemSize = itemSize
}
default: _itemSize = itemSize
}
let indexPath = IndexPath(forRow: section, inColumn: index)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = CGRect(x: xOffset,
y: yOffset,
width: _itemSize.width,
height: _itemSize.height).integral
// allow others cells to go under
if section == 0 && index == 0 { // top-left cell
attributes.zIndex = 1024
} else if section == 0 || index == 0 {
attributes.zIndex = 1023 // any ohter header cell
}
// sticky part - probably just in case here
if includesColumnHeader && section == 0 {
var frame = attributes.frame
frame.origin.y = collectionView!.contentOffset.y
attributes.frame = frame
}
if includesRowHeader && index == 0 {
var frame = attributes.frame
frame.origin.x = collectionView!.contentOffset.x
attributes.frame = frame
}
sectionAttributes.append(attributes)
xOffset += _itemSize.width
column += 1
if column == columnsCount {
if xOffset > contentWidth {
contentWidth = xOffset
}
column = 0
xOffset = 0
yOffset += _itemSize.height
}
}
attributesList.append(sectionAttributes)
}
let attributes = self.attributesList.last!.last!
contentHeight = attributes.frame.origin.y + attributes.frame.size.height
self.contentSize = CGSize(width: contentWidth,
height: contentHeight)
}
override var collectionViewContentSize: CGSize {
return self.contentSize
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
var curLayoutAttribute: UICollectionViewLayoutAttributes? = nil
if indexPath.section < self.attributesList.count {
let sectionAttributes = self.attributesList[indexPath.section]
if indexPath.row < sectionAttributes.count {
curLayoutAttribute = sectionAttributes[indexPath.row]
}
}
return curLayoutAttribute
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var attributes: [UICollectionViewLayoutAttributes] = []
for section in self.attributesList {
let filteredArray = section.filter({ (evaluatedObject) -> Bool in
return rect.intersects(evaluatedObject.frame)
})
attributes.append(contentsOf: filteredArray)
}
return attributes
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
//MARK: - Moving
override func layoutAttributesForInteractivelyMovingItem(at indexPath: IndexPath,
withTargetPosition position: CGPoint) -> UICollectionViewLayoutAttributes {
guard let dest = super.layoutAttributesForItem(at: indexPath as IndexPath)?.copy() as? UICollectionViewLayoutAttributes else { return UICollectionViewLayoutAttributes() }
dest.transform = CGAffineTransform(scaleX: 1.4, y: 1.4)
dest.alpha = 0.8
dest.center = position
return dest
}
override func invalidationContext(forInteractivelyMovingItems targetIndexPaths: [IndexPath],
withTargetPosition targetPosition: CGPoint,
previousIndexPaths: [IndexPath],
previousPosition: CGPoint) -> UICollectionViewLayoutInvalidationContext {
let context = super.invalidationContext(forInteractivelyMovingItems: targetIndexPaths,
withTargetPosition: targetPosition,
previousIndexPaths: previousIndexPaths,
previousPosition: previousPosition)
collectionView!.dataSource?.collectionView?(collectionView!,
moveItemAt: previousIndexPaths[0],
to: targetIndexPaths[0])
return context
}
}
Implement layoutAttributesForItemAtIndexPath. As per the documentation, "Subclasses must override this method and use it to return layout information for items in the collection view. ".
In my experience this method is normally not called when running in the simulator but can be called on the device. YMMV.
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 :)
How to add red dot on the top right side of the UITabBarItem.
I have searched a while and some guys said this can be done setting Badge Value of the UITabBarItem.But when I give it a try and set badge value to empty space " ",the red dot is somewhat big.How can I get a proper one?Big thanks.
If you want to avoid traversing subviews & potentially dangerous hacks in general, what I've done is set the badge's background colour to clear and used a styled bullet point to appear as a badge:
tabBarItem.badgeValue = "●"
tabBarItem.badgeColor = .clear
tabBarItem.setBadgeTextAttributes([NSAttributedStringKey.foregroundColor.rawValue: UIColor.red], for: .normal)
This seems more future-proof than the other answers.
you can try this method:
func addRedDotAtTabBarItemIndex(index: Int) {
for subview in tabBarController!.tabBar.subviews {
if let subview = subview as? UIView {
if subview.tag == 1314 {
subview.removeFromSuperview()
break
}
}
}
let RedDotRadius: CGFloat = 5
let RedDotDiameter = RedDotRadius * 2
let TopMargin:CGFloat = 5
let TabBarItemCount = CGFloat(self.tabBarController!.tabBar.items!.count)
let HalfItemWidth = CGRectGetWidth(view.bounds) / (TabBarItemCount * 2)
let xOffset = HalfItemWidth * CGFloat(index * 2 + 1)
let imageHalfWidth: CGFloat = (self.tabBarController!.tabBar.items![index] as! UITabBarItem).selectedImage.size.width / 2
let redDot = UIView(frame: CGRect(x: xOffset + imageHalfWidth, y: TopMargin, width: RedDotDiameter, height: RedDotDiameter))
redDot.tag = 1314
redDot.backgroundColor = UIColor.redColor()
redDot.layer.cornerRadius = RedDotRadius
self.tabBarController?.tabBar.addSubview(redDot)
}
set the badgeValue for your desired UITabBarItem as follow:
// for first tab
(tabBarController!.tabBar.items!.first! as! UITabBarItem).badgeValue = "1"
//for second tab
(tabBarController!.tabBar.items![1] as! UITabBarItem).badgeValue = "2"
// for last tab
(tabBarController!.tabBar.items!.last! as! UITabBarItem).badgeValue = "final"
for remove a badge from the UITabBarItem just assign nil
(tabBarController!.tabBar.items!.first! as! UITabBarItem).badgeValue = nil
you can get the output Like
for additional information please ref this link
Choice --2
var lbl : UILabel = UILabel(frame: CGRectMake(225, 5, 20, 20))
lbl.layer.borderColor = UIColor.whiteColor().CGColor
lbl.layer.borderWidth = 2
lbl.layer.cornerRadius = lbl.bounds.size.height/2
lbl.textAlignment = NSTextAlignment.Center
lbl.layer.masksToBounds = true
lbl.font = UIFont(name: hereaddyourFontName, size: 13)
lbl.textColor = UIColor.whiteColor()
lbl.backgroundColor = UIColor.redColor()
lbl.text = "1" //if you no need remove this
// add subview to tabBarController?.tabBar
self.tabBarController?.tabBar.addSubview(lbl)
the output is
That is very simple in current iOS versions
tabBarItem.badgeValue = " "
it shows the red dot on the top of the tabbar item
Swift 5+
This goes into the controller that belongs to the tab
alt. you just need to grab the right tabBarItem
func updateTabBarBadge(showDot: Bool) {
guard let tbi = tabBarItem else {
return
}
if showDot {
tbi.setBadgeTextAttributes([.font: UIFont.systemFont(ofSize: 6), .foregroundColor:UIColor(named: "Primary")!], for: .normal)
tbi.badgeValue = "⬤"
tbi.badgeColor = UIColor.clear
} else {
tbi.badgeValue = nil
}
}
I have figured out a hack solution.
func addRedDotAtTabBarItemIndex(index: Int,dotRadius: CGFloat) {
var tabBarButtons = [UIView]()
// find the UITabBarButton instance.
for subview in tabBarController!.tabBar.subviews.reverse() {
if subview.isKindOfClass(NSClassFromString("UITabBarButton")) {
tabBarButtons.append(subview as! UIView)
}
}
if index >= tabBarButtons.count {
println("out of bounds")
return
}
let tabBar = tabBarButtons[index]
var selectedImageWidth: CGFloat!
var topMargin: CGFloat!
for subview in tabBar.subviews {
if subview.isKindOfClass(NSClassFromString("UITabBarSwappableImageView")) {
selectedImageWidth = (subview as! UIView).frame.size.width
topMargin = (subview as! UIView).frame.origin.y
}
}
// remove existing red dot.
for subview in tabBar.subviews {
if subview.tag == 999 {
subview.removeFromSuperview()
}
}
let redDot = UIView(frame: CGRect(x: CGRectGetMidX(tabBar.bounds) + selectedImageWidth / 2 + dotRadius, y: topMargin, width: dotRadius * 2, height: dotRadius * 2))
redDot.backgroundColor = UIColor.redColor()
redDot.layer.cornerRadius = dotRadius // half of the view's height.
redDot.tag = 999
tabBar.addSubview(redDot)
}
Works both for iPad and iPhone.
Be able to hide and calculate index automatically.
Call self.setTabBarDotVisible(visible:true) if self is not an UITabBarController.
Call self.setTabBarDotVisible(visible:true, index:2) if self is an UITabBarController.
import UIKit
public extension UIViewController {
func setTabBarDotVisible(visible:Bool,index: Int? = nil) {
let tabBarController:UITabBarController!
if self is UITabBarController
{
tabBarController = self as! UITabBarController
}
else
{
if self.tabBarController == nil
{
return
}
tabBarController = self.tabBarController!
}
let indexFinal:Int
if (index != nil)
{
indexFinal = index!
}
else
{
let index3 = tabBarController.viewControllers?.index(of: self)
if index3 == nil
{
return;
}
else
{
indexFinal = index3!
}
}
guard let barItems = tabBarController.tabBar.items else
{
return
}
//
let tag = 8888
var tabBarItemView:UIView?
for subview in tabBarController.tabBar.subviews {
let className = String(describing: type(of: subview))
guard className == "UITabBarButton" else {
continue
}
var label:UILabel?
var dotView:UIView?
for subview2 in subview.subviews {
if subview2.tag == tag {
dotView = subview2;
}
else if (subview2 is UILabel)
{
label = subview2 as? UILabel
}
}
if label?.text == barItems[indexFinal].title
{
dotView?.removeFromSuperview()
tabBarItemView = subview;
break;
}
}
if (tabBarItemView == nil || !visible)
{
return
}
let barItemWidth = tabBarItemView!.bounds.width
let x = barItemWidth * 0.5 + (barItems[indexFinal].selectedImage?.size.width ?? barItemWidth) / 2
let y:CGFloat = 5
let size:CGFloat = 10;
let redDot = UIView(frame: CGRect(x: x, y: y, width: size, height: size))
redDot.tag = tag
redDot.backgroundColor = UIColor.red
redDot.layer.cornerRadius = size/2
tabBarItemView!.addSubview(redDot)
}
}
i test this question's answer. but not work on iPad.
now i found that, when u add this on iPhone, tabBarItem left and right margin is 2, and each items margin is 4. Code as below:
NSInteger barItemCount = self.tabBar.items.count;
UITabBarItem *barItem = (UITabBarItem *)self.tabBar.items[index];
CGFloat imageHalfWidth = barItem.image.size.width / 2.0;
CGFloat barItemWidth = (BXS_WINDOW_WIDTH - barItemCount * 4) / barItemCount;
CGFloat barItemMargin = 4;
CGFloat redDotXOffset = barItemMargin / 2 + barItemMargin * index + barItemWidth * (index + 0.5);
and iPad as below:
barItemWidth = 76;
barItemMargin = 34;
redDotXOffset = (BXS_WINDOW_WIDTH - 76 * barItemCount - 34 * (barItemCount - 1)) / 2.0 + 76 * (index + 0.5) + 34 * index;
Hope this is useful.
This it Swift 4 solution:
1) Add BaseTabBar custom class to your project:
import UIKit
class BaseTabBar: UITabBar {
static var dotColor: UIColor = UIColor.red
static var dotSize: CGFloat = 4
static var dotPositionX: CGFloat = 0.8
static var dotPositionY: CGFloat = 0.2
var dotMap = [Int: Bool]()
func resetDots() {
dotMap.removeAll()
}
func addDot(tabIndex: Int) {
dotMap[tabIndex] = true
}
func removeDot(tabIndex: Int) {
dotMap[tabIndex] = false
}
override func draw(_ rect: CGRect) {
super.draw(rect)
if let items = items {
for i in 0..<items.count {
let item = items[i]
if let view = item.value(forKey: "view") as? UIView, let dotBoolean = dotMap[i], dotBoolean == true {
let x = view.frame.origin.x + view.frame.width * BaseTabBar.dotPositionX
let y = view.frame.origin.y + view.frame.height * BaseTabBar.dotPositionY
let dotPath = UIBezierPath(ovalIn: CGRect(x: x, y: y, width: BaseTabBar.dotSize, height: BaseTabBar.dotSize))
BaseTabBar.dotColor.setFill()
dotPath.fill()
}
}
}
}
}
2) Change the custom class of UITabBar inside your UITabBarController to BaseTabBar.
3) Manage the dots in the place where you can access the tabBarController
func updateNotificationCount(count: Int) {
if let tabBar = navigationController?.tabBarController?.tabBar as? BaseTabBar {
if count > 0 {
tabBar.addDot(tabIndex: 0)
} else {
tabBar.removeDot(tabIndex: 0)
}
tabBar.setNeedsDisplay()
}
}
I added 5 tab bar indexes and add the dot points according to the notification occurs. First, create Dots view array.
var Dots = [UIView](repeating: UIView(), count: 5)
func addRedDotAtTabBarItemIndex(index: Int) {
if self.Dots[index].tag != index {
let RedDotRadius: CGFloat = 7
let RedDotDiameter = RedDotRadius
let TopMargin:CGFloat = 2
let tabSize = self.tabBarController.view.frame.width / CGFloat(5)
let xPosition = tabSize * CGFloat(index - 1)
let tabHalfWidth: CGFloat = tabSize / 2
self.Dots[index] = UIView(frame: CGRect(x: xPosition + tabHalfWidth - 2 , y: TopMargin, width: RedDotDiameter, height: RedDotDiameter))
self.Dots[index].tag = index
self.Dots[index].backgroundColor = UIColor.red
self.Dots[index].layer.cornerRadius = RedDotRadius
self.tabBarController.tabBar.addSubview(self.Dots[index])
}
}
If you want to remove the dot from selected index, use this code:
func removeRedDotAtTabBarItemIndex(index: Int) {
self.Dots[index].removeFromSuperview()
self.Dots[index].tag = 0
}
simple solution
set space in storyboard tabbaritem badge value.
if we add space below output you can get:
In Swift 5:
tabBarItem.badgeValue = "1"
to change from default color use:
tabBarItem.badgeColor = UIColor.systemBlue
From iOS 13, use UITabBarAppearance and UITabBarItemAppearance
let appearance = UITabBarAppearance()
let itemAppearance = UITabBarItemAppearance(style: .stacked)
itemAppearance.normal.badgeBackgroundColor = .clear
itemAppearance.normal.badgeTextAttributes = [.foregroundColor: UIColor.red]
profileViewController.tabBarItem.badgeValue = "●"