Last active
December 12, 2016 11:01
-
-
Save codetalks-new/baca283708e005039f84 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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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