Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save eugene-d/c62e00f54467fcd85a7e644d5e85322a to your computer and use it in GitHub Desktop.
Save eugene-d/c62e00f54467fcd85a7e644d5e85322a to your computer and use it in GitHub Desktop.
A Swift implement of objc.io's CollectionView Layout Demo project https://github.com/objcio/issue-3-collection-view-layouts
// Playground - noun: a place where people can play
import UIKit
class SampleCalendarEvent:NSObject{
let title = "Event\(random()%10000)"
let day = random()%7
let startHour = random()%20
let durationInHours = random()%5 + 1
override init() {
super.init()
}
func dayBetweenMinDayIndex(minDayIndex:Int,maxDayIndex:Int) -> Bool{
return day >= minDayIndex && day <= maxDayIndex
}
func hourBetweenMinStartHour(minStartHour:Int,maxStartHour:Int) -> Bool{
return startHour >= minStartHour && startHour <= maxStartHour
}
override var description : String{
return "\(title)-\(day) \(startHour)"
}
}
class HeaderView: UICollectionReusableView {
class var identifier:String{
return "HeaderView"
}
struct HeaderKinds {
static let dayHeaderKind = "DayHeaderView"
static let hourHeaderKind = "HourHeaderView"
}
let titleLabel = UILabel()
let bottomBorderLayer = CALayer()
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
setup()
}
func setup(){
titleLabel.setTranslatesAutoresizingMaskIntoConstraints(false)
titleLabel.font = UIFont.systemFontOfSize(12)
titleLabel.numberOfLines = 0
addSubview(titleLabel)
addConstraints()
let bottomBorderHeight:CGFloat = 1
bottomBorderLayer.frame = CGRect(x: 0, y: frame.height - bottomBorderHeight, width: frame.width, height: bottomBorderHeight)
bottomBorderLayer.backgroundColor = UIColor.blackColor().CGColor
layer.addSublayer(bottomBorderLayer)
}
func addConstraints(){
let constraints = NSLayoutConstraint.constraintsWithVisualFormat("H:|-10-[title]-10-|",
options: NSLayoutFormatOptions.DirectionLeadingToTrailing,metrics: nil,
views: ["title":titleLabel])
addConstraints(constraints)
let constraint = NSLayoutConstraint(item: titleLabel, attribute: .CenterY, relatedBy: .Equal, toItem: self, attribute: .CenterY, multiplier: 1, constant: 0)
addConstraint(constraint)
}
func bind(kind:String,indexPath:NSIndexPath){
if kind == HeaderKinds.dayHeaderKind{
titleLabel.text = "Day \(indexPath.item + 1)"
}else if kind == HeaderKinds.hourHeaderKind{
titleLabel.text = "\(indexPath.item + 1):00"
}
}
}
class CalendarEventCell: UICollectionViewCell {
class var identifier:String{
return "CalendarEventCell"
}
let titleLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
setup()
}
func setup(){
layer.cornerRadius = 10
layer.borderWidth = 1.0
layer.borderColor = UIColor(red: 0, green: 0, blue: 0.7, alpha: 1).CGColor
backgroundColor = UIColor(red: 164/255, green: 215/255, blue: 1, alpha: 1)
titleLabel.setTranslatesAutoresizingMaskIntoConstraints(false)
titleLabel.font = UIFont.boldSystemFontOfSize(12)
titleLabel.textColor = UIColor(red: 0, green: 64/255, blue: 128/255, alpha: 1)
titleLabel.numberOfLines = 0
addSubview(titleLabel)
addConstraints()
}
func addConstraints(){
let constraints = NSLayoutConstraint.constraintsWithVisualFormat("H:|-10-[title]-10-|",
options: NSLayoutFormatOptions.DirectionLeadingToTrailing,metrics: nil,
views: ["title":titleLabel])
addConstraints(constraints)
let vConstraints = NSLayoutConstraint.constraintsWithVisualFormat("V:|-10-[title]-(>=8)-|",
options: nil, metrics: nil,
views: ["title":titleLabel])
addConstraints(vConstraints)
}
func bind(event:SampleCalendarEvent){
titleLabel.text = event.title
layoutIfNeeded()
}
}
class CalendarDataSource:NSObject,UICollectionViewDataSource {
var events:[SampleCalendarEvent] = []
override init() {
super.init()
generateSampleData()
}
// MARK: Helper
func generateSampleData(){
var maxCount = 20 + random() % 6
while maxCount > 0{
events.append(SampleCalendarEvent())
maxCount--
}
}
func eventAtIndexPath(indexPath:NSIndexPath) ->SampleCalendarEvent{
return events[indexPath.item]
}
func indexPathsOfEventsBetweenMinDayIndex(minDayIndex:Int
,maxDayIndex:Int
,minStartHour:Int
,maxStartHour:Int) -> [NSIndexPath]{
var indexPaths:[NSIndexPath] = []
var index = 0
for event in events{
if event.dayBetweenMinDayIndex(minDayIndex, maxDayIndex: maxDayIndex)
&& event.hourBetweenMinStartHour(minStartHour, maxStartHour: maxStartHour){
indexPaths.append(NSIndexPath(forItem: index, inSection: 0))
}
index++
}
return indexPaths
}
// MARK:UICollectionViewDataSource
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int{
return events.count
}
// The cell that is returned must be retrieved from a call to -dequeueReusableCellWithReuseIdentifier:forIndexPath:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell{
let eventCell = collectionView.dequeueReusableCellWithReuseIdentifier(CalendarEventCell.identifier, forIndexPath: indexPath) as CalendarEventCell
eventCell.bind(events[indexPath.item])
return eventCell
}
// The view that is returned must be retrieved from a call to -dequeueReusableSupplementaryViewOfKind:withReuseIdentifier:forIndexPath:
func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView{
let headerView = collectionView.dequeueReusableSupplementaryViewOfKind(kind, withReuseIdentifier: HeaderView.identifier, forIndexPath: indexPath) as HeaderView
headerView.bind(kind, indexPath: indexPath)
return headerView
}
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int{
return 1
}
}
let DaysPerWeek:CGFloat = 7
let HoursPerDay : CGFloat = 24
let HorizontalSpacing :CGFloat = 10
let HeightPerHour : CGFloat = 50
let DayHeaderHeight : CGFloat = 40
let HourHeaderWidth : CGFloat = 60
class WeekCalendarLayout: UICollectionViewLayout {
var widthPerDay: CGFloat{
let totalWidth = collectionViewContentSize().width - HourHeaderWidth
let widthPerDay = totalWidth / DaysPerWeek
return widthPerDay
}
override func collectionViewContentSize() -> CGSize {
// Dont't scroll horizontally
let contentWidth = collectionView!.bounds.width
let contentHeight = DayHeaderHeight + (HeightPerHour * HoursPerDay)
return CGSize(width: contentWidth, height: contentHeight)
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
var attrs:[AnyObject] = []
// Cells
let visibleIndexPaths = indexPathsOfItemsInRect(rect)
for indexPath in visibleIndexPaths{
let attr = layoutAttributesForItemAtIndexPath(indexPath)
attrs.append(attr)
}
// Supplementary views
let dayHeaderViewIndexPaths = indexPathsOfDayHeaderViewsInRect(rect)
for indexPath in dayHeaderViewIndexPaths{
let attr = layoutAttributesForSupplementaryViewOfKind(HeaderView.HeaderKinds.dayHeaderKind, atIndexPath: indexPath)
attrs.append(attr)
}
let hourHeadViewIndexPaths = indexPathsOfHourHeaderViewsInRect(rect)
for indexPath in hourHeadViewIndexPaths{
let attr = layoutAttributesForSupplementaryViewOfKind(HeaderView.HeaderKinds.hourHeaderKind, atIndexPath: indexPath)
attrs.append(attr)
}
return attrs
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
let dataSource = collectionView!.dataSource as CalendarDataSource
let event = dataSource.eventAtIndexPath(indexPath)
var attrs = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
attrs.frame = frameForEvent(event)
return attrs
}
override func layoutAttributesForSupplementaryViewOfKind(elementKind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
var attrs = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: elementKind, withIndexPath: indexPath)
if elementKind == HeaderView.HeaderKinds.dayHeaderKind {
let x = HourHeaderWidth + widthPerDay * CGFloat(indexPath.item)
attrs.frame = CGRect(x: x, y: 0, width: widthPerDay, height: DayHeaderHeight)
attrs.zIndex = -10
}else if elementKind == HeaderView.HeaderKinds.hourHeaderKind{
let y = DayHeaderHeight + HeightPerHour * CGFloat(indexPath.item)
attrs.frame = CGRect(x: 0, y: y, width: collectionViewContentSize().width, height: HeightPerHour)
attrs.zIndex = -20
}
return attrs
}
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
return collectionView!.bounds.width != newBounds.width
}
// MARK: Helpers
func indexPathsOfItemsInRect(rect:CGRect) -> [NSIndexPath]{
let minVisibleDay = dayIndexFromXCoordinate(rect.minX)
let maxVisibleDay = dayIndexFromXCoordinate(rect.maxX)
let minVisibleHour = hourIndexFromYCoordiante(rect.minY)
let maxVisibleHour = hourIndexFromYCoordiante(rect.maxY)
NSLog("rect:\(rect),days:\(minVisibleDay)-\(maxVisibleDay),hours:\(minVisibleHour)-\(maxVisibleHour)")
if let dataSource = collectionView?.dataSource as? CalendarDataSource{
return dataSource.indexPathsOfEventsBetweenMinDayIndex(minVisibleDay,
maxDayIndex: maxVisibleDay,
minStartHour: minVisibleHour,
maxStartHour: maxVisibleHour)
}else{
return []
}
}
func dayIndexFromXCoordinate(xPosition:CGFloat) -> Int{
let dayIndex = (xPosition - HourHeaderWidth) / widthPerDay
return max(0, Int(dayIndex))
}
func hourIndexFromYCoordiante(yPosition:CGFloat) -> Int{
let hourIndex = (yPosition - DayHeaderHeight) / HeightPerHour
return max(0,Int(hourIndex))
}
func indexPathsOfDayHeaderViewsInRect(rect:CGRect) -> [NSIndexPath]{
if rect.minY > DayHeaderHeight{
return []
}
let minDayIndex = dayIndexFromXCoordinate(rect.minX)
let maxDayIndex = dayIndexFromXCoordinate(rect.maxX)
var indexPaths:[NSIndexPath] = []
for index in Range(start:minDayIndex,end:(maxDayIndex + 1)){
indexPaths.append(NSIndexPath(forItem: index, inSection: 0))
}
return indexPaths
}
func indexPathsOfHourHeaderViewsInRect(rect:CGRect) -> [NSIndexPath]{
if rect.minX > HourHeaderWidth{
return []
}
let minHourIndex = hourIndexFromYCoordiante(rect.minY)
let maxHourIndex = hourIndexFromYCoordiante(rect.maxY)
var indexPaths:[NSIndexPath] = []
for index in Range(start: minHourIndex, end: (maxHourIndex + 1)){
indexPaths.append(NSIndexPath(forItem: index, inSection: 0))
}
return indexPaths
}
func frameForEvent(event:SampleCalendarEvent) -> CGRect{
let x = HourHeaderWidth + widthPerDay * CGFloat(event.day)
let y = DayHeaderHeight + HeightPerHour * CGFloat(event.startHour)
let height = CGFloat(event.durationInHours) * HeightPerHour
return CGRect(x: x, y: y, width: widthPerDay, height: height)
}
}
let dataSource = CalendarDataSource()
let calendarLayout = WeekCalendarLayout()
let calendar = UICollectionView(frame: CGRect(x: 0, y: 0, width: 640, height: 640),collectionViewLayout: UICollectionViewFlowLayout())
calendar.backgroundColor = UIColor.whiteColor()
calendar.registerClass(CalendarEventCell.classForCoder(), forCellWithReuseIdentifier: CalendarEventCell.identifier)
calendar.registerClass(HeaderView.classForCoder(), forSupplementaryViewOfKind: HeaderView.HeaderKinds.dayHeaderKind, withReuseIdentifier: HeaderView.identifier)
calendar.registerClass(HeaderView.classForCoder(), forSupplementaryViewOfKind: HeaderView.HeaderKinds.hourHeaderKind, withReuseIdentifier: HeaderView.identifier)
calendar.dataSource = dataSource
calendar.collectionViewLayout = calendarLayout
calendar
//let cell = CalendarEventCell(frame: CGRect(x: 0, y: 0, width: 60, height: 80))
//cell.titleLabel.text = "Event 101"
//cell.layoutIfNeeded()
//cell
//let header = HeaderView(frame: CGRect(x: 0, y: 0, width: 100, height: 40))
//header.titleLabel.text = "Day 1"
//header.backgroundColor = UIColor.whiteColor()
//header.layoutIfNeeded()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment