Skip to content
Open

Tags #32

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion QVRWeekView.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
Empty file removed QVRWeekView/Assets/.gitkeep
Empty file.
27 changes: 20 additions & 7 deletions QVRWeekView/Classes/Common/EventData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,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
public let tags: [String]
// 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 } }

Expand All @@ -37,12 +39,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, tags: [String] = [], gradientLayer: CAGradientLayer? = nil) {
self.id = id
self.title = title
self.location = location
self.color = color
self.allDay = allDay
self.tags = tags
guard startDate.compare(endDate).rawValue <= 0 else {
self.startDate = startDate
self.endDate = startDate
Expand Down Expand Up @@ -119,6 +122,7 @@ open class EventData: NSObject, NSCoding {
coder.encode(location, forKey: EventDataEncoderKey.location)
coder.encode(color, forKey: EventDataEncoderKey.color)
coder.encode(allDay, forKey: EventDataEncoderKey.allDay)
coder.encode(tags, forKey: EventDataEncoderKey.tags)
coder.encode(gradientLayer, forKey: EventDataEncoderKey.gradientLayer)
}

Expand All @@ -131,13 +135,15 @@ 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)
let dTags = coder.decodeObject(forKey: EventDataEncoderKey.tags) as? [String] ?? []
self.init(id: dId,
title: dTitle,
startDate: dStartDate,
endDate: dEndDate,
location: dLocation,
color: dColor,
allDay: dAllDay,
tags: dTags,
gradientLayer: dGradientLayer)
} else {
return nil
Expand All @@ -152,7 +158,8 @@ 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.tags == rhs.tags)
}

public override var hash: Int {
Expand All @@ -167,8 +174,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(
Expand Down Expand Up @@ -204,19 +216,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, tags: self.tags)
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, tags: self.tags)
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, tags: self.tags)
newEvent.configureGradient(self.gradientLayer)
return newEvent
}
Expand Down Expand Up @@ -297,5 +309,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"
}
130 changes: 127 additions & 3 deletions QVRWeekView/Classes/Common/EventLayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -33,15 +36,136 @@ 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.tags.isEmpty {
let tagHeight: CGFloat = 18
let bottomMargin: CGFloat = 4
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 {
addTagsLayers(
tags: event.tags,
x: frame.origin.x + xPadding,
y: tagsY,
maxWidth: frame.width - 2 * xPadding,
tagHeight: tagHeight,
eventColor: event.color)
}
}
}

private func addTagsLayers(tags: [String], x: CGFloat, y: CGFloat, maxWidth: CGFloat, tagHeight: CGFloat, eventColor: UIColor) {
let tagSpacing: CGFloat = 4
let tagPadding: CGFloat = 6
let tagCornerRadius: CGFloat = tagHeight / 2
let iconSize: CGFloat = tagHeight // Icons same height as pills

var currentX: CGFloat = x

for tag in tags {
let tagLower = tag.lowercased()

// Try to load icon for any tag (automatically detects from Images.xcassets/tags/)
let iconImage = loadIconImage(named: tagLower)

// Calculate tag width
var tagWidth: CGFloat
let tagFont = UIFont(name: "Montserrat-Medium", size: 10) ?? UIFont.systemFont(ofSize: 10, weight: .medium)

if iconImage != nil {
tagWidth = iconSize // Just icon width, no padding
} else {
// Text only tags with padding (fallback when icon not found)
let tagText = tag 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
// Use destination out blend mode for transparent icons
iconLayer.compositingFilter = "destinationOut"
self.addSublayer(iconLayer)
} else {
// Create tag background layer (white pill for text fallback)
let tagBackgroundLayer = CALayer()
tagBackgroundLayer.frame = CGRect(x: currentX, y: y, width: tagWidth, height: tagHeight)
tagBackgroundLayer.backgroundColor = UIColor.white.cgColor
tagBackgroundLayer.cornerRadius = tagCornerRadius
self.addSublayer(tagBackgroundLayer)

// Create tag text layer with Montserrat Medium
let tagTextLayer = CATextLayer()
let tagText = tag as NSString
let textWidth = tagText.size(withAttributes: [.font: tagFont]).width

tagTextLayer.frame = CGRect(
x: currentX + tagPadding,
y: y + 3,
width: textWidth,
height: tagHeight - 6
)
tagTextLayer.string = tag
tagTextLayer.font = tagFont
tagTextLayer.fontSize = 10
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 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) {
Expand Down
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,51 @@ 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

The library includes built-in icon support for: `bed`, `alert`, `fail`, `success`, `drink`. 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:

1. **Add to your app's Asset Catalog** (Recommended):
- Open your app's `Assets.xcassets`
- Add a new Image Set for each icon (e.g., "meeting", "personal")
- Add PNG or PDF images to the image sets
- Use the image set name as the tag name

2. **Using PNG/PDF files**:
- Add PNG or PDF files to your app bundle
- Name them to match your tag names (e.g., "meeting.png")
- The library will automatically find and use them

The library searches for icons in this order:
1. Main app bundle's Asset Catalog
2. Framework bundle's Asset Catalog
3. Framework's Assets/tags directory (PNG/PDF)

Tags without matching icons will be displayed as text pills 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).
Expand Down