diff --git a/QVRWeekView.podspec b/QVRWeekView.podspec index ef17c04..72d4778 100644 --- a/QVRWeekView.podspec +++ b/QVRWeekView.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'QVRWeekView' -s.version = '0.14.2' +s.version = '0.15.0' s.summary = 'QVRWeekView is a simple calendar week view with support for horizontal, vertical scrolling and zooming.' s.swift_version = '5' diff --git a/QVRWeekView/Assets/.gitkeep b/QVRWeekView/Assets/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/QVRWeekView/Classes/Common/Constants.swift b/QVRWeekView/Classes/Common/Constants.swift index 7766c2f..3388be6 100644 --- a/QVRWeekView/Classes/Common/Constants.swift +++ b/QVRWeekView/Classes/Common/Constants.swift @@ -119,6 +119,19 @@ struct LayoutDefaults { static let passedWeekendDayViewColor = UIColor(red: 228/255, green: 228/255, blue: 228/255, alpha: 1.0) // Color for today's view cell. static let todayViewColor = defaultDayViewColor + + // MARK: - TAG DEFAULTS - + + // Default tag height + static let tagHeight = CGFloat(18) + // Default tag spacing (horizontal gap between tags) + static let tagSpacing = CGFloat(4) + // Default tag corner radius + static let tagCornerRadius = CGFloat(9) + // Default tag text size + static let tagTextSize = CGFloat(10) + // Default tag vertical margin (distance from bottom of event) + static let tagVerticalMargin = CGFloat(4) } struct NibNames { diff --git a/QVRWeekView/Classes/Common/Customization.swift b/QVRWeekView/Classes/Common/Customization.swift index 41f4862..82776d4 100644 --- a/QVRWeekView/Classes/Common/Customization.swift +++ b/QVRWeekView/Classes/Common/Customization.swift @@ -482,4 +482,66 @@ public extension WeekView { self.dayScrollView.horizontalScrolling = option } } + + // MARK: - TAG CUSTOMIZATION - + + /** + Height of tags in event cells. + */ + @objc var dayViewTagHeight: CGFloat { + get { + return self.dayScrollView.dayViewCellLayout.tagHeight + } + set(height) { + self.dayScrollView.dayViewCellLayout.tagHeight = height + } + } + + /** + Horizontal spacing between tags. + */ + @objc var dayViewTagSpacing: CGFloat { + get { + return self.dayScrollView.dayViewCellLayout.tagSpacing + } + set(spacing) { + self.dayScrollView.dayViewCellLayout.tagSpacing = spacing + } + } + + /** + Corner radius of tags. + */ + @objc var dayViewTagCornerRadius: CGFloat { + get { + return self.dayScrollView.dayViewCellLayout.tagCornerRadius + } + set(radius) { + self.dayScrollView.dayViewCellLayout.tagCornerRadius = radius + } + } + + /** + Font size of tag text. + */ + @objc var dayViewTagTextSize: CGFloat { + get { + return self.dayScrollView.dayViewCellLayout.tagTextSize + } + set(size) { + self.dayScrollView.dayViewCellLayout.tagTextSize = size + } + } + + /** + Vertical margin between tag and bottom of event. + */ + @objc var dayViewTagVerticalMargin: CGFloat { + get { + return self.dayScrollView.dayViewCellLayout.tagVerticalMargin + } + set(margin) { + self.dayScrollView.dayViewCellLayout.tagVerticalMargin = margin + } + } } diff --git a/QVRWeekView/Classes/Common/DayViewCellLayout.swift b/QVRWeekView/Classes/Common/DayViewCellLayout.swift index af08d9e..317f620 100644 --- a/QVRWeekView/Classes/Common/DayViewCellLayout.swift +++ b/QVRWeekView/Classes/Common/DayViewCellLayout.swift @@ -63,4 +63,17 @@ class DayViewCellLayout { var previewEventMinutePrecision: Double = LayoutDefaults.previewEventPrecisionInMinutes { didSet { update?() } } // Height of the preview event in hours. var previewEventHourHeight: Double = LayoutDefaults.previewEventHeightInHours { didSet { update?() } } + + // MARK: - TAG PROPERTIES - + + // Height of tags + var tagHeight: CGFloat = LayoutDefaults.tagHeight { didSet { update?() } } + // Horizontal spacing between tags + var tagSpacing: CGFloat = LayoutDefaults.tagSpacing { didSet { update?() } } + // Corner radius of tags + var tagCornerRadius: CGFloat = LayoutDefaults.tagCornerRadius { didSet { update?() } } + // Font size of tag text + var tagTextSize: CGFloat = LayoutDefaults.tagTextSize { didSet { update?() } } + // Vertical margin between tag and bottom of event + var tagVerticalMargin: CGFloat = LayoutDefaults.tagVerticalMargin { didSet { update?() } } } diff --git a/QVRWeekView/Classes/Common/EventData.swift b/QVRWeekView/Classes/Common/EventData.swift index c691310..1f5beca 100644 --- a/QVRWeekView/Classes/Common/EventData.swift +++ b/QVRWeekView/Classes/Common/EventData.swift @@ -8,6 +8,19 @@ import Foundation +/** + Represents a single tag with its associated color + */ +public struct EventTag { + public let name: String + public let color: UIColor + + public init(name: String, color: UIColor) { + self.name = name + self.color = color + } +} + /** Class event data stores basic data needed by the rest of the code to calculate and draw events in the dayViewCells in the dayScrollView. */ @@ -26,6 +39,8 @@ open class EventData: NSObject, NSCoding { public let color: UIColor // Stores if event is an all day event public let allDay: Bool + // Tags associated with the event with their colors + public let eventTags: [EventTag] // Stores an optional gradient layer which will be used to draw event. Can only be set once. private(set) var gradientLayer: CAGradientLayer? { didSet { gradientLayer = oldValue ?? gradientLayer } } @@ -37,12 +52,13 @@ open class EventData: NSObject, NSCoding { /** Main initializer. All properties. */ - public init(id: String, title: String, startDate: Date, endDate: Date, location: String, color: UIColor, allDay: Bool, gradientLayer: CAGradientLayer? = nil) { + public init(id: String, title: String, startDate: Date, endDate: Date, location: String, color: UIColor, allDay: Bool, eventTags: [EventTag] = [], gradientLayer: CAGradientLayer? = nil) { self.id = id self.title = title self.location = location self.color = color self.allDay = allDay + self.eventTags = eventTags guard startDate.compare(endDate).rawValue <= 0 else { self.startDate = startDate self.endDate = startDate @@ -119,6 +135,9 @@ open class EventData: NSObject, NSCoding { coder.encode(location, forKey: EventDataEncoderKey.location) coder.encode(color, forKey: EventDataEncoderKey.color) coder.encode(allDay, forKey: EventDataEncoderKey.allDay) + // Encode tags as flat array [name, color, name, color, ...] + let flatTags = eventTags.flatMap { [$0.name, $0.color] as [Any] } + coder.encode(flatTags, forKey: EventDataEncoderKey.tags) coder.encode(gradientLayer, forKey: EventDataEncoderKey.gradientLayer) } @@ -131,6 +150,14 @@ open class EventData: NSObject, NSCoding { let dColor = coder.decodeObject(forKey: EventDataEncoderKey.color) as? UIColor { let dGradientLayer = coder.decodeObject(forKey: EventDataEncoderKey.gradientLayer) as? CAGradientLayer let dAllDay = coder.decodeBool(forKey: EventDataEncoderKey.allDay) + // Decode tags from flat array [name, color, name, color, ...] + let flatTags = coder.decodeObject(forKey: EventDataEncoderKey.tags) as? [Any] ?? [] + var eventTags: [EventTag] = [] + stride(from: 0, to: flatTags.count - 1, by: 2).forEach { i in + if let name = flatTags[i] as? String, let color = flatTags[i+1] as? UIColor { + eventTags.append(EventTag(name: name, color: color)) + } + } self.init(id: dId, title: dTitle, startDate: dStartDate, @@ -138,6 +165,7 @@ open class EventData: NSObject, NSCoding { location: dLocation, color: dColor, allDay: dAllDay, + eventTags: eventTags, gradientLayer: dGradientLayer) } else { return nil @@ -152,7 +180,13 @@ open class EventData: NSObject, NSCoding { (lhs.title == rhs.title) && (lhs.location == rhs.location) && (lhs.allDay == rhs.allDay) && - (lhs.color.isEqual(rhs.color)) + (lhs.color.isEqual(rhs.color)) && + (lhs.eventTags.count == rhs.eventTags.count) && + (lhs.eventTags.enumerated().allSatisfy { i, tag in + i < rhs.eventTags.count && + tag.name == rhs.eventTags[i].name && + tag.color.isEqual(rhs.eventTags[i].color) + }) } public override var hash: Int { @@ -167,8 +201,13 @@ open class EventData: NSObject, NSCoding { open func getDisplayString(withMainFont mainFont: UIFont, infoFont: UIFont, andColor color: UIColor) -> NSAttributedString { let df = DateFormatter() df.dateFormat = "HH:mm" - let mainFontAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: mainFont, NSAttributedString.Key.foregroundColor: color.cgColor] - let infoFontAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: infoFont, NSAttributedString.Key.foregroundColor: color.cgColor] + + // Use Montserrat Bold for title, Montserrat Medium for description + let titleFont = UIFont(name: "Montserrat-Bold", size: 12) ?? UIFont.boldSystemFont(ofSize: 12) + let descFont = UIFont(name: "Montserrat-Medium", size: 10) ?? UIFont.systemFont(ofSize: 10, weight: .medium) + + let mainFontAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: titleFont, NSAttributedString.Key.foregroundColor: UIColor.white] + let infoFontAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: descFont, NSAttributedString.Key.foregroundColor: UIColor.white] let mainAttributedString = NSMutableAttributedString(string: self.title, attributes: mainFontAttributes) if !self.allDay { mainAttributedString.append(NSMutableAttributedString( @@ -204,19 +243,19 @@ open class EventData: NSObject, NSCoding { } public func remakeEventData(withStart start: Date, andEnd end: Date) -> EventData { - let newEvent = EventData(id: self.id, title: self.title, startDate: start, endDate: end, location: self.location, color: self.color, allDay: self.allDay) + let newEvent = EventData(id: self.id, title: self.title, startDate: start, endDate: end, location: self.location, color: self.color, allDay: self.allDay, eventTags: self.eventTags) newEvent.configureGradient(self.gradientLayer) return newEvent } public func remakeEventData(withColor color: UIColor) -> EventData { - let newEvent = EventData(id: self.id, title: self.title, startDate: self.startDate, endDate: self.endDate, location: self.location, color: color, allDay: self.allDay) + let newEvent = EventData(id: self.id, title: self.title, startDate: self.startDate, endDate: self.endDate, location: self.location, color: color, allDay: self.allDay, eventTags: self.eventTags) newEvent.configureGradient(self.gradientLayer) return newEvent } public func remakeEventDataAsAllDay(forDate date: Date) -> EventData { - let newEvent = EventData(id: self.id, title: self.title, startDate: date.getStartOfDay(), endDate: date.getEndOfDay(), location: self.location, color: self.color, allDay: true) + let newEvent = EventData(id: self.id, title: self.title, startDate: date.getStartOfDay(), endDate: date.getEndOfDay(), location: self.location, color: self.color, allDay: true, eventTags: self.eventTags) newEvent.configureGradient(self.gradientLayer) return newEvent } @@ -297,5 +336,6 @@ struct EventDataEncoderKey { static let location = "EVENT_DATA_LOCATION" static let color = "EVENT_DATA_COLOR" static let allDay = "EVENT_DATA_ALL_DAY" + static let tags = "EVENT_DATA_TAGS" static let gradientLayer = "EVENT_DATA_GRADIENT_LAYER" } diff --git a/QVRWeekView/Classes/Common/EventLayer.swift b/QVRWeekView/Classes/Common/EventLayer.swift index 099f6ef..d16940f 100644 --- a/QVRWeekView/Classes/Common/EventLayer.swift +++ b/QVRWeekView/Classes/Common/EventLayer.swift @@ -25,6 +25,9 @@ class EventLayer: CALayer { self.backgroundColor = event.color.cgColor } + let xPadding = layout.eventLabelHorizontalTextPadding + let yPadding = layout.eventLabelVerticalTextPadding + // Configure event text layer let eventTextLayer = CATextLayer() eventTextLayer.isWrapped = true @@ -33,15 +36,195 @@ class EventLayer: CALayer { withMainFont: layout.eventLabelFont, infoFont: layout.eventLabelInfoFont, andColor: layout.eventLabelTextColor) - - let xPadding = layout.eventLabelHorizontalTextPadding - let yPadding = layout.eventLabelVerticalTextPadding + eventTextLayer.frame = CGRect( x: frame.origin.x + xPadding, y: frame.origin.y + yPadding, width: frame.width - 2 * xPadding, height: frame.height - 2 * yPadding) self.addSublayer(eventTextLayer) + + // Add tags at the bottom if available + if !event.eventTags.isEmpty { + let tagHeight = layout.tagHeight + let bottomMargin = layout.tagVerticalMargin + let tagsY = frame.origin.y + frame.height - yPadding - tagHeight - bottomMargin + + // Only render tags if there's enough space + if tagsY > frame.origin.y + yPadding + 20 { + let tagLeftInset = bottomMargin + addTagsLayers( + eventTags: event.eventTags, + x: frame.origin.x + tagLeftInset, + y: tagsY, + maxWidth: frame.width - 2 * tagLeftInset, + layout: layout, + eventColor: event.color) + } + } + } + + private func addTagsLayers(eventTags: [EventTag], x: CGFloat, y: CGFloat, maxWidth: CGFloat, layout: DayViewCellLayout, eventColor: UIColor) { + let tagHeight = layout.tagHeight + let tagSpacing = layout.tagSpacing + let tagCornerRadius = layout.tagCornerRadius + let tagTextSize = layout.tagTextSize + let tagPadding: CGFloat = 6 + let iconSize: CGFloat = tagHeight + + var currentX: CGFloat = x + + // Process tags + for eventTag in eventTags { + let tagName = eventTag.name + let tagColor = eventTag.color + + let tagLower = tagName.lowercased() + + // Try to load icon for any tag (automatically detects from Images.xcassets/tags/) + let iconImage = loadIconImage(named: tagLower) + + // Check if tag is emoji-only + let isEmojiOnly = isEmoji(tagName) + + // Calculate tag width + var tagWidth: CGFloat + let tagFont = UIFont(name: "Montserrat-Medium", size: tagTextSize) ?? UIFont.systemFont(ofSize: tagTextSize, weight: .medium) + + if iconImage != nil { + // Icon from asset + tagWidth = iconSize + } else if isEmojiOnly { + // Emoji-only: no background, just emoji text + let emojiSize = tagName.size(withAttributes: [.font: UIFont.systemFont(ofSize: tagTextSize + 4)]) + tagWidth = emojiSize.width + 4 // Small margin around emoji + } else { + // Text-only with color background + let tagText = tagName as NSString + let textWidth = tagText.size(withAttributes: [.font: tagFont]).width + tagWidth = textWidth + (tagPadding * 2) + } + + // Check if tag fits on current line + if currentX + tagWidth > x + maxWidth { + break // Stop if doesn't fit + } + + if let image = iconImage { + // Create icon layer from Assets (no background pill) + let iconLayer = CALayer() + iconLayer.contents = image.cgImage + iconLayer.frame = CGRect( + x: currentX, + y: y, + width: iconSize, + height: iconSize + ) + iconLayer.contentsGravity = .resizeAspect + iconLayer.compositingFilter = "destinationOut" + self.addSublayer(iconLayer) + } else if isEmojiOnly { + // Render emoji-only text without background (no foregroundColor set to preserve emoji colors) + let emojiTextLayer = CATextLayer() + let emojiFont = UIFont.systemFont(ofSize: tagTextSize + 4) + let emojiTextHeight = emojiFont.lineHeight + let emojiTextY = y + (tagHeight - emojiTextHeight) / 2 + + emojiTextLayer.frame = CGRect( + x: currentX, + y: emojiTextY, + width: tagWidth, + height: emojiTextHeight + ) + emojiTextLayer.string = tagName + emojiTextLayer.font = emojiFont + emojiTextLayer.fontSize = tagTextSize + 4 + emojiTextLayer.contentsScale = UIScreen.main.scale + emojiTextLayer.alignmentMode = .center + emojiTextLayer.isWrapped = false + self.addSublayer(emojiTextLayer) + } else { + // Text with tag color background + let tagBackgroundLayer = CALayer() + + tagBackgroundLayer.frame = CGRect(x: currentX, y: y, width: tagWidth, height: tagHeight) + tagBackgroundLayer.backgroundColor = tagColor.cgColor + tagBackgroundLayer.cornerRadius = tagCornerRadius + self.addSublayer(tagBackgroundLayer) + + // Create tag text layer with event color (creates stamp out effect) + let tagTextLayer = CATextLayer() + let tagText = tagName as NSString + let textWidth = tagText.size(withAttributes: [.font: tagFont]).width + + let textHeight = tagFont.lineHeight + let textY = y + (tagHeight - textHeight) / 2 + + tagTextLayer.frame = CGRect( + x: currentX + tagPadding, + y: textY, + width: textWidth, + height: textHeight + ) + tagTextLayer.string = tagName + tagTextLayer.font = tagFont + tagTextLayer.fontSize = tagTextSize + tagTextLayer.foregroundColor = eventColor.cgColor + tagTextLayer.contentsScale = UIScreen.main.scale + tagTextLayer.alignmentMode = .center + self.addSublayer(tagTextLayer) + } + + // Move x position for next tag + currentX += tagWidth + tagSpacing + } + } + + private func isEmoji(_ string: String) -> Bool { + let cleaned = string.trimmingCharacters(in: .whitespaces) + if cleaned.isEmpty { + return false + } + + // Check if all characters are emoji or zero-width joiners + for scalar in cleaned.unicodeScalars { + // Allow emoji and variation selectors/joiners + if scalar.properties.isEmoji || scalar == "\u{200D}" { // Zero-width joiner + continue + } + // If we encounter a non-emoji character, it's not emoji-only + if !scalar.isASCII { + continue + } + return false + } + return true + } + + private func loadIconImage(named: String) -> UIImage? { + // Try to load from main app bundle under tags namespace (Images.xcassets/tags/) + if let image = UIImage(named: "tags/\(named)", in: Bundle.main, compatibleWith: nil) { + return image + } + + // Try without namespace in main bundle + if let image = UIImage(named: named, in: Bundle.main, compatibleWith: nil) { + return image + } + + // Try from framework bundle under tags namespace + let bundle = Bundle(for: EventLayer.self) + + if let image = UIImage(named: "tags/\(named)", in: bundle, compatibleWith: nil) { + return image + } + + // Try without namespace in framework bundle + if let image = UIImage(named: named, in: bundle, compatibleWith: nil) { + return image + } + + return nil } required init?(coder aDecoder: NSCoder) { diff --git a/QVRWeekView/Classes/Common/Extensions.swift b/QVRWeekView/Classes/Common/Extensions.swift index 9b875fe..70a2513 100644 --- a/QVRWeekView/Classes/Common/Extensions.swift +++ b/QVRWeekView/Classes/Common/Extensions.swift @@ -209,4 +209,4 @@ extension Dictionary where Key == DayDate, Value == [EventData] { self[dayDate]!.append(event) } } -} +} \ No newline at end of file diff --git a/QVRWeekView/Classes/Views/DayScrollView.swift b/QVRWeekView/Classes/Views/DayScrollView.swift index f747250..7a976eb 100644 --- a/QVRWeekView/Classes/Views/DayScrollView.swift +++ b/QVRWeekView/Classes/Views/DayScrollView.swift @@ -286,6 +286,10 @@ UICollectionViewDelegate, UICollectionViewDataSource, DayViewCellDelegate, Frame self.weekView?.eventViewWasTapped(eventData) } + func eventViewWasLongPressedIn(_ dayViewCell: DayViewCell, withEventData eventData: EventData) { + self.weekView?.eventViewWasLongPressed(eventData) + } + func dayViewCellWasLongPressed(_ dayViewCell: DayViewCell, hours: Int, minutes: Int) { self.weekView?.dayViewCellWasLongPressed(dayViewCell, at: hours, and: minutes) for (_, dayViewCell) in dayViewCells { diff --git a/QVRWeekView/Classes/Views/DayViewCell.swift b/QVRWeekView/Classes/Views/DayViewCell.swift index 366c125..a09c772 100644 --- a/QVRWeekView/Classes/Views/DayViewCell.swift +++ b/QVRWeekView/Classes/Views/DayViewCell.swift @@ -264,7 +264,13 @@ class DayViewCell: UICollectionViewCell, CAAnimationDelegate { return } - let yTouch = sender.location(ofTouch: 0, in: self).y + let touchPoint = sender.location(ofTouch: 0, in: self) + if sender.state == .began, let eventData = eventData(at: touchPoint) { + self.delegate?.eventViewWasLongPressedIn(self, withEventData: eventData) + return + } + + let yTouch = touchPoint.y let previewPos = self.previewPosition(forYCoordinate: yTouch) if sender.state == .began { @@ -378,6 +384,15 @@ class DayViewCell: UICollectionViewCell, CAAnimationDelegate { return CGPoint(x: self.frame.width / 2, y: yCoord) } + private func eventData(at point: CGPoint) -> EventData? { + for (id, frame) in eventFrames { + if frame.contains(point), let eventData = eventsData[id] { + return eventData + } + } + return nil + } + private static func genUniqueId() -> Int { var id: Int! repeat { @@ -395,6 +410,8 @@ protocol DayViewCellDelegate: class { func eventViewWasTappedIn(_ dayViewCell: DayViewCell, withEventData eventData: EventData) + func eventViewWasLongPressedIn(_ dayViewCell: DayViewCell, withEventData eventData: EventData) + } // Helper function inserted by Swift 4.2 migrator. diff --git a/QVRWeekView/Classes/Views/WeekView.swift b/QVRWeekView/Classes/Views/WeekView.swift index e78bc3d..7caefcf 100644 --- a/QVRWeekView/Classes/Views/WeekView.swift +++ b/QVRWeekView/Classes/Views/WeekView.swift @@ -360,6 +360,13 @@ open class WeekView: UIView { self.delegate?.didTapEvent(in: self, withId: eventData.id) } + /** + Method delegates event view long presses, and sends a callback with the event id up to the WeekViewDelegate. + */ + func eventViewWasLongPressed(_ eventData: EventData) { + self.delegate?.didLongPressEvent?(in: self, withId: eventData.id) + } + /** Method delegates day view cell long presses, and sends a callback the pressed time up to the WeekViewDelegate. */ @@ -774,6 +781,8 @@ open class WeekView: UIView { func didTapEvent(in weekView: WeekView, withId eventId: String) + @objc optional func didLongPressEvent(in weekView: WeekView, withId eventId: String) + func eventLoadRequest(in weekView: WeekView, between startDate: Date, and endDate: Date) @objc optional func activeDayChanged(in weekView: WeekView, to date: Date) diff --git a/README.md b/README.md index 8008507..d4c0845 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,39 @@ Below is a table of all customizable properties of the `WeekView` | velocityOffsetMultiplier:`CGFloat` | Sensitivity for horizontal scrolling. A higher number will multiply input velocity more and thus result in more cells being skipped when scrolling. | `0.75` | | horizontalScrolling:`HorizontalScrolling` | Used to determine horizontal scrolling behaviour. `.infinite` is infinite scrolling, `.finite(number, startDate)` is finite scrolling for a given number of days from the starting date. | `.infinite` +### Event Tags + +Events support tags which are displayed at the bottom of event cells. Tags can be text labels or icons. + +#### Using Tags + +Add tags to events by passing a string array: + +```swift +let event = EventData( + id: "1", + title: "Meeting", + startDate: startDate, + endDate: endDate, + location: "Room 101", + color: .blue, + allDay: false, + tags: ["Work", "Important"] +) +``` + +#### Custom Tag Icons + +These tags will display as icons instead of text if you add them to your app's Assets.xcassets + +To add your own custom tag icons: + - Open your app's `Assets.xcassets` + - Add a new Image Set for each icon (e.g., "meeting", "personal") + - Add images to the image sets + - Use the **image set name as the tag name** + +Tags without matching icons will be displayed as text tags with the event color. + ## How it works The main WeekView view is a subclass of UIView. The view layout is retrieved from the WeekView xib file. WeekView contains a top and side bar sub view. The side bar contains an HourSideBarView which displays the hours. WeekView also contains a DayScrollView (UIScrollView subclass) which controls vertical scrolling and also delegates and contains a DayCollectionView (UICollectionView subclass) which controls the horizontal scrolling. DayCollectionView cells are DayViewCells, whose view is generated programtically (due to inefficiencies caused by auto-layout).